Формат файла .NET – внутренняя структура сигнатур, часть 2 из 2

ОГЛАВЛЕНИЕ

Полное описание сигнатур, являющихся частью формата файла .NET.

•    Скачать двоичные DLL примеров программ – signatures_binary.zip - 16.07 Кб
•    Скачать исходный код примеров программ - signatures_source.zip - 6.14 KB

Оглавление

  • 1. Сигнатуры (продолжение)
    • 1.1 LocalVarSig
    • 1.2 CustomAttrib
    • 1.3 MethodSpec
    • 1.4 TypeSpec
    • 1.5 MarshalSpec
  • 2. Элементы
    • 2.1 CustomMod
    • 2.2 TypeDefOrRefEncoded
    • 2.3 Param
    • 2.4 RetType/span>
    • 2.5 Type
    • 2.6 ArrayShape
  • 3. Вывод

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 для этого примера кода приведена в таблице ниже.

Смещение

Значение

Что означает

0x05

0x04

размер сигнатуры

0x06

0x07

пролог сигнатуры (константа LOCAL_SIG).

0x07

0x01

Общее число переменных, объявленных в этом методе, равняется одной.

0x08

0x10

Так как фактическая переменная хранится в динамической куче, присутствует элемент BYREF значения 0x10.

0x09

0x08

Тип переменной (int32), смотрите константы в первой части.

Пример 2

Пример ниже показывает, что происходит с сигнатурой при использовании типизированной ссылки: в начале объявляется переменная IntVar, в следующей строке получается типизированная ссылка с помощью ключевого слова __makeref (недокументировано и не совместимо с CLS) и сохраняется в переменной TypedByRefVar.

// Полный исходник: LocalVarSig\2.cs
// Двоичный файл: LocalVarSig\2.dll
// (...)

[CLSCompliant(false)]
public void TestMethod()
{
    int IntVar = 0;
    TypedReference TypedByRefVar = __makeref(IntVar);
}

LocalVarSig для этого примера выглядит, как показано ниже.

Смещение

Значение

Что означает

0x1E

0x04

размер сигнатуры

0x1F

0x07

пролог сигнатуры (константаLOCAL_SIG)

0x20

0x02

Общее число переменных, объявленных в этом методе, равняется двум.

0x21

0x08

Тип первой переменной (int32), смотрите константы в первой части.

0x22

0x16

Тип второй переменной (TYPEDBYREF), смотрите константы в первой части.

Пример 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 для вышеприведенного кода рассмотрена в таблице ниже.

Смещение

Значение

Что означает

0x20

0x08

размер сигнатуры

0x21

0x07

пролог сигнатуры (константаLOCAL_SIG)

0x22

0x03

Общее число переменных, объявленных в этом методе, равняется трем.

0x23

0x12

Тип первой переменной (CLASS - за которым следует элемент TypeDefOrRefEncoded), смотрите константы в первой части.

0x24

0x08

Тип первой переменной, являющийся классом TestDataClass, описан в таблице метаданных TypeDef в строке 2, который является классом TestDataClass. Элемент TypeDefOrRefEncoded не объяснен в текущей главе.

0x25

0x0F

Тип второй переменной (PTR - за которым следует элементType), смотрите константы в первой части.

0x26

0x03

Тип указателя из предыдущего байта (char – в конце это char*), смотрите константы в первой части.

0x27

0x45

Третья переменная закреплена, смотрите константы.

0x28

0x0E

Тип третьей закрепленной переменной (string), смотрите константы.

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

Смещение

Значение

Что означает

0x3E

0x21

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

0x3F
0x40

0x01
0x00

Пролог хранится в виде несжатого значения unsigned int16 0x0001 с прямым порядком байтов.

0x41
0x42
0x43
0x44

0x01
0x00
0x00
0x00

Значение первого фиксированного аргумента атрибута (Fixed1) равняется 0x00000001и хранится в виде несжатого int32 с прямым порядком байтов. Это показывает верхняя линия на рисунке 2b и первый путь на рисунке 2c.

