Формат файла .NET – внутреннее устройство сигнатур (часть 1)

ОГЛАВЛЕНИЕ

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

Загрузить бинарные DLLпримеров программ - signatures_binary.zip - 16.40 Кб

Загрузить исходники примеров программ - signatures_source.zip - 5.69 Кб

Оглавление

  • 1. Введение
  • 2. Чтотакоесигнатуры?
  • 3. Начало работы
    • 3.1 Проводник CFF
    • 3.2 Порядок следования байтов
    • 3.3 Сжатое целое
    • 3.4 Константы
  • 4. Сигнатуры
    • 4.1 FieldSig
    • 4.2 PropertySig
    • 4.3 MethodDefSig
    • 4.4 MethodRefSig
    • 4.5 StandAloneMethodSig

Введение

1. Введение

Данная статья, состоящая из двух частей, охватывает сигнатуры, являющиеся второй важнейшей частью файла .NET после метаданных, о которых Дэниел Пистелли написал прекрасную статью, расположенную здесь. Настоятельно рекомендую прочитать эту статью перед тем, как двигаться вперед, кроме того, вы можете прочитать Подробное рассмотрение формата портативного исполняемого файла Win32 в журнале MSDN, описывающее формат файлов PE, образующий основу для метаданных .NET, сигнатур и кода промежуточного языка (IL). Безусловно, почти все можно найти в спецификации метаданных разбиения II, но, как водится, спецификации жертвуют читабельностью ради полноты, что стало другой причиной написания данной статьи.

2. Что такое сигнатуры?

Сигнатуры хранят данные, которые нельзя компактно сохранить в таблицах метаданных, например, типы параметров, аргументы, подаваемые в настраиваемые атрибуты, дескрипторы упорядочения(сортировки, формирования), и т.д. Хранение такой информации, как типы данных, в таблицах, может привести к чрезмерной фрагментации данных, неразборчивости и снизить производительность, поэтому специалисты CLI/CLR придумали сигнатуры, позволяющие хранить ранее упомянутый тип данных в компактной и хорошей форме, в следующих разделах вы ясно увидите, почему они так важны.


3. Начало работы

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

3.1 Проводник CFF

Для просмотра метаданных .NET и кода сигнатур будет использоваться проводник CFF, написанный Дэниэлом Пистелли. Проводник CFF – бесплатный инструмент, умеющий просматривать и редактировать заголовки PE, ресурсы и некоторые поля и флаги метаданных .NET. Вы можете загрузить его с данного сайта. На рисунке ниже показан проводник CFF, работающий с загруженной тестовой сборкой. Обычно сигнатуры индексируются с помощью столбца Сигнатура, красным кругом выделено расположение сигнатуры MethodDefSig, индексируемой Method.Signature в куче #Blob, которую можно просмотреть, нажав на элемент, обведенный зеленым кругом.



Рисунок 1.Работающий проводник CFF

3.2 Порядок следования байтов

Лучшее описание дано в Википедии:

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

В данном случае порядок следования байтов рассматривается как порядок байтов данных (обычно целочисленных), хранящихся в файле. Существует два метода (порядка) представления данных в файле, с порядком следования байтов, начиная со старшего, и с порядком следования байтов, начиная с младшего, файл PE/.NET использует оба метода, поэтому каждый из них рассматривается ниже.

Порядок следования байтов, начиная со старшего

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

 

100

101

102

103

 

...

1B

56

80

DA

...


Порядок следования байтов, начиная с младшего

По сравнению с порядком следования байтов, начиная со старшего, порядок следования байтов, начиная с младшего, сохраняет данные в обратном порядке, т.е. самый младший байт сохраняется в наименьшем смещении. В данном случае значение 0x1B5680DA, сохраненное в формате с порядком следования байтов, начиная с младшего, будет выглядеть так:

 

100

101

102

103

 

...

DA

80

56

1B

...

3.3 Сжатое целое

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

Если значение лежит между 0 (0x00) и 127 (0x7F) включительно, кодировать в виде однобайтового целого (бит 7 пустой, значение хранится в битах от 6 до 0).

Если значение лежит между 28 (0x80) и 214 - 1 (0x3FFF) включительно, кодировать в виде 2-байтового целого с установленным битом 15, пустым битом 14 (значение хранится в битах от 13 до 0).
В ином случае кодировать в виде 4-байтового целого, с установленным битом 31, установленным битом 30, пустым битом 29 (значение хранится в битах от 28 до 0).

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

Пример 1

Значение меньше 0x80, значит это первый случай, удаляем три ненужных байта.

 

Исходное значение (32-бит)

Сжатое значение

Сэкономленные байты

Шестнадцатеричный

00 00 00 03

0x03

3

Двоичный

00000000 0000000 00000000 00000011

00000011

-

Пример 2

Такой же, как пример 1.

 

