Литмир - Электронная Библиотека
A
A

Формально сущность, используемая для хранения набора вычисляемых значений, называется виртуальным стеком выполнения. Вы увидите, что 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

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

  .locals init ([0] string V_0)

<b>  // Загрузить в стек строку со значением &quot;Hello.&quot;</b>

  ldstr &quot; Hello.&quot;

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

  stloc.0

<b>  // Загрузить значение по индексу 0.</b>

  ldloc.0

<b>  // Вызвать метод с текущим значением.</b>

  call void [System.Console]System.Console::WriteLine(string)

  ret

}

На заметку! Как видите, язык CIL поддерживает синтаксис комментариев в виде двойной косой черты (и вдобавок синтаксис

/*...*/
). Подобно компилятору C# компилятор CIL игнорирует комментарии в коде.

Теперь, когда вы знаете основы директив, атрибутов и кодов операций CIL, давайте приступим к практическому программированию на CIL, начав с рассмотрения темы возвратного проектирования.

Возвратное проектирование

В главе 1 было показано, как применять утилиту

ildasm.exe
для просмотра кода CIL, сгенерированного компилятором С#. Тем не менее, вы можете даже не подозревать, что эта утилита позволяет сбрасывать код CIL, содержащийся внутри загруженной в нее сборки, во внешний файл. Полученный подобным образом код CIL можно редактировать и компилировать заново с помощью компилятора CIL (
ilasm.exe
).

Выражаясь формально, такой прием называется возвратным проектированием и может быть полезен в избранных обстоятельствах, которые перечислены ниже.

• Вам необходимо модифицировать сборку, исходный код которой больше не доступен.

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

• Вы конструируете библиотеку взаимодействия с СОМ и хотите учесть ряд атрибутов COM IDL, которые были утрачены во время процесса преобразования (такие как COM-атрибут

[helpstring]
).

Чтобы ознакомиться с процессом возвратного проектирования, создайте новый проект консольного приложения .NET Core на языке C# по имени

RoundTrip
посредством интерфейса командной строки .NET Core (CLI):

dotnet new console -lang c# -n RoundTrip -o .\RoundTrip -f net5.0

Модифицируйте операторы верхнего уровня, как показано ниже:

<b>// Простое консольное приложение С#.</b>

Console.WriteLine(&quot;Hello CIL code!&quot;);

Console.ReadLine();

Скомпилируйте программу с применением интерфейса CLI:

dotnet build

На заметку! Вспомните из главы 1, что результатом компиляции всех сборок .NET Core (библиотек классов и консольных приложений) будут файлы с расширением

*.dll
, которые выполняются с применением интерфейса .NET Core CLI. Нововведением .NET Core 3+ (и последующих версий) является то, что файл
dotnet.exe
копируется в выходной каталог и переименовывается согласно имени сборки. Таким образом, хотя выглядит так, что ваш проект был скомпилирован в
RoundTrip.exe
, на самом деле он компилируется в
RoundTrip.dll
, а файл
dotnet.exe
копируется в
RoundTrip.exe
вместе с обязательными аргументами командной строки, необходимыми для запуска
Roundtrip.dll
.

347
{"b":"847442","o":1}