Ключевыми элементами программы на IL являются сборки (assembly) и модули (module). Каждый модуль рассматривается как отдельный файл с кодом на IL. Сборка состоит из одного и более модулей.
Сборка всегда включает манифест, который содержит ряд данных:
Версию, имя, культуру и параметры безопасности
Список файлов, которые принадлежат сборке, а также криптографический хеш каждого файла
Типы, определенные в каждом файле, который используется сборкой
Цифровую подпись манифеста и публичный ключ, который используется для ее генерации (опционально)
Манифест един для всех модулей сборки и располагается в главном модуле сборки.
При определении программы на языке IL вначале идет заголовок программы, в котором и определяются базовые сведения о сборки и текущем модуле.
Например, возьмем из прошлой статьи простейшую программу, которая просто выводит строку на консоль:
.assembly extern System.Runtime { .ver 6:0:0:0 } .assembly extern System.Console { .ver 6:0:0:0 } .assembly HelloApp{} .module HelloApp.dll .class private auto Program { .method private static void main(string[] args) cil managed { .entrypoint ldstr "Hello, METANIT.COM!" call void [System.Console]System.Console::WriteLine(string) ret } }
Здесь заголовок программы выглядит следующим образом:
.assembly extern System.Runtime { .ver 6:0:0:0 } .assembly extern System.Console { .ver 6:0:0:0 } .assembly HelloApp{} .module HelloApp.dll
Заголовок может состоять из ряда функциональных блоков, основные из них:
.assembly: определяет параметры текущей сборки
.assembly extern: подключает внешние сборки
.file: определяет файлы, связанные с данной сборкой
.module: определение текущего модуля
.mresource: определяет ассоциированные со сборкой ресурсы
.module extern: подключает внешние модули
Если программа использует какой-то уже имеющийся функционал других сборок, то эти сборки надо подключить с помощью команды
.assembly extern [название_сборки]
Например, выражение
.assembly extern System.Runtime{}
подключает в программу функционал сборки System.Runtime.dll. В частности, она содержит определения базовых типов, как string, int и т.д. В программе выше как раз используется тип string для определения строки для вывода на консоль.
И при подключении сборки для нее может определяться ряд параметров:
.ver: версия сборки в формате .ver Int32:Int32:Int32:Int32
(старшая версия, младшая версия, номер
билда и ревизии). Например, в коде
.assembly extern System.Runtime { .ver 6:0:0:0 }
указывается, что используется 6-я версия сборки (.net 6.0)
.publickey: публичный ключ для генерации криптографического хеш-кода сборки в формате publickey=(значение)
.
.publickeytoken: младшие 8 байт хеша SHA-1 публичного ключа. Например:
.assembly extern System.Runtime { .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) .ver 6:0:0:0 }
.hash: значение хеша подключаемой сборки в формате .hash=(значение)
.culture код_культуры: языковая культура сборки. Код культуры соответствует формату IETF RFC1766: "язык-страна/регион", где язык представляет двухсимвольный код в нижнем регистре в формате ISO 639-1, а "страна/регион" - двухсимвольный код в верхнем регистре в формате ISO 3166
.custom: кастомный атрибут в формате .custom=(определение_атрибута)
После подключения внешних сборок идет определение текущей сборки. В примере выше:
.assembly HelloApp{}
Директива .assembly определяет манифест и указывает, к какой сборке принадлежит текщий модуль. После директивы .assembly идет название текущей сборки.
Далее внутри фигурных скобок идут параметры сборки. Они почти такие же, которые используются при подключении внешних сборок:
.ver: версия сборки
.publickey: публичный ключ для генерации криптографического хеш-кода сборки
.hash algorithm: алгоритм хеширования в формате .hash algorithm Int32
По умолчанию следует применять алгоритм SHA-1, для этого передается значение 32772 (0x8004 - в шестнадцатиричном формате).
.culture код_культуры: языковая культура сборки.
.custom: кастомный атрибут
В примере выше никаких параметров сборки не определялось. Но их также можно задать:
.assembly MSILApp { .hash algorithm 0x00008004 .ver 1:0:0:0 }
Сборка состоит из модулей. Модуль представляет отдельный файл, который содержит некоторый исполняемый код. Для определения модуля применяется директива .module, после которой указывается имя текущего файла:
.module HelloApp.dll
Вкратце рассмотрим некоторые заголовки, которые также могут применяться. Как правило они передают значения для генерируемого PE-файла и не являются особенностью MSIL или .NET.
Директива .cornflags устанавливает поле CLI-заголовка выходного PE-файла. по умолчанию это значение должно равняться 1.
.corflags 0x00000001
Директива .subsystem определяет тип среды приложения с помощью сохранения специального значения в заголовке PE-файла. Теоретически здесь может использоваться любое 32-битное целочисленное значение, однако обычно применяются следующие значения: 2 для программ с графическим интерфейсом и 3 для консольных программ. Например:
.subsystem 0x0003 // WINDOWS_CUI
Директива .imagebase задает значение для заголовка imagebase в PE-файле. Если вкратце, этот заголовок устанавливает предпочтительный адрес в виртуальной памяти, по которому файл будет загружаться. Например:
.imagebase 0x10000000
Для DLL обычно это адрес 0x10000000, а для EXE - 0x00400000
Директива .file alignment задает значение для заголовка FileAlignment в PE-файле. Этот заголовок устанавливает значение в байтах, которое используется для выравнивания данных. Это значение должно быть равно 0x200. Например:
.file alignment 0x00000200
Директива .stackreserve определяет объем выделяемого стека в байтах. Это значение должно быть равно 0x100000 (1Mb). Например:
.stackreserve 0x00100000