Исходное значение (32-бит)

Сжатое значение

Сэкономленные байты

Шестнадцатеричный

00 00 00 7F

7F

3

Двоичный

00000000 0000000 00000000 01111111

01111111

-

Пример 3

В этом примере исходное значение равняется 0x80, хотя одного байта достаточно для сохранения 0x80, использование сжатого целого требует освобождения последнего бита, поэтому для сохранения значения 0x80 в виде сжатого целого нужно иметь дополнительный байт.

 

Исходное значение (32-бит)

Сжатое значение

Сэкономленные байты

Шестнадцатеричный

00 00 00 80

80 80

2

Двоичный

00000000 0000000 00000000 10000000

10000000 10000000

-

Пример 4

Удаляем два ненужных байта.

 

Исходное значение (32-бит)

Сжатое значение

Сэкономленные байты

Шестнадцатеричный

00 00 2E 57

AE 57

2

Двоичный

00000000 0000000 00101110 01010111

10101110 01010111

-


Разумеется, сжатие обходится некоторой ценой, несколько битов должно быть зарезервировано, чтобы они указывали, сколько байтов занимает сжатое целое. Таким образом, максимальное закодированное целое имеет длину 29 битов со значением 0x1FFFFFFF. Сжатые целые физически кодируются с использованием порядка следования байтов, начиная со старшего.

3.4 Константы

Следующий список перечисляет распространенные константы, часто используемые почти во всех сигнатурах. В следующих частях статьи мы будем ссылаться на них очень часто по сокращениям, используя только последний член имени, например, ELEMENT_TYPE_I8 как I8, ELEMENT_TYPE_STRING как STRING, и т.д.

Имя

Значение

Примечания

ELEMENT_TYPE_END

0x00

Отмечает конец списка

ELEMENT_TYPE_VOID

0x01

System.Void

ELEMENT_TYPE_BOOLEAN

0x02

System.Boolean

ELEMENT_TYPE_CHAR

0x03

System.Char

ELEMENT_TYPE_I1

0x04

System.SByte

ELEMENT_TYPE_U1

0x05

System.Byte

ELEMENT_TYPE_I2

0x06

System.Int16

ELEMENT_TYPE_U2

0x07

System.UInt16

ELEMENT_TYPE_I4

0x08

System.Int32

ELEMENT_TYPE_U4

0x09

System.UInt32

ELEMENT_TYPE_I8

0x0A

System.Int64

ELEMENT_TYPE_U8

0x0B

System.UInt64

ELEMENT_TYPE_R4

0x0C

System.Single

ELEMENT_TYPE_R8

0x0D

System.Double

ELEMENT_TYPE_STRING

0x0E

System.String

ELEMENT_TYPE_PTR

0x0F

Неуправляемый указатель, сопровождаемый элементом Type(тип).

ELEMENT_TYPE_BYREF

0x10

Управляемый указатель, сопровождаемый элементом Type.

ELEMENT_TYPE_VALUETYPE

0x11

Модификатор типа значения, сопровождаемый меткой TypeDef или TypeRef

ELEMENT_TYPE_CLASS

0x12

Модификатор типа класса, сопровождаемый меткой TypeDef или TypeRef

ELEMENT_TYPE_VAR

0x13

Обобщенный параметр в определении обобщенного типа, представленный в виде числа

ELEMENT_TYPE_ARRAY

0x14

Модификатор типа многомерного массива.

ELEMENT_TYPE_GENERICINST

0x15

Реализация обобщенного типа. Сопровождается тип тип-арг-число тип-1 ... тип-n

ELEMENT_TYPE_TYPEDBYREF

0x16

Типовая ссылка.

ELEMENT_TYPE_I

0x18

System.IntPtr

ELEMENT_TYPE_U

0x19

System.UIntPtr

ELEMENT_TYPE_FNPTR

0x1B

Указатель на функцию, сопровождаемый полной сигнатурой метода

ELEMENT_TYPE_OBJECT

0x1C

System.Object

ELEMENT_TYPE_SZARRAY

0x1D

Модификатор типа одномерного массива с нулевой нижней границей.

ELEMENT_TYPE_MVAR

0x1E

Обобщенный параметр в определении обобщенного метода, представленный в виде числа

ELEMENT_TYPE_CMOD_REQD

0x1F

Обязательный модификатор, сопровождаемый меткой TypeDef или TypeRef

ELEMENT_TYPE_CMOD_OPT

0x20

Необязательный модификатор, сопровождаемый меткой TypeDef или TypeRef

ELEMENT_TYPE_INTERNAL

0x21

Реализован внутри CLI

ELEMENT_TYPE_MODIFIER

0x40

ORed со следующими типами элементов

ELEMENT_TYPE_SENTINEL

0x41

Сигнальная ме(а?)тка для сигнатуры метода vararg

