Опубликован: 28.06.2006 | Уровень: специалист | Доступ: платный | ВУЗ: Московский государственный технический университет им. Н.Э. Баумана
Лекция 5:

Common Intermediate Language

< Лекция 4 || Лекция 5: 123 || Лекция 6 >
Аннотация: Поток инструкций языка CIL. Инструкции для загрузки и сохранения значений, арифметические инструкции, инструкции для организации передачи управления.

Поток инструкций языка CIL

Язык CIL (Common Intermediate Language) является независимым от аппаратной платформы объектно-ориентированным ассемблером, используемым на платформе .NET для представления исполняемого кода. Для выполнения сборки .NET содержащийся в ней CIL-код переводится JIT-компилятором, входящим в состав CLR, в код конкретного процессора.

Инструкции CIL можно разделить на четыре основные группы. В первую группу входят инструкции общего назначения, которые служат для организации вычислений. Вторая группа содержит инструкции для работы с объектной моделью. Третья служит для генерации и обработки исключений. В четвертую мы относим неверифицируемые инструкции, которые генерируются, главным образом, компилятором языка C для представления небезопасных конструкций языка.

Разработчики набора инструкций CIL уделили большое внимание компактности CIL-кода. Для этого в набор инструкций было введено большое количество сокращенных вариантов инструкций, представляющих собой частные случаи других инструкций и кодирующихся меньшим количеством байт.

Формат потока инструкций

Тело метода в сборке .NET закодировано в виде потока инструкций языка CIL. Поток инструкций представляет собой массив байт, в котором размещены последовательности байт, кодирующие каждую инструкцию. При этом инструкции размещаются последовательно друг за другом без промежутков.

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

Разные инструкции CIL кодируются последовательностями байт различной длины. Размер каждой инструкции, а также порядок и смысл составляющих ее байт определяется описанием инструкции, которое можно найти в [3].

Формат инструкции

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

В разделе, посвященном виртуальной системе выполнения VES, говорилось о том, что операнды инструкций CIL размещаются на стеке вычислений. Тем не менее, многие инструкции имеют дополнительные встроенные операнды (inline operands), которые находятся прямо в потоке инструкций. Например, инструкция ldloc, загружающая на стек вычислений значение локальной переменной, имеет встроенный операнд, задающий номер переменной. А инструкция call, вызывающая метод, имеет встроенный операнд, задающий токен метаданных, по которому можно найти описание вызываемого метода. Встроенные операнды размещаются в потоке инструкций сразу после кода инструкции. В таблице 3.1 перечислены все варианты встроенных операндов. Для кодирования встроенных операндов, занимающих более одного байта и не являющихся токенами метаданных, используется порядок байт, при котором младший байт идет первым ("little-endian").

Таблица 3.1. Варианты встроенных операндов инструкций CIL
Операнд Размер в байтах Описание
none 0 У некоторых инструкций встроенные операнды отсутствуют
int8 1 Знаковое 8-битовое целое число
int32 4 Знаковое 32-битовое целое число
int64 8 Знаковое 64-битовое целое число
unsigned int8 1 Беззнаковое 8-битовое целое число
unsigned int16 2 Беззнаковое 16-битовое целое число
float32 4 32-битовое число с плавающей запятой
float64 8 64-битовое число с плавающей запятой
token 4 Токен метаданных
switch переменный Массив адресов переходов для инструкции switch

Особого внимания заслуживает встроенный операнд для инструкции switch. Эта инструкция осуществляет множественный условный переход в зависимости от некоторого целого значения, которое берется из стека вычислений. Ее встроенный операнд представляет собой массив адресов переходов. Он кодируется следующим образом: сначала идет 32-разрядное целое число без знака, обозначающее количество адресов переходов (размер массива), затем следуют сами адреса. При этом каждый адрес кодируется в виде 32-разрядного целого числа со знаком.

