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

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

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 ]

3 голосов
/ 24 апреля 2010

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

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

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

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

2 голосов
/ 24 апреля 2010

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

Это будет безопаснее:

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.

2 голосов
/ 24 апреля 2010

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

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

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

1 голос
/ 30 июня 2013

Поскольку сборщик мусора не типизирован и различает только ссылки на объекты и простые биты, перекрывающиеся ссылки не будут его путать. Однако, хотя одна ссылка на объект может полностью перекрывать другую, это не поддается проверке, или небезопасно (стандарт 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.

0 голосов
/ 24 апреля 2010

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

...