ELEMENT_TYPE_PINNED

0x45

Обозначает локальную переменную, указывающую на закрепленный объект

 

0x50

Показывает аргумент типа System.Type.

 

0x51

Используется в специальных атрибутах, чтобы задавать упакованный объект (§23.3 в спецификации ECMA-355).

 

0x52

Зарезервирован

 

0x53

Используется в специальных атрибутах для обозначения поля (§22.10, §23.3 в спецификации ECMA-355).

 

0x54

Используется в специальных атрибутах для обозначения свойства (§22.10, §23.3 в спецификации ECMA-355).

 

0x55

Используется в специальных атрибутах для задания перечисления (§23.3 в спецификации ECMA-355).

 


4. Сигнатуры

Почти все приготовления завершены, и можно начинать рассматривать сигнатуры, но стоит упомянуть еще несколько вещей, не очевидных для всех. Первое – почти все целые числа в сигнатурах сжаты. Второе – все сигнатуры начинаются с размера (в байтах), занимаемого ими в куче #Blob, разумеется, это значение хранится с использованием сжатия целых. Наконец, что не менее важно, значения, показывающие местоположение сигнатуры в куче #Blob, абсолютные, т.е. вам не нужно ничего прибавлять/отнимать от основного значения (так, как в красном круге (на рисунке) на рисунке 1), чтобы найти сигнатуру в куче.

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

Так как данная статья – скорее руководство, в данном разделе по байтам рассматриваются все сигнатуры, начиная с простейших и заканчивая самыми сложными. Каждая рассматриваемая сигнатура связана с описанием, диаграммой или синтаксисом, скопированным из спецификации, и набором примеров, полные двоичные файлы и исходники которых можно загрузить вверху данной статьи, по возможности приложения написаны с помощью C#, в ином случае – с использованием CIL (раньше MSIL).

4.1 FieldSig

Как сказано выше, мы начинаем с простейших сигнатур. Одна из них - сигнатура FieldSig, она в основном описывает тип поля и специальные модификаторы, прикрепленные к полю, проиндексирована столбцом Field.Signature. Конечно, сигнатура поля начинается с размера всей сигнатуры, следующим идет пролог FIELD, имеющий постоянное значение 0x6, ноль или больше специальных модификаторов, и тип поля. Схема синтаксиса для FieldSig показана ниже на рисунке 2
Замечание: не путайте специальные модификаторы со специальными атрибутами! Это абсолютно разные вещи. Так как специальные модификаторы образуют часть нескольких сигнатур, они будут рассмотрены в следующем разделе. В примерах данного раздела никакие специальные модификаторы не используются.



Рисунок 2. Схема синтаксиса сигнатуры FieldSig

Пример 1

Этот пример вполне понятный: создается простое поле типа int32, как показано ниже.

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

public int TestField;

Теперь нужно загрузить двоичную сборку FieldSig\1.dll в проводник CFF и перейти в таблицу Field, чтобы найти строку, связанную с нашим полем (должна быть только одна). Рисунок ниже должен помочь вам.

 

Рисунок 3. Строка поля TestField в таблице метаданных Field

Мы нашли ее! Переходим на 0x000A в #Blob.

Рисунок 4. Сигнатура FieldSig, просматриваемая программой проводник CFF

Теперь разберем сигнатуру по байтам в таблице ниже.

Смещение

Значение

Что означает

0x0A

0x02

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

0x0B

0x06

Пролог

0x0C

0x08

Значение типа поля int32, смотрите константы

Пример 2

В этот раз мы меняем тип поля на string (строка), как показывает следующий листинг кода.

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

public string TestField;

Сигнатура FieldSig для поля TestField все еще находится в 0x000A, и только последний байт изменился с 0x08 на 0x0E.

Смещение

Значение

Что означает

0x0A

0x02

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

0x0B

0x06

Пролог

0x0C

0x0E

Значение типа поля string, смотрите константы

 


4.2 PropertySig

Сигнатура PropertySig индексируется столбцом Property.Type, она хранит информацию о свойстве – то есть число параметров, передаваемых свойству, чтобы получить данные, ноль или больше специальных модификаторов, тип возвращаемого значения, тип каждого переданного параметра. Но в сигнатуре PropertySig появилась одна новая вещь, а именно – флаг HASTHIS (из постоянного значения 0x20), он показывает, передает ли во время выполнения вызванный метод указатель на целевой объект в качестве своего первого аргумента (указатель this). Флаг HASTHIS устанавливается, когда свойство (фактически, его установщик и получатель) является экземпляром или виртуальным, и не устанавливается, когда свойство (установщик и получатель) является статическим. Флаг (если установлен) - ORed вместе со значением пролога сигнатуры. Ниже показана полная схема синтаксиса для данной сигнатуры.

 