Рассмотрим примеры кодирования инструкций CIL:

  1. Инструкция ldarg.0 загружает на стек вычислений значение первого аргумента метода. Она является сокращенной версией инструкции ldarg, не содержит встроенных операндов и имеет код 0x02:
    /* 02 */ ldarg.0
  2. Инструкция arglist загружает на стек вычислений специальный описатель массива переменных параметров метода. Она не содержит встроенных операндов и имеет двухбайтовый код 0xFE 0x00:
    /* FE 00 */ arglist
  3. Инструкция ldc.i4.s 16 загружает на стек вычислений целочисленную константу 16. Она является сокращенной версией инструкции ldc.i4, имеет код 0x1F и содержит встроенный операнд типа int8:
    /* 1F | 10 */ ldc.i4.s 16
  4. Инструкция ldc.r4 1.0 загружает на стек вычисления число 1.0 (константу с плавающей запятой). Она имеет код 0x22 и содержит встроенный операнд типа float32:
    /* 22 | 0000803F */ ldc.r4 1.0
  5. Инструкция isinst System.String служит для динамической проверки типа объекта на стеке вычислений. Она имеет код 0x75 и содержит встроенный операнд типа token, в котором хранится токен метаданных, указывающий на тип:
    /* 75 | (02)00000F */ isinst System.String

    В скобки помещен первый байт токена метаданных, обозначающий номер таблицы метаданных. Обратите внимание, что значение токена метаданных для типа System.String в различных сборках может отличаться.

  6. Инструкция call System.String::Compare вызывает метод. Ее встроенный операнд содержит токен метаданных, указывающий на описание вызываемого метода:
    /* 28 | (06)0000CD */ call System.String::Compare
Адреса переходов

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

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

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

...
target_addr:	add		; цель перехода
       		...
	        br rel_addr	; инструкция перехода
next_addr:  	...

Здесь используется инструкция безусловного перехода br. При этом в качестве цели перехода выступает инструкция add, расположенная по абсолютному адресу target_addr. Если инструкция, следующая за инструкцией br, имеет абсолютный адрес next_addr, то адрес перехода rel_addr вычисляется следующим образом:

reladdr := target_addr - next_addr

Адреса переходов кодируются во встроенных операндах инструкций перехода в виде 8-битных или 32-битных целых чисел со знаком. При этом 8-битные адреса используются в сокращенных вариантах инструкций перехода.

Ограничения на последовательности инструкций

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

Упрощение JIT-компилятора благодаря введению ограничений на последовательности инструкций достигается, главным образом, за счет того, что эти ограничения позволяют использовать в JIT-компиляторе однопроходный алгоритм перевода CIL-кода в код процессора.

Давайте сформулируем эти ограничения:

  1. Ограничение на структуру стека вычислений.

    Структура стека вычислений определяется количеством и типами значений, лежащих на стеке.

    Для любой инструкции, входящей в поток инструкций, структура стека должна быть постоянной вне зависимости от того, из какого места программы на нее передается управление.

  2. Ограничение на размер стека вычислений.

    В заголовке метода должна быть указана максимальная глубина стека вычислений. Другими словами, максимальное количество значений, которое может размещаться на стеке вычислений в процессе выполнения метода, должно быть заранее известно еще до JIT-компиляции этого метода.

    Это ограничение, на первый взгляд, может показаться несколько странным, если принять во внимание, что благодаря ограничению 1 JIT-компилятор может легко вычислить максимальную глубину стека в процессе компиляции. Цель введения подобного ограничения состоит в том, чтобы JIT-компилятор, приступая к компиляции метода, мог сразу выделить нужное количество памяти под свои внутренние структуры данных.

  3. Ограничение на обратные переходы.

    Если при последовательном переборе инструкций, формирующих тело метода, JIT-компилятор встречает инструкцию, расположенную сразу за инструкцией безусловного перехода, и если на эту инструкцию еще не было перехода, то JIT-компилятор не может вычислить структуру стека вычислений для этой инструкции. В этом случае он предполагает, что стек для этой инструкции должен быть пуст.

    Чтобы лучше понять данную ситуацию, рассмотрим пример:

    ...
    		    	br L2
    		L1: 	ldc.0  ; здесь стек считается пустым
    	                ...
    		L2: 	br L1  ; обратный переход на L1
    	                ...

    Когда JIT-компилятор доходит до инструкции ldc.0, расположенной непосредственно после инструкции безусловного перехода br L2, он не может определить для нее структуру стека вычислений, так как еще не дошел до того места программы, откуда на нее передается управление. В принципе, просканировав дальше программу, это место можно обнаружить (это инструкция br L1 ), но тогда алгоритм JIT-компилятора должен быть многопроходным.

< Лекция 4 || Лекция 5: 123 || Лекция 6 >
Анастасия Булинкова
Анастасия Булинкова
Рабочим названием платформы .NET было
Bogdan Drumov
Bogdan Drumov
Молдова, Республика
Azamat Nurmanbetov
Azamat Nurmanbetov
Киргизия, Bishkek