Формат файла .NET – внутренняя структура сигнатур, часть 2 из 2 - Сигнатуры
ОГЛАВЛЕНИЕ
1. Сигнатуры (продолжение)
Здесь приведено продолжение первой части.
1.1 LocalVarSig
Сигнатура LocalVarSig также индексируется столбцом StandAloneSig.Signature, он хранит тип всех локальных переменных, выделенных во время выполнения метода. Элемент LOCAL_SIG является прологом сигнатуры и имеет постоянное значение 0x07, элемент Count является сжатым беззнаковым целым, хранящим количество локальных переменных, принадлежащих связанному с ними методу, элемент BYREF является сокращением константы ELEMENT_TYPE_BYREF (смотрите константы в первой части) и показывает, что элемент Type(тип) указывает на реальную переменную. Элемент Constraint(ограничение) показывает, что целевой тип не будет перемещаться сборщиком мусора при выполнении восстановления памяти, так как локальные переменные находятся в стеке (где сборщик мусора не выполняет никаких действий), Type(тип) переменной должен быть ссылочным типом (как System.Object – выделяется в куче) или типом значения (как System.Decimal – выделяется в стеке), но если целевой тип (закрепленный) является типом значения, его определение должно содержать элемент BYREF, в данном случае ссылка на переменную хранится в стеке, но сама переменная выделяется в куче. Более подробно о закреплении рассказано здесь. На рисунке 1 ниже показана полная схема синтаксиса для этой сигнатуры.
Обратите особое внимание на элемент TYPEDBYREF на схеме ниже: это типизированная ссылка, она содержит не только управляемый указатель (как нормальная ссылка) на адрес, но и динамическое представление данных. Процитируем описание элемента из спецификации: "Сигнатура локальной переменной типизированной ссылки указывает, что локальная переменная будет содержать управляемый указатель на адрес и динамическое представление типа, хранящиеся по этому адресу. Сигнатура типизированной ссылки похожа на константу byref, но тогда как byref определяет тип как часть ограничения byref (и, стало быть, статически как часть описания типа), типизированная ссылка динамически предоставляет информацию о типе. Типизированная ссылка, по сути, является полной сигнатурой и не сочетается с другими ограничениями. В частности, невозможно задать byref, тип которого является типизированной ссылкой".
Типизированная ссылка также очень помогает при передаче byref распакованных данных (т.е. данные, хранящиеся в стеке, всегда являются типами значений) в методы, не ограниченные статически принимаемым ими типом и требующие, наряду с передачей управляемого указателя на адрес, статического типа адреса: типизированная ссылка удовлетворяет эти потребности. Заметьте, что параметр «типизированная ссылка» также может ссылаться на адрес, находящийся в стеке, и время жизни этого адреса будет ограничено временем выполнения метода (в течение которого выделяется типизированная ссылка), а значит, компилятор CIL проверяет время жизни byref и параметра типизированной ссылки (подробней читайте в §12.4.1.5.2 в спецификации ECMA-355). Типизированная ссылка представлена в .NET BCL (библиотека базовых классов) в виде структуры TypedReference.
Рисунок 1. Схема синтаксиса сигнатуры LocalVarSig
Пример 1
Данный пример показывает объявление типов значений byref в стеке (только), пример кода написан на языке CIL (общий промежуточный язык) и выглядит так.
// Полный исходник: LocalVarSig\1.il
// Двоичный файл: LocalVarSig\1.dll
// (...)
.method public static void TestMethod()
{
.locals init(int32 &IntVarByRef)
ret
}
Сигнатура LocalVarSig для этого примера кода приведена в таблице ниже.
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
пролог сигнатуры (константа |
|
|
Общее число переменных, объявленных в этом методе, равняется одной. |
|
|
Так как фактическая переменная хранится в динамической куче, присутствует элемент |
|
|
Тип переменной ( |
Пример 2
Пример ниже показывает, что происходит с сигнатурой при использовании типизированной ссылки: в начале объявляется переменная IntVar, в следующей строке получается типизированная ссылка с помощью ключевого слова __makeref (недокументировано и не совместимо с CLS) и сохраняется в переменной TypedByRefVar.
// Полный исходник: LocalVarSig\2.cs
// Двоичный файл: LocalVarSig\2.dll
// (...)
[CLSCompliant(false)]
public void TestMethod()
{
int IntVar = 0;
TypedReference TypedByRefVar = __makeref(IntVar);
}
LocalVarSig для этого примера выглядит, как показано ниже.
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
пролог сигнатуры (константа |
|
|
Общее число переменных, объявленных в этом методе, равняется двум. |
|
|
Тип первой переменной ( |
|
|
Тип второй переменной ( |
Пример 3
Рассмотрим немного более сложный пример: в этом примере кода создается класс TestDataClass, имеющий лишь один член по имени StringVarToBePinned типа string(строка). В методе TestMethod (помеченный как unsafe) создается экземпляр класса TestDataClass, в строке ниже закрепляется член StringVarToBePinned и ссылка на него присваивается указателю FixedVar с помощью ключевого слова fixed. Такая обработка гарантирует, что между фигурными скобками {и} член dataClass.StringVarToBePinned не будет перемещаться действиями сборщика мусора, а значит, указатель FixedVar на член всегда будет действителен внутри фигурных скобок ключевого слова fixed. В методе нельзя непосредственно объявить переменную, которая будет закреплена, потому что такое значение уже закреплено (помещено в стек), поэтому переменная должна быть обернута классом TestDataClass (помещенным в кучу).
// Полный исходник: LocalVarSig\3.cs
// Двоичный файл: LocalVarSig\3.dll
// компилировать с параметром "/опасный"
// (...)
public class TestDataClass
{
public string StringVarToBePinned;
}
public class TestClass
{
public unsafe void TestMethod()
{
TestDataClass dataClass = new TestDataClass();
fixed (char* FixedVar = dataClass.StringVarToBePinned) { }
}
}
Данный пример сложен еще по одной причине: в какой-то момент он использует еще не описанный элемент, то есть TypeDefOrRefEncoded, этот элемент определяет, в какой строке и в какой таблице метаданных (TypeDef, TypeRef или TypeSpec) описан заданный тип. Здесь эти элементы детально не разбираются, если хотите – перейдите сразу к описанию этого элемента в подразделе 5.2 TypeDefOrRefEncoded в следующем разделе. LocalVarSig для вышеприведенного кода рассмотрена в таблице ниже.
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
пролог сигнатуры (константа |
|
|
Общее число переменных, объявленных в этом методе, равняется трем. |
|
|
Тип первой переменной ( |
|
|
Тип первой переменной, являющийся классом |
|
|
Тип второй переменной ( |
|
|
Тип указателя из предыдущего байта ( |
|
|
Третья переменная закреплена, смотрите константы. |
|
|
Тип третьей закрепленной переменной ( |
1.2 CustomAttrib
Данная сигнатура хранит экземпляры пользовательских атрибутов, но немного отличается от рассмотренных ранее сигнатур. Главное отличие заключается в том, что CustomAttrib ,в отличие от, например, сигнатуры MethodRefSig, хранит значения параметров, заданных пользовательскому атрибуту, и не хранит типы параметров. То есть сигнатура CustomAttrib хранит лишь значения параметров (фиксированных и именованных), заданных при создании экземпляра пользовательского атрибута; информация об их типах и количестве не дублируется в сигнатуре. Сигнатура индексируется столбцом CustomAttribute.Value, столбец Parent(родитель) указывает, в какой таблице (TypeDef – для типа, MethodDef – для метода, и т.д.) и в какой строке описан приписанный элемент (метод, тип и т.д.). Есть и второе существенное различие по сравнению с другими сигнатурами: в сигнатуре CustomAttrib все двоичные значения хранятся с несжатым прямым порядком байтов, исключая элемент PackedLen (рассмотрен ниже) и размер сигнатуры. Не путайте пользовательский атрибут с пользовательским модификатором. Полная схема синтаксиса состоит из 4 частей, рассмотрим первую.
Рисунок 2a. Схема синтаксиса сигнатуры CustomAttrib
Пока она весьма простая, начинается с Prolog, имеющего постоянное значение 0x0001 и занимающего два байта (unsigned int16 – несжатый и с прямым порядком байтов). Затем идут фиксированные аргументы (FixedArg показан на рисунке 2b), их количество и типы могут быть получены путем изучения соответствующей строки конструктора в таблице метаданных MethodDef или MemberRef (если класс атрибута хранится в другой сборке): обратите внимание, что метод vararg нельзя использовать в качестве конструктора атрибута. Далее следует количество именованных параметров (NumNamed – двухбайтовый unsigned int16 - несжатый и с прямым порядком байтов), и в конце расположены сами именованные параметры, повторенные NumNamed раз.
Рисунок 2b. Схема синтаксиса сигнатуры CustomAttrib
Эта схема немного сложнее предыдущей, но тоже весьма простая: верхний путь на схеме показывает, что параметр не является одномерным массивом с нулевой базой (SZARRAY, смотрите константы в первой части); нижний путь показывает путь SZARRAY, являющийся массивом; количество элементов в массиве SZARRAY хранится в элементе NumElem типа int32 (несжатый и с прямым порядком байтов), занимающем 4 байта; если параметр SZARRAY является null(пустой), то NumNamed задано значение 0xFFFFFFFF. CLI разрешает использовать только одномерные массивы с нижней границей нуля (SZARRAY), одномерный массив с нулевой базой типа int32 является int32[], но не int32[,,] и также не int32[3...8]. Чтобы узнать больше о массивах в .NET, прочитайте статью Типы массивов в .NET из журнала MSDN.
Рисунок 2c. Схема синтаксиса сигнатуры CustomAttrib
Пожалуй, эта часть – самая непонятная из всех четырех. Формат, принимаемый Elem, зависит от следующих условий (взято из спецификации).
Если тип параметра простой (первая линия на схеме выше) (bool, char, float32, float64, int8, int16, int32, int64, unsigned int8,unsigned int16, unsigned int32 или unsigned int64), то 'блоб' содержит его двоичное значение (Val). (bool – один байт со значением 0 (false) или 1 (true); char – двухбайтный символ Юникод; а остальные имеют свой очевидный смысл.) Данная схема также используется, если тип параметра – enum – хранит значение целого типа, лежащего в основе enum(перечисление).
Если тип параметра - строка (вторая линия на схеме выше), то блоб содержит SerString – количество байтов PackedLen (сжатый и с обратным порядком байтов – добавлено автором), за которым следуют символы UTF8. Если строка равна null, ее PackedLen имеет значение 0xFF (без следующих символов). Если строка пустая (""), то PackedLen имеет значение 0x00 (без следующих символов).
Если тип параметра - System.Type (смотрите ключевое слово typeof – добавлено автором статьи) (тоже вторая линия на схеме выше), его значение хранится как SerString (как определено в предыдущем абзаце), представляющее его каноническое имя. Каноническое имя содержит сборку, где оно определено, его версию, культуру и маркер открытого ключа. Если имя сборки не указано, CLI сначала ищет в текущей сборке, а затем в системной библиотеке (mscorlib); в этих двух особых случаях разрешено не указывать имя сборки, версию, культуру и маркер открытого ключа.
Если тип параметра - System.Object (третья линия на схеме выше), хранимое значение является "упакованным" экземпляром этого типа значения. В данном случае блоб содержит фактический FieldOrPropType типа (смотрите ниже), с последующим распакованным значением аргумента. [Примечание: нельзя передать значение null в этом случае.]
Рисунок 2d. Схема синтаксиса сигнатуры CustomAttrib
Последняя часть показывает формат элемента NamedArg, являющегося именованным аргументом (полем или свойством). Так как поля и свойства могут иметь одинаковое имя, первым элементом является FIELD, имеющее постоянное однобайтовое значение 0x53, если именованный параметр ссылается на поле или PROPERTY, имеющее постоянное однобайтовое значение 0x54, если именованный параметр ссылается на свойство. Далее идет элемент FieldOrPropType, описывающий тип именованного свойства или поля в одном или двух байтах: если тип именованного параметра – распакованный простой тип значения (определен выше), то FieldOrPropType должен содержать ровно одно постоянное значение связанного с ним типа (BOOLEAN, CHAR, I1, U1, I2, U2, I4, U4, I8, U8, R4, R8, STRING – смотрите таблицу констант в первой части), но если тип именованного параметра – упакованный простой тип значения, то перед элементом FieldOrPropType стоит байт, содержащий значение 0x51, в этом случае FieldOrPropType занимает два байта. Элемент FieldOrPropName является SerString (объяснен выше), содержащим имя свойства или поля. В конце идет один элемент FixedArg, показанный выше. Элемент NamedArg является нормальным FixedArg, перед которым стоит некоторая дополнительная информация, указывающая, какое поле или свойство он обозначает.
Пример 1
Данный пример показывает формат элемента SerString и как CustomAttrib различает поля и свойства, служащие именованными параметрами. В примере ниже имеется атрибут TestAttribute, требующий передачи одного фиксированного параметра Fixed1 типа int32, дополнительно можно (и это делается) передать два дополнительных именованных параметра типа int16 и string, как показано в куске кода ниже.
// Полный исходник: CustomAttrib\1.cs
// Двоичный файл: CustomAttrib\1.dll
// (...)
[AttributeUsage(AttributeTargets.Class)]
public class TestAttribute : Attribute
{
public TestAttribute(int Fixed1) { }
public short Named1 { get; set; }
public string Named2;
}
[Test(1, Named1 = 1, Named2 = "Abcd")]
public class TestClass { }
Полная сигнатура CustomAttrib для этого случая имеет длину 33 байта, поэтому в некоторых точках несколько байтов были объединены в одну строку с общим описанием.
Смещение |
Значение |
Что означает |
|
|
Размер сигнатуры хранится в виде сжатого целого с обратным порядком байтов. |
|
|
Пролог хранится в виде несжатого значения |
|
|
Значение первого фиксированного аргумента атрибута ( |
|
|
Количество именованных параметров, заданных в атрибуте, обозначено элементом |
|
|
Значение этого байта показывает, что целевой именованный параметр представлен свойством (смотрите константы в первой части), это элемент |
|
|
Тип целевого свойства ( |
|
|
Это строка |
|
|
Значение первого именованного аргумента атрибута ( |
|
|
Значение этого байта показывает, что целевой именованный параметр представлен полем (смотрите константы в первой части), это элемент |
|
|
Тип целевого поля ( |
|
|
Это вновь строка |
|
|
Значение второго именованного аргумента атрибута ( |
Пример 2
В этом примере показан формат сигнатуры при использовании System.Type, SZARRAY и упакованных типов значений в качестве аргументов атрибута TestAttribute, определенного ниже.
// Полный исходник: CustomAttrib\2.cs
// Двоичный файл: CustomAttrib\2.dll
// (...)
[AttributeUsage(AttributeTargets.Class)]
public class TestAttribute : Attribute
{
public TestAttribute(object Param1, int[] Param2, Type Param3) { }
}
[Test(1, new int[] {1, 2, 3}, typeof(string))]
public class TestClass { }
Как и в прошлом примере, сигнатура очень длинная (занимает 116 байтов), поэтому она разбита на более мелкие части.
Смещение |
Значение |
Что означает |
|
|
Размер сигнатуры хранится в виде сжатого целого с обратным порядком байтов. |
|
|
Пролог хранится в виде несжатого значения |
|
|
Тип первого фиксированного аргумента ( |
|
|
Значение, тип которого был задан в предыдущем байте, так как типом значения является |
|
|
Далее идет определение второго параметра, так как второй аргумент является одномерным массивом с нулевой базой ( |
|
|
Значение первого элемента массива во втором параметре, длиной 4 байта, так как тип массива - |
|
|
Значение второго элемента массива во втором параметре, длиной 4 байта, так как тип массива - |
|
|
Значение третьего элемента массива во втором параметре, длиной 4 байта, так как тип массива |
|
|
|
|
|
Два заключительных байта, не входящие в предыдущую |
1.3 MethodSpec
Сигнатура MethodSpec простая, она описывает каждую реализацию обобщенного метода, индексируется столбцом MethodSpec.Signature, и ее синтаксис имеет следующий вид, начинаясь с пролога GENRICINST (видите ли вы отсутствующую "E" ?), имеющего однобайтное значение 0x0A (значение этой константы отличается от значения константы ELEMENT_TYPE_GENERICINST, определенной в таблице констант в первой части), где Type повторяется GenArgCount раз.
MethodSpecBlob ::=
GENRICINST GenArgCount Type Type*
Пример 1
В примере ниже реализован обобщенный метод TestMethod с заданием трех обобщенных аргументов.
// Полный исходник: MethodSpec\1.cs
// Двоичный файл: MethodSpec\1.dll
// (...)
public class TestClass
{
public void TestMethod<GenArg1, GenArg2, GenArg3>() { }
}
public class TestRunClass
{
public void TestRunMethod()
{
new TestClass().TestMethod<short, int, string>();
}
}
MethodSpec для этого случая выглядит так:
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
пролог |
|
|
Количество обобщенных аргументов, заданных в обобщенном методе. |
|
|
Тип первого параметра ( |
|
|
Тип второго параметра ( |
|
|
Тип третьего параметра ( |
1.4 TypeSpec
Сигнатура TypeSpec индексируется столбцом TypeSpec.Signature и используется при: реализации типа в виде многомерного массива; реализации типа в виде одномерного массива, перед которым стоит пользовательский модификатор(ы); реализации обобщенного типа и иных действиях, как показано на схеме ниже. Поскольку некоторые элементы еще не объяснены (такие как пользовательские модификаторы, формы массивов), используется лишь ограниченная функциональность сигнатуры TypeSpec. В следующей главе рассматриваются элементы CustomMod, ArrayShape, TypeDefOrRefEncoded, и мы вернемся к сигнатуре TypeSpec и воспользуемся остальными возможностями сигнатуры. Также заметьте, что в отличие от предыдущего примера, где константа/пролог GENRICINST (отсутствует "E") также используется, в TypeSpec используется константа ELEMENT_TYPE_GENERICINST, определенная в таблице общих констант (в первой части статьи).
TypeSpecBlob ::=
PTR CustomMod* VOID
| PTR CustomMod* Type
| FNPTR MethodDefSig
| FNPTR MethodRefSig
| ARRAY Type ArrayShape
| SZARRAY CustomMod* Type
| GENERICINST (CLASS | VALUETYPE) TypeDefOrRefEncoded GenArgCount Type Type*
Пример 1
В этом примере реализован обобщенный тип TypeSpec, как показано в куске кода ниже.
// Полный исходник: TypeSpec\1.cs
// Двоичный файл: TypeSpec\1.dll
// (...)
public class TestClass<GenArg1, GenArg2> { }
public class TestRunClass
{
public void TestRunMethod()
{
TestClass<int, string> TestVar = new TestClass<int, string>();
}
}
TypeSpec для этого случая выглядит так:
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
константа |
|
|
тип обобщенного типа ( |
|
|
Реализованный обобщенный тип описан в таблице метаданных |
|
|
Количество обобщенных аргументов, заданных в типе, равно двум. |
|
|
тип первого обобщенного параметра ( |
|
|
тип второго обобщенного параметра ( |
1.5 MarshalSpec
Сигнатура MarshalSpec генерируется при использовании атрибута MarshalAs в полях, параметрах и возвращаемых параметрах. Она задает, как данные должны преобразовываться при вызове из/в неуправляемый код через платформозависимый вызов. Сигнатура индексируется в столбце FieldMarshal.NativeType ( имя таблицы метаданных неверно, в действительности не важно, MarshalSpec описывает поле, параметр или возвращаемый параметр), она всегда индексируется вышеназванным столбцом. Элементы ParamNum и NumElem в листинге синтаксиса ниже описывают, соответственно, параметр в вызове метода, дающий количество элементов в массиве, количество элементов или дополнительных элементов. И те, и другие элементы хранятся в сигнатуре как сжатые целые, их цель – помогать вычислить полный размер в байтах, занимаемый массивом в памяти. Специфичная для Microsoft реализация дескриптора преобразования богаче, чем описано здесь, и использует дополнительные константы и расширенный синтаксис. Дополнительные сведения о реализации Microsoft MarshalSpec смотрите в спецификации метаданных часть II - раздел §23.4.
MarshalSpec ::=
NativeIntrinsic
| ARRAY ArrayElemType
| ARRAY ArrayElemType ParamNum
| ARRAY ArrayElemType ParamNum NumElem
ArrayElemType ::=
NativeIntrinsic
NativeIntrinsic ::=
BOOLEAN | I1 | U1 | I2 | U2 | I4 | U4 | I8 | U8 | R4 | R8
| LPSTR | LPSTR | INT | UINT | FUNC
Для вычисления размера массива в байтах применяется следующий псевдокод, где @ParamNum означает значение, переданное для номера параметра ParamNum.
if ParamNum = 0
SizeInBytes = NumElem * sizeof (elem)
else
SizeInBytes = ( @ParamNum + NumElem ) * sizeof (elem)
endif
Таблица констант для данной сигнатуры приведена ниже, в вышеприведенных синтаксических дескрипторах и примерах в этом подразделе: вместо полных имен констант используются сокращения.
Имя |
Значение |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Пример 1
Начнем с простейшего примера, показанного в листинге кода ниже.
// Полный исходник: MarshalSpec\1.cs
// Двоичный код: MarshalSpec\1.dll
// (...)
[MarshalAs(UnmanagedType.LPWStr)]
public string TestField;
Этот код сгенерировал следующую сигнатуру MarshalSpec.
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
Поле |
Пример 2
Пришло время для более сложного примера – мы преобразуем массив типа int32 в LPArray (указатель на первый элемент массива в стиле C). Поскольку такой тип массива не дает информации о ранге и границах связанных данных массива, надо указать, какой параметр метода отвечает за предоставление информации о том, сколько элементов имеется в массиве. Это делается путем задания необязательного параметра SizeParamIndex, вдобавок к нему устанавливается необязательный параметр SizeConst, указывающий, что массив Param1 содержит еще 10 элементов, наряду с заданным аргументом ArraySize. Обратите внимание, что есть также тип массива SafeArray, являющийся самоописывающим массивом, содержащим тип, ранг и границы связанных данных, и не требующий задания дополнительных параметров в MarshalAsAttribute, но он специфичен для Microsoft и поэтому не описан здесь.
// Полный исходник: MarshalSpec\2.cs
// Двоичный файл: MarshalSpec\2.dll
// (...)
public void TestMethod(
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2, SizeConst = 10)] int[] Param1,
int ArraySize)
{
// пустая команда
}
Следующая сигнатура MarshalSpec должна генерироваться вышеприведенным кодом.
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
тип параметра преобразования ( |
|
|
Константа |
|
|
Параметр |
|
|
Параметр |
|
|
Параметр |