0x45
0x46

0x02
0x00

Количество именованных параметров, заданных в атрибуте, обозначено элементом NumNamed на рисунке 2a и хранится в виде unsigned int16с прямым порядком байтов. Задано ровно два необязательных параметра, и значение этого двухбайтного элемента равно 0x0002.

0x47

0x54

Значение этого байта показывает, что целевой именованный параметр представлен свойством (смотрите константы в первой части), это элемент PROPOERTY на рисунке 2d.

0x48

0x06

Тип целевого свойства (int16, смотрите константы в первой части). ЭтотбайтпредставленэлементомFieldOrPropType на рисунке 2d.

0x49
0x4A
0x4B
0x4C
0x4D
0x4E
0x4F

0x06
0x4E
0x61
0x6D
0x65
0x64
0x31

Это строка SerString, задающая имя целевого свойства (представлено элементом FieldOrPropName на рисунке 2d). SerString – нормальная строка юникод, перед которой стоит ее размер в байтах, размер хранится как сжатое целое с обратным порядком байтов. Строка занимает 6 байтов (смещение 0x49), так как имя строки содержит только символы из таблицы ASCII, каждый один символ занимает ровно один байт, можно легко прочитать текст строки Named1.

0x50
0x51

0x01
0x00

Значение первого именованного аргумента атрибута (Named1) равняется 0x00001и хранится в виде несжатого int16 с прямым порядком байтов. Это показывает верхняя линия на рисунке 2b и первый путь на рисунке 2c.

0x52

0x53

Значение этого байта показывает, что целевой именованный параметр представлен полем (смотрите константы в первой части), это элемент FIELD на рисунке 2d.

0x53

0x0E

Тип целевого поля (string, смотрите константы в первой части). ЭтотбайтпредставленэлементомFieldOrPropType на рисунке 2d.

0x54
0x55
0x56
0x57
0x58
0x59
0x5A

0x06
0x4E
0x61
0x6D
0x65
0x64
0x32

Это вновь строка SerString, задающая имя целевого свойства (представлено элементом FieldOrPropName на рисунке 2d). Длина этой строки – 6 байтов (смотрите на смещение 0x54), остальные байты похожи на предыдущую строку, отличается лишь последний байт, текстом строки является Named2, смотрите таблицу ASCII.

0x5B
0x5C
0x5D
0x5E
0x5F

0x04
0x41
0x62
0x63
0x64

Значение второго именованного аргумента атрибута (Named2) равняется Abcd (смотрите таблицу ASCII) и хранится как SerString. Это показывает верхняя линия на рисунке 2b и средний путь на рисунке 2c. Поскольку 0x5F - 0x3E = 0x21, т.е. конечное смещение – первое смещение = размер сигнатуры, сигнатура заканчивается здесь.

Пример 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 байтов), поэтому она разбита на более мелкие части.

Смещение

Значение

Что означает

0x2B

0x74

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

0x2C
0x2D

0x01
0x00

Пролог хранится в виде несжатого значенияunsigned int16 0x0001 с прямым порядком байтов.

0x2E

0x08

Тип первого фиксированного аргумента (int32– упакован внутри System.Object). Этот случай обозначен третьим путем на рисунке 2c, где сразу перед значением стоит тип значения.

0x2F
0x30
0x31
0x32

0x01
0x00
0x00
0x00

Значение, тип которого был задан в предыдущем байте, так как типом значения является int32, занимающий ровно 4 байта. Оно хранится с прямым порядком байтов, поэтому значение равно 0x00000001.

0x33
0x34
0x35
0x36

0x03
0x00
0x00
0x00

Далее идет определение второго параметра, так как второй аргумент является одномерным массивом с нулевой базой (SZARRAY). Эти 4 байта задают количество элементов, заданных в массиве второго параметра, это значение хранится как unsigned int32с прямым порядком байтов.

0x37
0x38
0x39
0x3A

0x01
0x00
0x00
0x00

