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

ОГЛАВЛЕНИЕ

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