Переменные и циклы на CIL
68C# --- Сборки .NET --- Переменные и циклы на CIL
Объявление локальных переменных в CIL
Давайте посмотрим, как в CIL объявлять локальную переменную. Для этого предположим, что необходимо создать в CIL метод по имени MyLocalVariables(), не принимающий аргументов и возвращающий void, и определить внутри него три локальных переменных типа System.String, System.Int32 и System.Object:
.method public hidebysig static void MyLocalVariables() cil managed
{
.maxstack 8
// Определение локальных переменных
.locals init ([0] string s, [1] int32 i, [2] object obj)
// Загрузка строки в стек
ldstr "Всем привет"
// Извлечение значения из стека и присваивание его переменной
stloc.0
ldc.i4 10
stloc.1
// Создание нового объекта
newobj instance void [mscorlib]System.Object::.ctor()
stloc.2
ret
}
Как здесь видно, в первую очередь для размещения локальных переменных в CIL должна использоваться директива .locals вместе с атрибутом init. Внутри соответствующих скобок с каждой переменной необходимо ассоциировать определенный числовой индекс (в примере это [0], [1] и [2]). Далее вместе с каждым из этих индексов понадобится указать тип данных (обязательно) и имя переменной (необязательно).
После определения локальных переменных останется только загрузить значения в стек (с помощью различных кодов операций, предназначенных для загрузки) и сохранить их в этих локальных переменных (посредством различных кодов операций, предназначенных для сохранения значений).
Отображение параметров на локальные переменные в CIL
Объявление локальных переменных непосредственно в CIL с использованием директивы .local init уже было показано, а теперь необходимо ознакомиться с отображением входных параметров на локальные методы. Рассмотрим следующий статический метод на C#:
public static int Add(int a, int b)
{
return a + b;
}
В CIL этот метод, который настолько просто выглядит в C#, потребует массы дополнений. Чтобы представить его на CIL, понадобится, во-первых, разместить входные аргументы (а и Ь) в виртуальном стеке выполнения с помощью кода операции ldarg, во-вторых, использовать код операции add для извлечения двух значений из стека, вычисления их суммы и затем опять ее сохранения в стеке, и, в-третьих, извлечь эту сумму из стека и вернуть ее вызывающему коду с использованием кода операции ret.
Если просмотреть этот метод на C# в утилите ildasm.exe, то видно, что csc.exe вставил массу дополнительных лексем, хотя основная часть CIL-кода выглядит довольно просто:
.method public hidebysig static int32 Add(int32 a,
int32 b) cil managed
{
.maxstack 2
ldarg.0 // Загрузка а в стек
ldarg.1 // Загрузка b в стек
add // Сложение обоих значений
ret
}
Скрытая ссылка this
Обратите внимание, что в CIL-коде для ссылки на входные аргументы (а и b) используются их индексные позиции (0 и 1), причем нумерация этих позиций в виртуальном стеке выполнения начинается с нуля.
При изучении и создании CIL-кода нужно помнить о том, что каждый нестатический метод, который принимает входные аргументы, автоматически получает дополнительный входной параметр, представляющий собой ссылку на текущий объект (похожую на ключевое слово this в C#). Если, например, метод Add() определен не как статический:
// Более не является статическим!
public int Add(int a, int b)
{
return a + b;
}
то входные аргументы а и b будут загружаться с помощью ldarg.1 и ldarg.2 (а не ldarg. О и ldarg.1, как ожидалось). Объясняется это тем, что в ячейке с номером О будет содержаться неявная ссылка this. Ниже приведен псевдокод, который позволит удостовериться в этом:
.method public hidebysig static int32 AddTwoIntParams (
MyClass this, int32 a, int32 b) cil managed
{
ldarg.0 // Загрузка MyClass в стек,
ldarg.1 // Загрузка а в стек,
ldarg.2 // Загрузка b в стек.
}
Представление итерационных конструкций в CIL
Итерационные конструкции в языке программирования C# представляются с помощью таких ключевых слов, как for, foreach, while и do, каждое из которых имеет специальное представление в CIL. Для примера рассмотрим следующий классический цикл for:
public static void MyIteration()
{
for (int i = 0; i < 10; i++) ;
}
Вспомните, что для управления ходом выполнения программы на основе условий в CIL применяются коды операций br (br, blt и т.д.). В данном примере условие гласит, что выполнение цикла должно завершаться тогда, когда значение локальной переменной i становится больше или равно 10. С каждым проходом к значению i добавляется 1, после чего проверяемое условие вычисляется заново.
Кроме того, при использовании любого из связанных с ветвлением кодов операций в CIL должна быть определена специальная метка (или две) для обозначения места, куда будет произведен переход в случае удовлетворения условия. С учетом всего вышесказанного взглянем на следующий (расширенный) код CIL, сгенерированный в ildasrn.exe (вместе с соответствующими метками):
.method public hidebysig static void MyIteration() cil managed
{
// Размер кода: 19 (0x13)
.maxstack 2
.locals init ([0] int32 i,
[1] bool CS$4$0000)
IL_0000: nop
IL_0001: ldc.i4.0
IL_0002: stloc.0
IL_0003: br.s IL_0009
IL_0005: ldloc.0
IL_0006: ldc.i4.1
IL_0007: add
IL_0008: stloc.0
IL_0009: ldloc.0
IL_000a: ldc.i4.s 10
IL_000c: clt
IL_000e: stloc.1
IL_000f: ldloc.1
IL_0010: brtrue.s IL_0005
IL_0012: ret
} // end of method Program::MyIteration
Этот CIL-код начинается с определения локальной переменной int32 и ее загрузки в стек. После этого осуществляется переход туда и обратно между метками IL_0009 и IL_0005, во время каждого из которых значение i увеличивается на 1 и проверяется на предмет того, по-прежнему ли оно меньше 10. Если нет, происходит выход из метода.