Значение первого элемента массива во втором параметре, длиной 4 байта, так как тип массива - int32, значение - 0x00000001.

0x3B
0x3C
0x3D
0x3E

0x02
0x00
0x00
0x00

Значение второго элемента массива во втором параметре, длиной 4 байта, так как тип массива -int32, значение - 0x00000002.

0x3F
0x40
0x41
0x42

0x03
0x00
0x00
0x00

Значение третьего элемента массива во втором параметре, длиной 4 байта, так как тип массива - int32, значение - 0x00000003.

0x43
0x44
0x45
0x46
0x47
0x48
0x49
0x4A
0x4B
0x4C
0x4D
0x4E
0x4F
0x50
0x51
0x52
0x53
0x54
0x55
0x56
0x57
0x58
0x59
0x5A
0x5B
0x5C
0x5D
0x5E
0x5F
0x60
0x61
0x62
0x63
0x64
0x65
0x66
0x67
0x68
0x69
0x6A
0x6B
0x6C
0x6D
0x6E
0x6F
0x70
0x71
0x72
0x73
0x74
0x75
0x76
0x77
0x78
0x79
0x7A
0x7B
0x7C
0x7D
0x7E
0x7F
0x80
0x81
0x82
0x83
0x84
0x85
0x86
0x87
0x88
0x89
0x8A
0x8B
0x8C
0x8D
0x8E
0x8F
0x90
0x91
0x92
0x93
0x94
0x95
0x96
0x97
0x98
0x99
0x9A
0x9B
0x9C
0x9D

0x5A
0x53
0x79
0x73
0x74
0x65
0x6D
0x2E
0x53
0x74
0x72
0x69
0x6E
0x67
0x2C
0x20
0x6D
0x73
0x63
0x6F
0x72
0x6C
0x69
0x62
0x2C
0x20
0x56
0x65
0x72
0x73
0x69
0x6F
0x6E
0x3D
0x32
0x2E
0x30
0x2E
0x30
0x2E
0x30
0x2C
0x20
0x43
0x75
0x6C
0x74
0x75
0x72
0x65
0x3D
0x6E
0x65
0x75
0x74
0x72
0x61
0x6C
0x2C
0x20
0x50
0x75
0x62
0x6C
0x69
0x63
0x4B
0x65
0x79
0x54
0x6F
0x6B
0x65
0x6E
0x3D
0x62
0x37
0x37
0x61
0x35
0x63
0x35
0x36
0x31
0x39
0x33
0x34
0x65
0x30
0x38
0x39

SerString длиной 90 байтовописываетканоническоеимятипа, заданноевтретьемпараметре, имеетследующеезначениеSystem.String, mscorlib, Version(версия)=2.0.0.0, Culture(культура)=neutral(нейтральная), PublicKeyToken=b77a5c561934e089. Она показана средним путем на рисунке 2c

0x9E
0x9F

0x00
0x00

Два заключительных байта, не входящие в предыдущую SerString(0x9F - 0x44 != 0x5A), но являющиеся частью всего CustomAttrib (0x9F - 0x2C = 0x74) и не содержащие никаких данных. Вероятно, каноническое имя имеет некое выравнивание, и поэтому данные нули присутствуют. К сожалению, спецификация ничего не говорит о них.

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 для этого случая выглядит так:

Смещение

Значение

Что означает

0x18

0x05

размер сигнатуры

0x19

0x0A

пролог

0x1A

0x03

Количество обобщенных аргументов, заданных в обобщенном методе.

0x1B

0x06

Тип первого параметра (int16), смотрите константы в первой части.

0x1C

0x08

Тип второго параметра (int32), смотрите константы в первой части.

0x1D

0x0E

Тип третьего параметра (string), смотрите константы в первой части.

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 для этого случая выглядит так:

Смещение

Значение

Что означает

0x13

0x06

размер сигнатуры

0x14

0x15

константа ELEMENT_TYPE_GENERICINST, смотрите таблицу констант в первой части.

0x15

0x12

