Переменные и циклы на CIL

68

Объявление локальных переменных в 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. Если нет, происходит выход из метода.

Пройди тесты
Лучший чат для C# программистов