Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
В табл. 19.4 описаны наиболее полезные коды операций, имеющие прямое отношение к логике реализации членов; они сгруппированы по функциональности.
Коды операций из следующей обширной категории (подмножество которых описано в табл. 19.5) применяются для загрузки (заталкивания) аргументов в виртуальный стек выполнения. Обратите внимание, что все эти ориентированные на загрузку коды операций имеют префикс Id (load — загрузить).
В дополнение к набору кодов операций, связанных с загрузкой, CIL предоставляет многочисленные коды операций, которые явно извлекают из стека самое верхнее значение. Как было показано в нескольких начальных примерах, извлечение значения из стека обычно предусматривает его сохранение во временном локальном хранилище с целью дальнейшего использования (наподобие параметра для предстоящего вызова метода). Многие коды операций, извлекающие текущее значение из виртуального стека выполнения, снабжены префиксом st (store — сохранить). В табл. 19.6 описаны некоторые распространенные коды операций.
Имейте в виду, что различные коды операций CIL будут неявно извлекать значения из стека во время выполнения своих задач. Например, при вычитании одного числа из другого с применением кода операции sub должно быть очевидным то, что перед самим вычислением операция sub должна извлечь из стека два следующих доступных значения. Результат вычисления снова помещается в стек.
Директива .maxstack
При написании кода реализации методов на низкоуровневом языке CIL необходимо помнить о специальной директиве под названием .maxstack. С ее помощью устанавливается максимальное количество переменных, которые могут находиться внутри стека в любой заданный момент времени на протяжении периода выполнения метода. Хорошая новость в том, что директива .maxstack имеет стандартное значение (8), которое должно подойти для подавляющего большинства создаваемых методов. Тем не менее, если вы хотите указывать все явно, то можете вручную подсчитать количество локальных переменных в стеке и определить это значение явно:
.method public hidebysig instance void
Speak() cil managed
{
// Внутри области действия этого метода в стеке находится
// в точности одно значение (строковый литерал).
.maxstack 1
ldstr "Hello there..."
call void [mscorlib]System.Console::WriteLine(string)
ret
}
Объявление локальных переменных в CIL
Теперь давайте посмотрим, как объявлять локальные переменные. Предположим, что необходимо построить в CIL метод по имени MyLocalVariables(), который не принимает аргументы и возвращает void, и определить в нем три локальные переменные с типами System.String, System.Int32 и System.Object. В C# такой метод выглядел бы следующим образом (вспомните, что локальные переменные не получают стандартные значения и потому перед использованием должны быть инициализированы):
public static void MyLocalVariables()
{
string myStr = "CIL code is fun!";
int myInt = 33;
object myObj = new object();
}
А вот как реализовать метод MyLocalVariables() на языке CIL:
.method public hidebysig static void
MyLocalVariables() cil managed
{
.maxstack 8
// Определить три локальные переменные.
.locals init (string myStr, int32 myInt, object myObj)
// Загрузить строку в виртуальный стек выполнения.
ldstr "CIL code is fun!"
// Извлечь текущее значение и сохранить его в локальной переменной [0].
stloc.0
// Загрузить константу типа i4 (сокращение для int32) со значением 33.
ldc.i4.s 33
// Извлечь текущее значение и сохранить его в локальной переменной [1].
stloc.1
// Создать новый объект и поместить его в стек.
newobj instance void [mscorlib]System.Object::.ctor()
// Извлечь текущее значение и сохранить его в локальной переменной [2].
stloc.2
ret
}
Первым шагом при размещении локальных переменных с помощью CIL является применение директивы .locals в паре с атрибутом init. Каждая переменная идентифицируется своим типом данных и необязательным именем. После определения локальных переменных значения загружаются в стек (с использованием различных кодов операций загрузки) и сохраняются в этих локальных переменных (с помощью кодов операций сохранения).
Отображение параметров на локальные переменные в CIL
Вы уже видели, каким образом объявляются локальные переменные в CIL с применением директивы .locals init; однако осталось еще взглянуть на то, как входные параметры отображаются на локальные переменные. Рассмотрим показанный ниже статический метод С#:
public static int Add(int a, int b)
{
return a + b;
}
Такой с виду невинный метод требует немалого объема кодирования на языке CIL. Во-первых, входные аргументы (а и b) должны быть помещены в виртуальный стек выполнения с использованием кода операции ldarg (load argument — загрузить аргумент). Во-вторых, с помощью кода операции add из стека будут извлечены следующие два значения и просуммированы с сохранением результата обратно в стек. В-третьих, сумма будет извлечена из стека и возвращена вызывающему коду посредством кода операции ret. Дизассемблировав этот метод C# с применением ildasm.exe, вы обнаружите множество дополнительных лексем, которые были внедрены в процессе компиляции, но основная часть кода CIL довольно проста:
.method public hidebysig static int32 Add(int32 a,
int32 b) cil managed
{
.maxstack 2
ldarg.0 // Загрузить а в стек.
ldarg.1 // Загрузить b в стек.
add // Сложить оба значения.
ret
}
Скрытая ссылка this
Обратите внимание, что ссылка на два входных аргумента (а и b) в коде CIL производится с использованием их индексных позиций (0 и 1), т.к. индексация в виртуальном стеке выполнения начинается с нуля.
Во время исследования или написания кода CIL нужно помнить о том, что каждый нестатический метод, принимающий входные аргументы, автоматически получает неявный дополнительный параметр, который представляет собой ссылку на текущий объект (подобно ключевому слову this в С#). Скажем, если бы метод Add() был определен как нестатический:
// Больше не является статическим!
public int Add(int a, int b)
{