тип обобщенного типа (CLASS), смотрите таблицу констант в первой части.

0x16

0x08

Реализованный обобщенный тип описан в таблице метаданных TypeDef в строке 2, элемент TypeDefOrRefEncoded не объяснен в текущей главе.

0x17

0x02

Количество обобщенных аргументов, заданных в типе, равно двум.

0x18

0x08

тип первого обобщенного параметра (int32), смотрите константы в первой части.

0x19

0x0E

тип второго обобщенного параметра (string), смотрите константы в первой части.

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

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

Имя

Значение

NATIVE_TYPE_BOOLEAN

0x02

NATIVE_TYPE_I1

0x03

NATIVE_TYPE_U1

0x04

NATIVE_TYPE_I2

0x05

NATIVE_TYPE_U2

0x06

NATIVE_TYPE_I4

0x07

NATIVE_TYPE_U4

0x08

NATIVE_TYPE_I8

0x09

NATIVE_TYPE_U8

0x0A

NATIVE_TYPE_R4

0x0B

NATIVE_TYPE_R8

0x0C

NATIVE_TYPE_LPSTR

0x14

NATIVE_TYPE_LPWSTR

0x15

NATIVE_TYPE_INT

0x1F

NATIVE_TYPE_UINT

0x20

NATIVE_TYPE_FUNC

0x26

NATIVE_TYPE_ARRAY

0x2A

NATIVE_TYPE_MAX

0x50

Пример 1

Начнем с простейшего примера, показанного в листинге кода ниже.

// Полный исходник: MarshalSpec\1.cs
// Двоичный код: MarshalSpec\1.dll
// (...)

[MarshalAs(UnmanagedType.LPWStr)]
public string TestField;

Этот код сгенерировал следующую сигнатуру MarshalSpec.

Смещение

Значение

Что означает

0x1C

0x01

размер сигнатуры

0x1D

0x15

Поле TestField преобразуется вLPWSTRв неуправляемом коде.

Пример 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 должна генерироваться вышеприведенным кодом.

Смещение

Значение

Что означает

0x1B

0x05

размер сигнатуры

0x1C

0x2A

тип параметра преобразования (ARRAY), смотрите таблицу констант для дескриптора преобразования.

0x1D

0x50

Константа MAX (смотрите таблицу констант для дескриптора преобразования) показывает, что этот массив не дает информации о типе элементов массива.

0x1E

0x02

Параметр ParamNum хранится как сжатое целое.

0x1F

0x0A

Параметр NumElem хранится как сжатое целое.

0x20

0x01

Параметр ElemMult хранится как сжатое целое. Это странный параметр, спецификация упоминает его лишь два раза, говоря, что если преобразуемый тип является ARRAY, то ElemMult должен быть установлен в 0x01, но не оговаривает его смысл и его местоположение в сигнатуре MarshalSpec (смотрите раздел §22.17 в спецификации метаданных часть II).


2. Элементы

Были рассмотрены все сигнатуры, но это не конец. Сигнатуры состоят из более мелких частей, именуемых "элементы", они были отделены, так как находятся в составе более чем одной сигнатуры, а значит, не надо повторять объяснение для конкретного элемента(ов) в каждой сигнатуре. В данной главе они разобраны более детально.

2.1 CustomMod

Этот элемент часто повторялся в разобранных сигнатурах, и поэтому он идет первым. Пользовательские модификаторы похожи на пользовательские атрибуты, но, в отличие от них, пользовательские модификаторы входят в состав сигнатуры. Пользовательские модификаторы определяются в CIL с помощью ключевых слов modreq (обязательный модификатор) и modopt (необязательный модификатор) в объявлении метода, оба требуют задания типа (класс или структура) в качестве их "аргумента". Две сигнатуры, различающиеся только по добавлению пользовательского модификатора (обязательного или необязательного), не должны считаться совпадающими, и, как говорит спецификация:
Различие между обязательными и необязательными модификаторами важно для инструментов, отличных от CLI, имеющих дело с метаданными, как правило, компиляторов и анализаторов программ. Обязательный модификатор показывает, что в измененном элементе  есть специальная семантика, которую нельзя игнорировать, тогда как необязательный модификатор можно игнорировать. Например, префикс const(константа) в языке программирования C может быть оформлен необязательным модификатором, так как вызывающий оператор метода, имеющий параметр с префиксом const, не должен обрабатывать его особым образом. Наоборот, параметр, который должен быть создан с помощью копирующего конструктора в C++, должен быть помечен обязательным пользовательским атрибутом, так как он является вызывающим оператором, делающим копию.

