Если вы не занимаетесь разработкой исключительно низкоуровневого программного обеспечения .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
(ради краткости из листинга были удалены коды операций