Наложение нескольких ссылочных полей CLR друг с другом в явной структуре?

Изменить: я хорошо знаю, что это очень хорошо работает с типами значений, мой конкретный вопрос об использовании этого для ссылочных типов.

Edit2: я также знаю, что вы не можете накладывать ссылочные типы и типы значений в структуре, это только в случае наложения нескольких полей ссылочных типов друг на друга.

Я возился со структурами в.NET/C#, и я только что узнал, что вы можете сделать это:

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication1 {

    class Foo { }
    class Bar { }

    [StructLayout(LayoutKind.Explicit)]
    struct Overlaid {
        [FieldOffset(0)] public object AsObject;
        [FieldOffset(0)] public Foo AsFoo;
        [FieldOffset(0)] public Bar AsBar;
    }

    class Program {
        static void Main(string[] args) {
            var overlaid = new Overlaid();
            overlaid.AsObject = new Bar();
            Console.WriteLine(overlaid.AsBar);

            overlaid.AsObject = new Foo();
            Console.WriteLine(overlaid.AsFoo);
            Console.ReadLine();
        }
    }
}

В основном обходится необходимость выполнять динамическое приведение во время выполнения с помощью структуры, которая имеет явную разметку поля, а затем обращаться к объекту внутри, как к правильному типу.

Теперь мой вопрос: может ли это как-то привести к утечкам памяти или какому-либо другому неопределенному поведению внутри CLR? Или это полностью поддерживаемое соглашение, которое можно использовать без каких-либо проблем?

Мне известно, что это один из самых темных углов CLR, и этот метод является жизнеспособным вариантом лишь в очень немногих конкретных случаях.

5 ответов

Решение

Я не могу понять, как версия с явным макетом может быть проверена без каких-либо дополнительных проверок во время выполнения, поскольку она позволяет вам видеть ненулевую ссылку на что-то, что не имеет объявленного типа.

Это было бы безопаснее:

struct Overlaid { // could also be a class for reference-type semantics
    private object asObject;
    public object AsObject {get {return asObject;} set {asObject = value;} }
    public Foo AsFoo { get {return asObject as Foo;} set {asObject = value;} }
    public Bar AsBar { get {return asObject as Bar;} set {asObject = value;} }
}

Нет риска порванных ссылок и т. Д., И все же только одно поле. Он не связан с каким-либо рискованным кодом и т. Д. В частности, он не рискует чем-то глупым, например:

    [FieldOffset(0)]
    public object AsObject;
    [FieldOffset(0)]
    public Foo AsFoo;
    [FieldOffset(1)]
    public Bar AsBar; // kaboom!!!!

Другая проблема заключается в том, что вы можете поддерживать только одно поле таким образом, если вы не можете гарантировать режим CPU; Смещение 0 легко, но становится сложнее, если вам нужно несколько полей и нужно поддерживать x86 и x64.

Итак, вы нашли дыру в петле, CLR разрешает это, поскольку все перекрывающиеся поля являются объектами. Все, что позволит вам связываться со ссылкой на объект, будет отвергнуто с помощью TypeLoadException:

  [StructLayout(LayoutKind.Explicit)]
  struct Overlaid {
    [FieldOffset(0)]
    public object AsObject;
    [FieldOffset(0)]
    public IntPtr AsPointer;
  }

Но вы можете использовать это, предоставляя поля классов. Ничего действительно плохого не происходит, пока вы просто читаете значения полей, например, вы можете получить значение дескриптора отслеживания.

Однако запись этих полей приводит к исключению ExecutionEngineException. Однако я думаю, что это эксплойт, если вы можете правильно угадать значение дескриптора отслеживания. Практическое использование достаточно близко к нулю, хотя.

Поскольку сборщик мусора нетипизирован и различает только ссылки на объекты и простые биты, перекрывающиеся ссылки не будут его путать. Однако, хотя одна ссылка на объект может полностью перекрывать другую, это не поддается проверке, или небезопасно (стандарт ECMA-335, стр. 180, II.10.7 Управление макетом экземпляра). Легко построить программу, которая использует эту непроверяемость для ужасного сбоя:

using System.Runtime.InteropServices;

class Bar
{
    public virtual void func() { }
}

[StructLayout(LayoutKind.Explicit)]
struct Overlaid
{
    [FieldOffset(0)]
    public object foo;

    [FieldOffset(0)]
    public Bar bar;
}

class Program
{
    static void Main(string[] args)
    {
        var overlaid = new Overlaid();
        overlaid.foo = new object();
        overlaid.bar.func();
    }
}

Здесь вызов func загружает указатель функции из одного последнего элемента виртуальной таблицы класса объекта. Согласно этой статье после vtbl есть таблица дескрипторов. Обработка его как указателя на функцию приводит к исключению System.AccessViolationException.

Если вы выровняете тип небезопасным способом, среда выполнения выдаст TypeLoadException под нагрузкой даже при компиляции с /unsafe, Так что я думаю, что вы в безопасности.

Я предполагаю - так как вы можете использовать StructLayout и скомпилировать свой код без /unsafe flags-- что это особенность CLR. Вам нужен атрибут StructLayout просто потому, что в C# нет прямого способа объявления типов таким образом.

Посмотрите на эту страницу, где подробно описывается, как структуры C# преобразуются в IL, и вы заметите, что существует множество схем памяти, встроенных в сам IL/CLR.

Я не знаю никаких проблем с этим. Кроме того, я сомневаюсь, что Microsoft разрешит такое использование, если оно будет опасным неочевидным способом.

Другие вопросы по тегам