К сожалению, C# имеет проблемы с обработкой параметров с прикрепленными пользовательскими модификаторами, читайте об этом в статьях Modopt, сигнатуры методов, и неполные спецификации и Подробнее о modopt на CodeBetter.com.
CMOD_OPT и CMOD_REQD – константы, определенные в таблице констант в первой части, элементы TypeDefEncoded и TypeRefEncoded являются одним элементом TypeDefOrRefEncoded, тщательно разобранным в следующем разделе. Заметьте, что к полю, свойству, параметру или возвращаемому параметру может быть прикреплено ноль, один или больше CustomMod. Нельзя определить пользовательский модификатор с помощью C#, исключая System.Reflection.Emit. В пространстве имен System.Runtime.CompilerServices есть несколько индикаторов, которые можно применить к пользовательскому модификатору, например, CallConvCdecl, IsConst, IsLong.

 

Рисунок 3. Схема синтаксиса элемента CustomMod

Пример 1

В примере ниже поле TestField помечено модификатором modreq, так как CustomMod находится внутри сигнатуры FieldSig, показанной в самом начале статьи на рисунке 2. Индикатор IsLong отличает long от целого в C++, но в данном случае в основе этого пользовательского модификатора нет специальной семантики, лишь показан формат элемента CustomMod в сигнатуре. Значение элемента TypeDefOrRefEncoded показано дважды, в двух системах счисления - шестнадцатеричной (подпись внизу 16) и двоичной (подпись внизу 2). В следующем подразделе узнаете, почему.

// Полный исходник: CustomMod\1.il
// Двоичный файл: CustomMod\1.dll
// (...)

.field public int64 modreq([mscorlib]System.CompilerServices.IsLong) TestField

Таблица ниже показывает всю сигнатуру FieldSig, индексируемую столбцом Field.Signature, вместе с встроенным пользовательским модификатором, сгенерированным ключевым словом modreq.

Смещение

Значение

Что означает

0x01

0x04

размер сигнатуры

0x02

0x06

пролог FieldSig

0x03

0x1F

встреченный пользовательский обязательный модификатор (modreq), смотрите константы в первой части.

0x04

0x0516 
000001012

Элемент TypeDefOrRefEncoded, в данном случае он указывает на первую строку таблицы TypeRef, то есть на класс IsLong. Этот элемент описан в следующем подразделе.

0x05

0x0A

тип поля (int64), смотрите константы в первой части.

 2.2 TypeDefOrRefEncoded

Теперь рассмотрим самый загадочный элемент TypeDefOrRefEncoded, он не так сложен, как кажется. Этот элемент определяет, в какой таблице метаданных и в какой строке таблицы хранится информация о ссылочном типе. Первые два самые младшие биты кодируют таблицу метаданных: 0 - для TypeDef (ссылочный тип хранится в текущей сборке), 1 – для TypeRef (ссылочный тип хранится в отдельной сборке), и 2 - для TypeSpec (ссылочный тип является обобщенным типом, массивом, и т.д., смотрите раздел 4.9 TypeSpec). Остальные биты кодируют индекс строки: обратите внимание, что индексы начинается с единицы, то есть первая строка в любой таблице метаданных всегда имеет индекс 1, а не 0.

Пример 1

В этом примере объявлено одно поле с прикрепленным к нему пользовтельским обязательным модификатором, modreq принимат в качестве аргумента тип TestClass, объявленный в той же сборке, как показано ниже.

