Чтение онлайн

ЖАНРЫ

Язык программирования C#9 и платформа .NET5
Шрифт:

return x + y;

}

В терминах CIL действие сложения двух чисел выражается посредством кода операции

0X58
. В том же духе вычитание двух чисел выражается с помощью кода операции
0X59
, а действие по размещению нового объекта в управляемой куче записывается с использованием кода операции
0X73
. С учетом описанной реальности "код CIL" , обрабатываемый JIT-компилятором, представляет собой не более чем порцию двоичных данных.

К счастью, для каждого двоичного кода операции CIL предусмотрен соответствующий мнемонический эквивалент.

Например, вместо кода
0X58
может применяться мнемонический эквивалент
add
, вместо
0X59
sub
, а вместо
0X73
newob
j. С учетом такой разницы между кодами операций и их мнемоническими эквивалентами декомпиляторы CIL, подобные
ildasm.exe
, транслируют двоичные коды операций сборки в соответствующие им мнемонические эквиваленты CIL. Вот как
ildasm.exe
представит в CIL предыдущий метод Add, написанный на языке C# (в зависимости от версии .NET Core вывод может отличаться):

.method assembly hidebysig static int32 Add(int32 x,int32 y) cil managed

{

// Code size 9 (0x9)

.maxstack 2

.locals init ([0] int32 int32 V_0)

IL_0000: /* 00 | */ nop

IL_0001: /* 02 | */ ldarg.0

IL_0002: /* 03 | */ ldarg.1

IL_0003: /* 58 | */ add

IL_0004: /* 0A | */ stloc.0

IL_0005: /* 2B | 00 */ br.s IL_0007

IL_0007: /* 06 | */ ldloc.0

IL_0008: /* 2A | */ ret

} //end of method

Если вы не занимаетесь разработкой исключительно низкоуровневого программного обеспечения .NET Core (вроде специального управляемого компилятора), то иметь дело с числовыми двоичными кодами операций CIL никогда не придется. На практике когда программисты, использующие .NET Core, говорят о "кодах операций CIL", они имеют в виду набор дружественных строковых мнемонических эквивалентов (что и делается в настоящей книге), а не лежащие в основе числовые значения.

Заталкивание и выталкивание: основанная на стеке природа CIL

В языках .NET Core высокого уровня (таких как С#) предпринимается попытка насколько возможно скрыть из виду низкоуровневые детали CIL. Один из особенно хорошо скрываемых аспектов — тот факт, что CIL является языком программирования, основанным на использовании стека. Вспомните из исследования пространств имен коллекций (см. главу 10), что класс

Stack<T>
может применяться для помещения значения в стек, а также для извлечения самого верхнего значения из стека с целью последующего использования. Разумеется, программисты на языке CIL не работают с объектом типа
Stack<T>
для загрузки и выгрузки вычисляемых значений, но применяемый ими образ действий похож на заталкивание и выталкивание.

Формально сущность, используемая для хранения набора вычисляемых значений, называется виртуальным стеком выполнения. Вы увидите, что CIL предоставляет несколько кодов операций, которые служат для помещения значения в стек; такой процесс именуется загрузкой. Кроме того, в CIL определены дополнительные коды операций,

которые перемещают самое верхнее значение из стека в память (скажем, в локальную переменную), применяя процесс под названием сохранение.

В мире CIL невозможно напрямую получать доступ к элементам данных, включая локально определенные переменные, входные аргументы методов и данные полей типа. Вместо этого элемент данных должен быть явно загружен в стек и затем извлекаться оттуда для использования в более позднее время (запомните упомянутое требование, поскольку оно содействует пониманию того, почему блок кода CIL может выглядеть несколько избыточным).

На заметку! Вспомните, что код CIL не выполняется напрямую, а компилируется по требованию. Во время компиляции кода CIL многие избыточные аспекты реализации оптимизируются. Более того, если для текущего проекта включена оптимизация кода (на вкладке Build (Сборка) окна свойств проекта в Visual Studio), то компилятор будет также удалять разнообразные избыточные детали CIL.

Чтобы понять, каким образом CIL задействует модель обработки на основе стека, создайте простой метод C# по имени

PrintMessage
, который не принимает аргументов и возвращает
void
. Внутри его реализации будет просто выводиться значение локальной переменной в стандартный выходной поток:

void PrintMessage

{

string myMessage = "Hello.";

Console.WriteLine(myMessage);

}

Если просмотреть код CIL, который получился в результате трансляции метода

PrintMessage
компилятором С#, то первым делом обнаружится, что в нем определяется ячейка памяти для локальной переменной с помощью директивы
.locals
. Затем локальная строка загружается и сохраняется в этой локальной переменной с применением кодов операций
ldstr
(загрузить строку) и
stloc.0
(сохранить текущее значение в локальной переменной, находящейся в ячейке
0
).

Далее с помощью кода операции

ldloc.0
(загрузить локальный аргумент по индексу
0
) значение (по индексу 0) загружается в память для использования в вызове метода
System.Console.WriteLine
, представленном кодом операции
call
. Наконец, посредством кода операции
ret
производится возвращение из функции. Ниже показан (прокомментированный) код CIL для метода
PrintMessage
(ради краткости из листинга были удалены коды операций
nop
):

.method assembly hidebysig static void PrintMessage cil managed

{

.maxstack 1

// Определить локальную переменную типа string (по индексу 0).

.locals init ([0] string V_0)

// Загрузить в стек строку со значением "Hello."

ldstr " Hello."

// Сохранить строковое значение из стека в локальной переменной.

Поделиться с друзьями: