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

ОГЛАВЛЕНИЕ

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 маленьким, компактным и логичным.