// Полный исходник: TypeDefOrRefEncoded\1.il
// Двоичный файл: TypeDefOrRefEncoded\1.dll
// (...)

.class public TestClass extends [mscorlib]System.Object { }

.field public int64 modreq(TestClass) TestField

FieldSig для вышеприведенного примера кода выглядит так:

Смещение

Значение

Что означает

0x01

0x04

размер сигнатуры

0x02

0x06

пролог FieldSig

0x03

0x1F

встреченный пользовательский обязательный модификатор (modreq), смотрите константы в первой части.

0x04

0x0816 
000010002

ЭлементTypeDefOrRefEncoded, на этот раз он указывает на второй ряд таблицы TypeDef, первые два самых младших бита означают тип таблицы (002- TypeDef), биты с 3 до 8 означают номер строки в таблице (0000102 - 2), то есть TestClass. ТеперьсравнитеегосэлементомTypeDefOrRefEncoded из предыдущего подраздела.

0x05

0x0A

тип поля (int64), смотрите константы в первой части.

2.3 Param

Этот элемент описывает один параметр, заданный в методе или свойстве, и оттого входящий в состав PropertySig, MethodDefSig, MethodRefSig, и т.д. Ниже приведена схема синтаксиса для элемента Param:

 

Рисунок 4. Схема синтаксиса элемента Param

Пример 1

В методе TestMethod, показанном ниже, есть два пользовательских модификатора, прикрепленных к одному параметру. Цель данного примера - показать формат элемента Param и снова показать, как работает элемент TypeDefOrRefEncoded.

// Полный исходник: Param\1.il
// Двоичный файл: Param\1.dll
// (...)

.class public TestClass extends [mscorlib]System.Object { }

.method public static void TestMethod(int32 modopt(TestClass) modreq([mscorlib]System.Runtime.CompilerServices.IsLong) Param1)
{
    ret
}

Соответствующая сигнатура MethodDefSig для этого метода такова:

Смещение

Значение

Что означает

0x01

0x08

размер сигнатуры

0x02

0x00

метод static(статический)

0x03

0x01

количество параметров

0x04

0x01

тип возвращаемого значения (void), смотрите константы в первой части

0x05

0x1F

встреченный пользовательский обязательный модификатор (modreq), смотрите константы в первой части

0x06

0x0916 
000010012

Ссылочный ряд равен 2в таблице метаданных TypeRef, то есть тип IsLong.

0x07

0x20

встреченный пользовательский необязательный модификатор (modopt), смотрите константы в первой части.

0x08

0x0816 
000010002

Ссылочный ряд равен 2в таблице метаданныхTypeDef, то есть тип TestClass.

0x09

0x08

тип первого параметра (int32), смотрите константы в первой части.

2.4 RetType

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

 

Рисунок 5. Схема синтаксиса элемента RetType

2.5 Type

Неудивительно, что элемент Type описывает тип, и не только простой тип (такой как int32, bool, string, и т.д.), но и массивы, обобщенные типы экземпляров и сложные типы (классы и структуры). Листинг ниже показывает схему синтаксиса для этого элемента. Разумеется, слова, написанные в верхнем регистре, являются константами, значения которых можно найти в таблице констант в первой части. Константа GENERICINST входит в состав данного элемента, но помните, что сигнатуры TypeSpec, MethodSpec и MethodDefSig имеют разные цели.

Type ::=      
BOOLEAN | CHAR | I1 | U1 | I2 | U2 | I4 | U4 | I8 | U8 | R4 | R8 | I | U |
| ARRAY Type ArrayShape
| CLASS TypeDefOrRefEncoded
| FNPTR MethodDefSig
| FNPTR MethodRefSig
| GENERICINST (CLASS | VALUETYPE) TypeDefOrRefEncoded GenArgCount Type *
| MVAR number
| OBJECT
| PTR CustomMod* Type
| PTR CustomMod* VOID
| STRING
| SZARRAY CustomMod* Type
| VALUETYPE TypeDefOrRefEncoded
| VAR number