Рисунок 5. Схема синтаксиса сигнатуры PropertySig


Пример 1

Первый пример тривиальный: мы создали одно свойство экземпляра типа int32, как показано ниже.

// Полный исходник: PropertySig\1.cs 
// Бинарный файл: PropertySig\1.dll
// (...)

public int TestProperty { get; set; }

Сигнатура начинается со смещения 0x001A в куче #Blob.

Смещение

Значение

Что означает

0x1A

0x03

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

0x1B

0x28

Пролог ORed с константой HASTHIS, так как 0x20 OR 0x08 = 0x28.

0x1C

0x00

Количество параметров, передаваемых методу-получателю свойства, смотрите рисунок 5 выше.

0x1D

0x08

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

Пример 2

Данный пример немного сложней, так как он использует индексированное свойство, возвращающее(го) разное значение в зависимости от параметров, переданных свойству, и, как видно ниже, такой тип свойства не имеет никакого имени (в C#), но в таблице метаданных Field оно всегда объявляется как Item(элемент). Можно определить только одно индексированное свойство на каждый класс/структуру, но его можно перегружать.

// Полный исходник: PropertySig\2.cs 
// Бинарный файл: PropertySig\2.dll
// (...)

public int this [int Param1, string Param2]
{
get { return 0; }
set { }
}

Сигнатура ранее упомянутого поля хранится со смещением 0x001B в #Blob и описана в таблице ниже.\

Смещение

Значение

Что означает

0x1B

0x05

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

0x1C

0x28

Свойство имеет тип экземпляра, поэтому вновь пролог сигнатуры объединен с помощью OR с константой HASTHIS.

0x1D

0x02

Количество параметров, передаваемых методу-получателю свойства, смотрите рисунок 5 выше.

0x1E

0x08

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

0x1F

0x08

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

0x20

0x0E

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

Пример 3

В этом примере мы попытаемся отключить флаг HASTHIS, объявив свойство как статическое.

// Полный исходник: PropertySig\3.cs 
// Бинарный файл: PropertySig\3.dll
// (...)

public class TestClas
{
public static int TestProperty { get; set; }
}

На этот раз вышеуказанная сигнатура свойства начинается со смещения 0x001A в #Blob.

Смещение

Значение

Что означает

0x1A

0x03

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

0x1B

0x08

Постоянное значение пролога (только).

0x1C

0x00

Количество параметров, передаваемых методу-получателю свойства, смотрите рисунок 5 выше.

0x1D

0x08

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

 


4.3 MethodDefSig

Данная сигнатура хранит информацию, связанную с методами, определенными в текущей сборке, такую как тип соглашения о вызовах, количество обобщенных параметров, количество нормальных параметров метода, тип возвращаемой переменной и тип каждого параметра, переданного методу. Она индексируется столбцом MethodDef.Signature.

Рисунок 6. Схема синтаксиса сигнатуры MethodDefSig

Дополнительно используются несколько флагов (перечислены в таблице ниже), они объединяются с помощью OR и размещаются во втором байте сигнатуры (первый байт – размер сигнатуры).

Имя

Значение

Что означает

HASTHIS

0x20

Первый передаваемый методу аргумент - указатель this, этот флаг установлен, когда метод – экземпляр или виртуальный. Вы также можете прочитать объяснение флага HASTHIS в предыдущем подразделе

EXPLICITTHIS

0x40

Спецификация гласит: "Обычно список параметров (всегда сопровождающий соглашение о вызовах) не предоставляет информацию о типе указателя this, так как его можно получить из другой информации. Когда явный экземпляр комбинации задан, все же, первый тип в последующем списке параметров задает тип указателя this, а следующие элементы задают типы самих параметров". Заметьте, что если EXPLICITTHIS установлен, HASTHIS тоже должен быть установлен.

DEFAULT

0x00

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

VARARG

0x05

Задает соглашение о вызовах для методов с изменяющимися аргументами.

GENERIC

0x10

Метод имеет один или больше обобщенных параметров.

Пример 1

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

// Полный исходник: MethodDefSig\1.cs 
// Бинарный файл: MethodDefSig\1.dll
// (...)

public void TestMethod<GenArg1, GenArg2>(int Param1, object Param2) { }

Сигнатура MethodDefSig для примера метода хранится в смещении 0x000A и выглядит так.

Смещение

Значение

Что означает

0x0A

0x06

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

0x0B

0x30

Так как это экземпляр и обобщенный метод, флаги HASTHIS и GENERIC установлены, 0x20 OR 0x10 = 0x30.

0x0C

0x02

Количество обобщенных параметров.

0x0D

0x02

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

0x0E

0x01

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

0x0F

0x08

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

0x10

0x1C

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

Пример 2

В данном примере снова показано использование флага HASTHIS, определение рассматриваемого метода выглядит так.

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

public class TestClas
{
public static void TestMethod(int Param1, object Param2) { }
}

Сигнатура снова хранится в 0x000A в куче #Blob и выглядит так.

Смещение

Значение

Что означает

0x0A

0x05

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

0x0B

0x00

Есть один установленный флаг, а именно DEFAULT –  это означает, что метод статический, и позволяет CLR определить используемое соглашение о вызовах. Метод не обобщенный, так как флаг GENERIC не установлен, следовательно, следующий байт задает количество нормальных (не обобщенных) параметров, передаваемых методу.

0x0C

0x02

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

0x0D

0x01

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

0x0E

0x08

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

0x0F

0x1C

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


Пример 3

Теперь посмотрим, как работает флаг EXPLICITTHIS: его можно включить путем использования ключевого слова explicit в определении метода, разумеется, на языке CIL.

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

.method instance explicit void TestMethod () cil managed
{
.maxstack 2
ret
}

MethodDefSig для вышеуказанного метода выглядит так.

Смещение

Значение

Что означает

0x01

0x03

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

0x02

0x60

Флаги HASTHIS и EXPLICITTHIS установлены, так как 0x20 OR 0x40 = 0x60.

0x03

0x00

Количество параметров, принимаемых методом.

0x04

0x01

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

Пример 4

В этом примере был создан метод, принимающий изменяемые аргументы, т.е. кроме нормальных параметров, заданных в объявлении, он принимает переменное количество параметров изменяемого типа. Добавление vararg в ключевое слово языка CIL в определение метода заставляет метод принимать изменяемые аргументы, что видно по листингу кода ниже.
Важно: Использование ключевого слова params в C# не устанавливает флаг VARARG в связанной с методом сигнатуре. Метод, использующий ключевое слово params в C#, всего лишь оформляется компилятором C# атрибутом ParamArray, и дополнительные параметры рассматриваются как нормальный массив. Можно сделать метод истинно VARARG в C#, следуя данной инструкции, но это не совместимо с CLS.

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

.method instance vararg void TestMethod () cil managed
{
.maxstack 2
ret
}

Сигнатура метода рассмотрена в следующей таблице.

Смещение

Значение

Что означает

0x01

0x03

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

0x02

0x25

Метод является экземпляром и принимает изменяемые аргументы, следовательно, флаги HASTHIS и VARARG установлены, и 0x20 OR 0x05 = 0x25

0x03

0x00

Количество параметров, принимаемых методом.

0x04

0x01

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

 


 4.4 MethodRefSig

Эта сигнатура очень похожа (если не идентична) на ранее упомянутую MethodDefSig, но MethodRefSig описывает соглашение о вызовах метода, параметры, и т.д., в точке, где метод вызывается (также называемой точка вызова). Сигнатура индексируется столбцом MemberRef.Signature, и если метод не принимает изменяемые аргументы, сигнатура идентична MethodDefSig и должна точно совпадать с сигнатурой, указанной в определении целевого метода, в ином случае – как показано ниже.

 

Рисунок 7. Схема синтаксиса сигнатуры MethodRefSig

При вызове метода VARARG в связанной с ним MethodRefSig имеется одна дополнительная константа, а именно SENTINEL. Данное значение предназначено лишь для одного - оно обозначает конец обязательных параметров, передаваемых методу, и начало дополнительных (изменяемых) параметров, дополнительную информацию о значениях сигнальной метки можно найти здесь. Также заметьте, что целочисленный ParamCount показывает общее число параметров, переданных методу. В таблице ниже перечислены все сокращения, используемые в сигнатуре MethodRefSig, когда она отличается от MethodDefSig.

Имя

Значение

Что означает

HASTHIS

0x20

Первый аргумент, передаваемый методу, - указатель this, этот флаг установлен, когда метод – экземпляр или виртуальный. Объяснение флага HASTHIS также дано в подразделе 4.2.

EXPLICITTHIS

0x40

Спецификация гласит: "Обычно список параметров (всегда сопровождающий соглашение о вызовах) не предоставляет информацию о типе указателя this, так как его можно получить из другой информации. Когда явный экземпляр комбинации задан, все же, первый тип в последующем списке параметров задает тип указателя this, а следующие элементы задают типы самих параметров". Заметьте, что если EXPLICITTHIS установлен, HASTHIS тоже должен быть установлен.

VARARG

0x05

Задает соглашение о вызовах для методов с изменяемыми аргументами.

SENTINEL

0x41

Обозначает конец обязательных параметров.

Пример 1

Дабы убедить вас, что при вызове не VARARG метода нет разницы между MethodDefSig и связанной с ней сигнатурой MethodRefSig, был создан следующий код.

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

public void TestMethod(int Param1, string Param2) { }
Collapse Copy Code
// Полный исходник: MethodRefSig\1b.cs
// Двоичный файл: MethodRefSig\1b.dll
// (...)

new TestClass().TestMethod(0, "A simple parameter");

Теперь рассмотрим сигнатуру TestMethod's MethodDefSig, хранящуюся в файле MethodRefSig\1a.dll.

Смещение

Значение

Что означает

0x0A

0x05

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

0x0B

0x20

Метод - экземпляр, следовательно, флаг HASTHIS установлен, что означает, что первый аргумент, переданный методу, - указатель this.

0x0C

0x02

Метод принимает точно два параметра.

0x0D

0x01

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

0x0E

0x08

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

0x0F

0x0E

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

Связанная с ней MethodRefSig выглядит точно так же, но хранится в другом смещении.

Смещение

Значение

Что означает

0x13

0x05

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

0x14

0x20

Метод - экземпляр, следовательно, флаг HASTHIS установлен, что означает, что первый аргумент, переданный методу, - указатель this.

0x15

0x02

Метод принимает точно два параметра.

0x16

0x01

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

0x17

0x08

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

0x18

0x0E

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

Пример 2

В этом примере показано, как сигнатура MethodRefSig обращается с вызовом методов VARARG. Для этой цели был создан подлинно VARARG метод, принимающий один обязательный параметр, и остальные, изменяемые параметры. Помните, что использование ключевого слова params в C# не устанавливает флаг VARARG в связанной с методом сигнатуре, так как params всего лишь оформляет метод атрибутом ParamArray, и дополнительные параметры рассматриваются как массив объектов некоторого типа. Чтобы установить флаг VARARG в сигнатуре, вы должны добавить __arglist в определение метода в качестве последнего параметра, но это не совместимо с CLS ( за дополнительной информацией идите сюда).

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

[CLSCompliant(false)]
public void TestMethod(string RequiredParam, __arglist)
{
Console.WriteLine("Required parameter is: " + RequiredParam);

Console.WriteLine("Additional parameters are: ");
ArgIterator argumentIterator = new ArgIterator(__arglist);
for (int i = 0; i < argumentIterator.GetRemainingCount(); i++)
{
Console.WriteLine(__refvalue(argumentIterator.GetNextArg(), string));
}

}

Теперь пришло время вызвать наш метод из отдельной сборки: метод вызывается с одним обязательным аргументом типа string, и с двумя дополнительными аргументами типа int32, как показано ниже.

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

[CLSCompliant(false)]
public void TestRunMethod()
{
new TestClass().TestMethod(
"I am required parameter.",
__arglist(0, 1));
}

Для вышеуказанного вызова имеются две строки в таблице MemberRef. Неясно, почему это так, но сигнатура из первой обнаруженной строки имеет установленный флаг HASTHIS, однако он не содержит никакой информации об изменяемых аргументах, переданных методу. Спецификация ничего не говорит о таком странном поведении, но сигнатура, проиндексированная второй строкой, правильная, так что давайте посмотрим.

Смещение

Значение

Что означает

0x23

0x07

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

0x24

0x25

Метод является экземпляром и принимает изменяемые параметры, поэтому HASTHIS OR VARARG = 0x20 OR 0x05 = 0x25.

0x25

0x03

Общее число параметров, переданных методу, равняется 3: один обязательный и два дополнительных.

0x26

0x01

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

0x27

0x0E

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

0x28

0x41

Константа SENTINEL, все параметры после данного значения дополнительные.

0x29

0x08

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

0x30

0x08

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

 


4.5 StandAloneMethodSig

Тип данной сигнатуры очень похож на MethodRefSig, он предоставляет сигнатуру точки вызова для метода, но имеет два важных отличия. Первое отличие в том, что StandAloneSig может задавать неуправляемый целевой метод, StandAloneSig обычно создается в качестве подготовки к выполнению инструкции calli, вызывающей управляемый или неуправляемый код. Второе важное отличие в том, что сигнатура StandAloneSig индексируется столбцом StandAloneSig.Signature, являющимся единственным столбцом в таблице метаданных StandAloneSig. Более того – ни на одну из строк в данной таблице не ссылается никакая другая таблица (поэтому она названа независимой), эта таблица заполнена генераторами кода. Сигнатура в столбце StandAloneSig.Signature должна быть сигнатурой StandAloneMethodSig для каждого выполнения инструкции calli, или сигнатурой LocalVarSig, описывающей локальные переменные в каждом методе, и которая будет подробнее объяснена в следующем разделе. Схема синтаксиса для сигнатуры StandAloneSig приведена ниже.

Рисунок 8. Схема синтаксиса сигнатуры StandAloneMethodSig

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

Важно: Как вскоре станет ясно, существуют разные соглашения о вызовах для вызова методов, принимающих изменяемые параметры, для управляемого и неуправляемого кода. Диаграмма для каждого случая может выглядеть по-разному: например, соглашение о вызовах VARARG вызывает управляемые методы, принимающие изменяемые параметры. В этом случае сигнатура имеет дополнительные элементы, SENTINEL и один или больше Param (закрашенные прямоугольники), однако соглашение о вызовах C также вызывает методы, принимающие изменяемые параметры (неуправляемый код), но сигнатура для этого случая заканчивается непосредственно перед элементом Param. Компилятор генерирует сигнатуры как указано выше, к сожалению, наш пример кода компилируется, но выбрасывает исключение, и невозможно понять, в чем заключается проблема. Поэтому нельзя точно судить о правильности данного заключения, более того, спецификация неясная: "Две отдельных схемы были объединены в одну в данной схеме, с использованием закраски, чтобы различать их. Таким образом, для следующих соглашений о вызовах: DEFAULT (управляемое), STDCALL, THISCALL и FASTCALL (неуправляемое), сигнатура заканчивается непосредственно перед элементом SENTINEL (все эти сигнатуры не являются vararg).

Однако для управляемого и неуправляемого соглашений о вызовах vararg – VARARG (управляемое) и C (неуправляемое) – сигнатура может содержать элемент SENTINEL и заключительные элементы Param (они не обязательные). Эти необязательные элементы обозначаются путем закрашивания прямоугольников на схеме синтаксиса". Видите ли вы это? Почему прямоугольник C не закрашен, если использование соглашения о вызовах C может добавить элементы SENTINEL и Param при вызове неуправляемого метода, принимающего изменяемые аргументы? При каких условиях элементы Param не обязательные? Инструкция calli очень редко встречается в на 100% правильно работающем коде, вызывающем неуправляемый метод (392 сборки из нашего GAC выполняют инструкцию calli лишь дважды и только по отношению к управляемым методам!), поэтому нельзя утверждать, что наши объяснения для следующего примера кода в этом подразделе абсолютно истинные. Если кто-то знает, как сигнатура StandAloneMehtodSig выглядит при правильном вызове неуправляемого метода (принимающего или не принимающего изменяемые аргументы – в обоих случаях код выбрасывает исключение), сообщите нам, мы были бы очень благодарны.

Имя

Значение

Что означает

HASTHIS

0x20

Первый аргумент, передаваемый методу, - указатель this. Этот флаг установлен, когда метод – экземпляр или виртуальный. Объяснение флага HASTHIS также дано в подразделе 4.2.

EXPLICITTHIS

0x40

Спецификация гласит: "Обычно список параметров (всегда сопровождающий соглашение о вызовах) не предоставляет информацию о типе указателя this, так как его можно получить из другой информации. Когда явный экземпляр комбинации задан, все же, первый тип в последующем списке параметров задает тип указателя this, а следующие элементы задают типы самих параметров". Заметьте, что если EXPLICITTHIS установлен, HASTHIS тоже должен быть установлен.

DEFAULT

0x00

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

VARARG

0x05

Задает соглашение о вызовах для управляемых методов с изменяемыми аргументами.

C

0x01

Соглашение о вызовах для неуправляемого целевого метода, особенности этого соглашения следующие:

Параметры передаются справа налево.

Программа, вызывающая метод, выполняет очистку стека.

Только данное соглашение о вызовах позволяет вызывать неуправляемые методы, имеющие изменяемые параметры (vararg применяется для управляемых методов).

Данное соглашение о вызовах можно использовать путем добавления ключевого слова unmanaged cdecl в определение метода на языке CIL.

STDCALL

0x02

Соглашение о вызовах для неуправляемого целевого метода. Особенности этого соглашения следующие:

Параметры передаются справа налево.

Вызванный метод выполняет очистку стека.

Данное соглашение о вызовах можно использовать путем добавления ключевого слова unmanaged stdcall в определение метода на языке CIL.

THISCALL

0x03

Соглашение о вызовах для неуправляемого целевого метода. Особенности этого соглашения следующие:

Параметры передаются справа налево.

Вызванный метод выполняет очистку стека.

Указатель this помещается в регистр ECX.

Данное соглашение о вызовах можно использовать путем добавления ключевого слова unmanaged thiscall в определение метода на языке CIL.

FASTCALL

0x04

Соглашение о вызовах для неуправляемого целевого метода. Особенности этого соглашения следующие:

Некоторые параметры помещаются в регистры ECX и EDX, остальные аргументы помещаются (проталкиваются) в стек справа налево.

Вызванный метод выполняет очистку стека.

Данное соглашение о вызовах можно использовать путем добавления ключевого слова unmanaged fastcall в определение метода на языке CIL.

SENTINEL

0x41

Обозначает конец обязательных параметров.

Замечание: Здесь стоит упомянуть одну вещь: в отличие от CL (компилятор Microsoft C\C++), ILASM (компилятор Microsoft CIL) не добавляет никаких специальных символов (таких как "@", "_", "?", и т.д.) в имя метода при использовании любого из соглашений о вызовах для неуправляемых целевых методов. Компилятор CIL не оформляет имена никаких методов специальными символами, так как он генерирует байтовый код, позднее компилируемый в машинный код оперативным компилятором CLR. Поэтому когда вы выбираете некоторое соглашение о вызовах при программировании в CIL, компилятор(e) ILASM не определяет, кто (вызывающая программа или вызванный метод) очищает стек, не определяет, в каком порядке аргументы передаются методу, и не меняет имена методов, все это делается во время оперативной компиляции/оптимизации. Если вы не понимаете, о чем идет речь, прочитайте статью Неманджа Трифунович под названием “Раскрытие тайны соглашений о вызовах”, подробно описывающую разные типы соглашений о вызовах для C и C++, что они означают, как работают, и т.д.

Пример 1

В листинге примера кода есть два управляемых метода: первый метод имеет один постоянный параметр типа int32 и также возвращает int32 (фактически он не возвращает ничего, так как отсутствуют данные, помещаемые в оценочный стек); второй перечисленный метод выполняет первый метод, что показано ниже.

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

.method public static int32 TestMethod(int32 required)
{
ret
}

.method public static void TestRunMethod()
{
.maxstack 8
ldc.i4.1
ldftn int32 TestMethod(int32)
calli int32(int32)
ret
}

Перед тем как метод TestRunMethod выполняет TestMethod, он помещает одно значение (аргумент) int32 в оценочный стек с помощью инструкции ldc.i4.1, затем помещает указатель на первый метод в оценочный стек с помощью инструкции ldftn, в конце он вызывает тестовый, ничего не делающий управляемый метод, выполняет calli, и эта последняя инструкция генерирует сигнатуру StandAloneMethodSig, разобранную в таблице ниже.

Смещение

Значение

Что означает

0x01

0x04

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

0x02

0x00

Метод не использует никакого конкретного соглашения о вызовах, метод не является методом -экземпляром, так как флаг HASTHIS не установлен.

0x03

0x01

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

0x04

0x08

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

0x05

0x08

Тип первого обязательного параметра (int32), смотрите константы.


Пример 2

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

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

.method public hidebysig static vararg void TestMethod(int32 required)
{
ret
}

.method public hidebysig static void TestRunMethod()
{
.maxstack 3
ldc.i4.1
ldc.i4.2
ldftn vararg void TestMethod(int32, ..., int32)
calli vararg void(int32, ..., int32)
ret
}

В этом случае сигнатура, генерируемая инструкцией calli, выглядит так же, как и сигнатура MethodRefSig, рассмотренная в предыдущем подразделе. Давайте посмотрим.

Смещение

Значение

Что означает

0x01

0x06

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

0x02

0x05

Метод статический, принимающий изменяемые аргументы.

0x03

0x02

Общее число параметров, передаваемых методу, равно 2: один обязательный и один дополнительный.

0x04

0x01

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

0x05

0x08

Тип первого обязательного параметра (int32), смотрите константы.

0x06

0x41

Константа SENTINEL, все параметры после данного значения дополнительные.

0x07

0x08

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


Пример 3

Это самый проблематичный пример во всей статье: метод в примере кода ниже вызывает неуправляемый метод, принимающий изменяемые аргументы, код компилируется, но выбрасывает исключение TypeLoadException ("Сигнатура неправильная"), к сожалению, спецификация не объясняет четко этот случай (смотрите важное примечание в начале данного подраздела). Показанный ниже пример кода, как и в первом примере, вызывает метод, принимающий изменяемые аргументы, но в этот раз вызываемый метод неуправляемый.

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

.method public hidebysig static unmanaged cdecl void TestMethod(int32 required, ...)
{
ret
}

.method public hidebysig static void TestRunMethod()
{
.maxstack 3
ldc.i4.1
ldc.i4.2
ldftn unmanaged cdecl void TestMethod(int32, ...)
calli unmanaged cdecl void(int32, ...)
ret
}

Сигнатура, сгенерированная calli, очень странная, она заканчивается непосредственно перед первым дополнительным элементом Param, переданным методу.

Смещение

Значение

Что означает

0x01

0x05

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

0x02

0x01

Метод статический и неуправляемый, тип соглашения о вызовах - C (задается ключевым словом unmanagedcdecl), следовательно, принимает изменяемые аргументы.

0x03

0x01

Общее число параметров, переданных методу, равно 1, один обязательный и один отброшенный (непонятно почему).

0x04

0x01

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

0x05

0x08

Тип первого обязательного параметра (int32), смотрите константы.

0x06

0x41

Константа SENTINEL, все параметры после данного значения дополнительные, к сожалению, после этого значения нет дополнительных аргументов. Если вы знаете причину этого, сообщите нам.