В прошлом почти все компиляторы генерировали код для конкретных процессорных архитектур. Все CLR-совместимые компиляторы вместо этого генерируют IL-код, который также называется управляемым модулем, потому что CLR управляет его жизненным циклом и выполнением. Рассмотрим составные части управляемого модуля:
1. ЗаголовокPE32 или PE32+: Файл с заголовком в формате PE32 может выполняться в 32- или 64-разрядной ОС, а с заголовком PE32+ только в 64-разрядной ОС. Заголовок показывает тип файла: GUI, GUI или DLL, он также имеет временную метку, показывающую, когда файл был собран. Для модулей, содержащих только IL-код, основной объем информации в РЕ-заголовке игнорируется, Для модулей, содержащих процессорный код, этот заголовок содержит сведения о процессорном коде.
2. Заголовок CLR: Содержит информацию, которая превращает этот модуль в управляемый. Заголовок включает нужную версию СLR, некоторые флаги, метку метаданных, точки входа в управляемый модуль (метод Main), месторасположение и размер метаданных модуля, ресурсов и т.д.
|
|
3. Метаданные - это набор таблиц данных, описывающих то, что определено в модуле. Есть два основных вида таблиц: описывающие типы и члены, определенные в вашем исходном коде, и описывающие типы и члены, на которые имеются ссылки в вашем исходном коде. Метаданные служат многим целям:
a. устраняют необходимость в заголовочных и библиотечных файлах при компиляции, так как все сведения о типах и членах, на которые есть ссылки, содержатся в файле с IL-кодом, в котором они реализованы. Компиляторы могут читать метаданные прямо из управляемых модулей.
b. при компиляции IL-кода в машинный код CLR выполняет верификацию (проверку «безопасности» выполнения кода) используя метаданные, например, нужное ли число параметров передается методу, корректны ли их типы, правильно ли используется возвращаемое значение и т.д.
c. позволяют сборщику мусора отслеживать жизненный цикл объектов и т.д.
4. IL-код: управляемый код, создаваемый компилятором при компиляции исходного кода. Во время исполнения CLR компилирует IL-код в команды процессора.
По умолчанию CLR-совместимые компиляторы генерируют управляемый код, безопасность выполнения которого поддается проверке средой CLR. Вместе с тем возможно разрабатывать неуправляемый или «небезопасный» код, которому разрешается работать непосредственно с адресами памяти и управлять байтами в этих адресах. Эта возможность, обычно полезна при взаимодействии с неуправляемым кодом или при необходимости добиться максимальной производительности при выполнении критически важных алгоритмов. Однако использовать неуправляемый код довольно рискованно, т.к. он способен разрушить существующие структуры данных.
|
|
Чтобы понять принцип выполнения программы в среде CLR рассмотрим небольшой пример:
Управляемый модуль | Console | |||
static void Main () { Console.WriteLine(²УРА! ²); Console.WriteLine(²Сегодня информатика!!! ²); } | … static void WriteLine(string); … | |||
Команды процессора | ||||
JITCompiler | ||||
1. В сборке, реализующей данный тип (Console), найти в метаданных вызываемый метод (WriteLine). 2. Взять из метаданных IL-код для этого метода. 3. Выделить блок памяти. 4. Скомпилировать IL-кода команды процессора и сохранить процессорный код в памяти, выделенной на этапе 3. 5. Изменить точку входа метода в таблице типа, чтобы она указывала на блок памяти, выделенный на этапе 3. 6. Передать управление процессорному коду, содержащемуся в выделенном блоке памяти. |
Непосредственно перед исполнением функции Main CLR находит все типы, на которые ссылается ее код. В нашем случае метод Main ссылается на единственный тип — Console, и CLR выделяет единственную внутреннюю структуру WriteLine.
Когда Main первый раз обращается к WriteLine, вызывается функция JITCompiler (условное название), которая отвечает за компиляцию IL-кода вызываемого метода в собственные команды процессора. Функции JITCompiler известен вызываемый метод и тип, в котором он определен. JITCompiler ищет в метаданных соответствующей сборки IL-код вызываемого метода, затем проверяет и компилирует IL-код в собственные команды процессора, которые сохраняются в динамически выделенном блоке памяти. После этого JITCompiler возвращается к внутренней структуре данных типа и заменяет адрес вызываемого метода адресом блока памяти, содержащего собственные команды процессора. В завершение JITCompiler передает управление коду в этом блоке памяти. Далее управление возвращается в Main, который продолжает работу в обычном порядке.
Затем Main обращается к WriteLine вторично. К этому моменту код WriteLine уже проверен и скомпилирован, так что производится обращение к блоку памяти, минуя вызов JITCompiler. Отработав, метод WriteLine возвращает управление Main.
Таким образом, за счет такой компиляции производительность теряется только при первом вызове метода. Все последующие обращения к одной и той же структуре выполняются «на полной скорости», без повторной верификация и компиляция.