Пример 1

Посмотрим, что случится с сигнатурой MethodDefSig, если метод принимает обобщенные типы как нормальные параметры.

// Полный исходник: Type\1.cs
// Двоичный файл: Type\1.dll
// (...)

public class TestClass<GenArg1, GenArg2> { }

public class TestRunClass
{
    public void TestRunMethod()
    {
        TestMethod(new TestClass<int, string>());
    }

    public void TestMethod(TestClass<int, string> Param1) { }
}

Разбор сигнатуры MethodDefSig для метода TestMethod.

Смещение

Значение

Что означает

0x0E

0x09

размер сигнатуры

0x0F

0x20

Метод является методом экземпляра.

0x10

0x01

количество нормальных параметров

0x11

0x01

тип возвращаемого значения (void), смотрите константы в первой части.

0x12

0x15

тип первого параметра – обобщенный тип (GENERICINST), смотрите константы в первой части.

0x13

0x12

тип первого параметра - обобщённый класс (CLASS), смотрите константы в первой части.

2.6 ArrayShape

Многие люди, использующие платформу .NET, знают, что массив может иметь более одной размерности, но не знают, что каждая размерность в массиве может иметь нижнюю границу. Вероятно, причина в том, что большинство разработчиков использует язык C#, не позволяющий использовать нижние границы, за исключением использования метода Array.CreateInstance для создания такого типа массива. Элемент ArrayShape хранит полное определение многомерного массива, он хранит число размерностей, размер и нижнюю границу каждой размерности, имеющиеся у массива. Ниже приведена схема синтаксиса для данного элемента вместе с кратким описанием, скопированным из спецификации.

 

Рисунок 6. Схема синтаксиса элемента ArrayShape

Rank - целое (хранится в сжатой форме, смотрите §23.2), задающее число размерностей в массиве (должно равняться 1 или больше). NumSizes – сжатое целое, говорящее, сколько размерностей имеют заданные размеры (должно равняться 0 или больше). Size – сжатое целое, задающее размер этой размерности – последовательность начинается с первой размерности и продолжается до полного числа элементов NumSizes. Аналогично, NumLoBounds – сжатое целое, говорящее, сколько размерностей имеют заданные нижние границы (должно равняться 0 или больше). И LoBound - сжатое целое, задающее нижнюю границу этой размерности - последовательность начинается с первой размерности и продолжается до полного числа элементов NumLoBounds. Нельзя пропускать никакую размерность в этих двух последовательностях, но число заданных размерностей может быть меньше, чем Rank.

Замечание: Не путайте многомерные массивы с неровными массивами. Многомерный массив в CIL может быть, например: int32[,], а неровный массив является int32[][]. Также заметьте, что ArrayShape хранит информацию только о многомерных массивах. Одномерный массив обозначается константой SZARRAY – вот и все (смотрите элемент Type). Дополнительные сведения о массивах в .NET читайте в статье Типы массивов в .NET в журнале MSDN.

Важно: К сожалению, как показывает второй пример, компилятор ILASM имеет ряд проблем с обработкой нижних границ массивов (поле LoBound на рисунке 6), нижняя граница умножается на два. Это неправильно, так как спецификация говорит, что нижние границы должны храниться в сигнатурах без внесения изменений. Ниже приведена таблица, взятая из спецификации, показывающая примеры объявлений массивов и их правильных параметров в элементе ArrayShape. Более того, спецификация не оговаривает, в каком случае(ях) поля NumSizes и NumLoBounds могут быть меньше, чем поле Rank. По нашим наблюдениям, поля NumSizes и NumLoBounds меньше, чем Rank, лишь в одном случае – когда нижняя граница не задана для всех размерностей (это показано во второй строке в таблице ниже), иначе NumSizes и NumLoBounds всегда равны Rank, это противоречит третьей и пятой строке в таблице ниже.

Объявление

Type

Rank

NumSizes

