Наложение нескольких ссылочных полей 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 разрешит такое использование, если оно будет опасным неочевидным способом.