К счастью, для каждого двоичного кода операции CIL есть соответствующая мнемоника. Например, мнемоника add может использоваться вместо 0X58, sub – вместо 0X59, a newobj – вместо 0X73. Ввиду указанных различий между мнемониками и кодами операций, нетрудно догадаться, что декомпиляторы CIL, такие как, например, ildasm.exe, переводят двоичные коды операций компоновочного блока в соответствующую мнемонику CIL.
.method public hidebysig static int32 Add(int32 x, int32 y) cil managed {
…
// Лексема 'add' является более понятной мнемоникой CIL,
// используемой для представления кода операции 0X58.
add
…
}
Тем, кто не сталкивается с необходимостью разработки низкоуровневого программного обеспечения .NET (например, пользовательского управляемого компилятора), обычно не приходится иметь дело непосредственно с числовыми кодами операций CIL. Поэтому практически всегда, когда программисты .NET говорят о "кодах операций CIL", они (как и я в этом тексте) имеют в виду набор более понятной мнемоники, а не лежащие в ее основе двоичные значения.
Добавление и извлечение данных: стековая природа CIL
Высокоуровневые языки .NET (например, такие как C#) пытаются максимально скрыть низкоуровневые сложности. Одним из аспектов разработки .NET, который оказывается скрытым особенно хорошо, является тот факт, что CIL является языком, целиком основанным на стековом программировании. Напомним, что при исследований пространства имен System.Collections (см. главу 7) мы с вами выяснили, что тип stack может использоваться для добавления значения в стек, а также для удаления из стека значения, размещенного на вершине стека. Конечно, разработчики CIL-приложений для загрузки и выгрузки значений не используют непосредственно объект System.Сollections.Stack, однако они применяют аналогичные операции.
Формально объект, используемый для хранения набора значений, называется
В CIL просто невозможно получить доступ к элементам данных непосредственно, и это касается как локально определенных переменных, так и входных аргументов методов, а также полей данных типов. Нужно сначала явно загрузить элемент в стек, чтобы затем "вытолкнуть" его оттуда для дальнейшего использования (помните об этом, ведь именно поэтому блок программного кода CIL может казаться несколько избыточным).
Чтобы понять, как CIL использует стековую модель, рассмотрим простой C#-метод PrintMessage, который не имеет аргументов и ничего не возвращает. В рамках реализации этого метода вы просто выводите значение локальной строковой переменной в поток стандартного вывода.
public void PrintMessage {
string myMessage = "Привет.";
Consolе.WriteLine(myMessage);
}
Если рассмотреть результат трансляции этого метода компилятором C# в термины CIL, вы сразу заметите, что метод PrintMessage определяет ячейку хранения для локальной переменной, используя директиву.locals. Локальная строка затем загружается и сохраняется в этой локальной неременной с помощью кодов операций ldstr (загрузка строки) и stloc.0 (это можно прочитать, как "запомнить текущее значение в локальной переменной с индексом 0").
Значение (снова с индексом 0) затем загружается в память с помощью кода операции ldloc.0 ("загрузить локальный аргумент с индексом 0") для использования в вызове метода System.Console.WriteLine (указанном с помощью кода операции call). Наконец, происходит возврат из функции через код операции ret.
.method public hidebysig instance void PrintMessage cil managed {
.maxstack 1
// Определение локальной строковой переменной (с индексом 0).
.locals init ([0] string myMessage)
// Загрузка строки со значением "Привет."
ldstr "Привет."
// Сохранение строкового значения в стеке в локальной переменной.
stloc.0
// Загрузка значения с индексом 0.
ldloc.0
// Вызов метода с текущим значением.
call void [mscorlib]System.Console::WriteLine(string)
ret
}