Size

NumLoBounds

LoBound

[0...2]

I4

1

1

3

0

-

[,,,,,,]

I4

7

0

-

0

-

[0...3, 0...2,,,,]

I4

6

2

4 3

2

0 0

[1...2, 6...8]

I4

2

2

2 3

2

1 6

[5, 3...5, , ]

I4

4

2

5 3

2

0 3

Пример 1

Посмотрим, как ArrayShape работает на практике.

// Полный исходник: ArrayShape\1.il
// Двоичный файл: ArrayShape\1.dll
// (...)

.field public int32[,,] TestField

Следующая сигнатура FieldSig должна генерироваться вышеприведенным многомерным массивом.

Смещение

Значение

Что означает

0x01

0x06

размер сигнатуры

0x02

0x06

пролог FieldSig

0x03

0x14

значение типа поля - ARRAY, смотрите константы в первой части

0x04

0x08

тип массива - int32, смотрите константы в первой части

0x05

0x03

количество размерностей массива (поле Rank на рисунке 6).

0x06

0x00

размер размерностей массива не задан (поле NumSizes на рисунке 6).

0x07

0x00

Нижние границы размерностей массива не заданы (поле NumLoBounds на рисунке 6).

Пример 2

Этот пример показывает, как элемент ArrayShape ведет себя при объявлении многомерных массивов с заданными нижними границами.

// Полный исходник: ArrayShape\2.il
// Двоичный файл: ArrayShape\2.dll
// (...)

.field public int32[0...5,,4...6] TestField

Вся сигнатура FieldSig выглядит так:

Смещение

Значение

Что означает

0x01

0x0C

размер сигнатуры

0x02

0x06

пролог FieldSig

0x03

0x14

значение типа поля - ARRAY, смотрите константы в первой части

0x04

0x08

тип массива - int32, смотрите константы в первой части

0x05

0x03

количество размерностей массива (поле Rank на рисунке 6)

0x06

0x03

количество размеров для этого массива (поле NumSizes на рисунке 6).

0x07

0x06

размер первой размерности массива (поле Size на рисунке 6).

0x08

0x00

размер второй размерности массива, ноль означает – не задан (поле Size на рисунке 6).

0x09

0x03

размер третей размерности массива (поле Size на рисунке 6).

0x0A

0x03

количество нижних границ для этого массива (поле NumLoBounds на рисунке 6).

0x0B

0x00

нижняя граница первой размерности массива (поле LoBound на рисунке 6).

0x0C

0x00

нижняя граница второй размерности массива (поле LoBound на рисунке 6).

0x0D

0x08

нижняя граница третьей размерности массива (поле LoBound на рисунке 6). Граница умножается на два, смотрите важное примечание в начале текущего подраздела.

Пример 3

Теперь посмотрим, как элемент ArrayShape выглядит в реальности, и сравним результаты со спецификацией.

// Полный исходник: ArrayShape\3.il
// Двоичный файл: ArrayShape\3.dll
// (...)

.field public int32[0...2] TestField

Да, NumLoBounds равняется Rank, несмотря на то, что спецификация говорит, что NumLoBounds должен равняться нулю.

Смещение

Значение

Что означает

0x01

0x08

размер сигнатуры

0x02

0x06

пролог FieldSig

0x03

0x14

значение типа поля - ARRAY, смотрите константы в первой части

0x04

0x08

тип массива - int32, смотрите константы в первой части

0x05

0x01

количество размерностей массива (поле Rank на рисунке 6)

0x06

0x01

количество размеров для этого массива (поле NumSizes на рисунке 6)

0x07

0x03

размер первой размерности массива (поле Size на рисунке 6)

0x08

0x01

количество нижних границ для этого массива (поле NumLoBounds на рисунке 6)

0x09

0x00

нижняя граница первой размерности массива (поле LoBound на рисунке 6)

3. Вывод

Как видно, сигнатуры являются сложным уродством, но делают исполняемый файл .NET маленьким, компактным и логичным.