Common Intermediate Language и системное программирование в Microsoft .NET

Common Intermediate Language

Разбить на страницы
Показывать лекцию целиком

Поток инструкций языка 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").

Варианты встроенных операндов инструкций 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:

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

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

  • Инструкция 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-кода в код процессора.

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

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

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

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

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

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

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

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

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

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

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

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

  • Язык CIL: инструкции общего назначения

    В этом разделе мы рассмотрим ту часть инструкций языка CIL, которая служит для организации вычислений, а именно:

  • инструкции для загрузки и сохранения значений;
  • арифметические инструкции;
  • инструкции для организации передачи управления.
  • Инструкции для загрузки и сохранения значений

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

    Загрузка констант

    Эта группа инструкций (см. таблицу 3.2) служит для загрузки константных значений на стек вычислений. При этом значения кодируются в самих инструкциях в виде их кодов или встроенных операндов.

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

    ... -> ... , constant
    Инструкции для загрузки констант
    Код Инструкция Встроенный операнд Описание
    0x14 ldnull - Загрузка константы null
    0x15 ldc.m1 - Загрузка целого числа -1( int32 )
    0x16 - 0x1E ldc.0 - ldc.8 - Загрузка целых чисел от 0 до 8 ( int32 )
    0x1F ldc.s int8 Загрузка целых чисел от -128 до 127 ( int32 )
    0x20 ldc.i4 int32 Загрузка целых чисел ( int32 )
    0x21 ldc.i8 int64 Загрузка целых чисел ( int64 )
    0x22 ldc.r4 float32 Загрузка чисел с плавающей запятой ( F )
    0x23 ldc.r8 float64 Загрузка чисел с плавающей запятой ( F )

    Работа с переменными и параметрами методов

    Локальные переменные и параметры методов имеют номера от 0 до 65534. Существуют три варианта инструкций для работы с переменными и параметрами:

  • сокращенные инструкции, которые работают с переменными и параметрами, имеющими номера от 0 до 3;
  • сокращенные инструкции, допускающие номера переменных и параметров от 0 до 255;
  • обычные инструкции, работающие с любыми переменными и параметрами.
  • В таблице 3.3 перечислены инструкции, выполняющие загрузку значений переменных и параметров на стек вычислений. Все они имеют следующую диаграмму стека:

    ... -> ... , value
    Инструкции для загрузки параметров и локальных переменных
    Код Инструкция Встроенный операнд Описание
    0x02 - 0x05 ldarg.0 - ldarg.3 - Загрузка параметров с номерами от 0 до 3
    0x06 - 0x09 ldloc.0 - ldloc.3 - Загрузка локальных переменных с номерами от 0 до 3
    0x0E ldarg.s unsigned int8 Загрузка параметров с номерами от 0 до 255
    0x11 ldloc.s unsigned int8 Загрузка локальных переменных с номерами от 0 до 255
    0xFE 0x09 ldarg unsigned int16 Загрузка параметров с номерами от 0 до 65534
    0xFE 0x0C ldloc unsigned int16 Загрузка локальных переменных от 0 до 65534

    Кроме инструкций, загружающих значения переменных и параметров, существуют инструкции, загружающие на вершину стека вычислений адреса переменных и параметров (см. таблицу 3.4). Загружаемые адреса имеют тип управляемых указателей. Диаграмма стека для этих инструкций выглядит следующим образом:

    ... -> ... , address
    Инструкции для загрузки адресов параметров и локальных переменных
    Код Инструкция Встроенный операнд Описание
    0x0F ldarga.s unsigned int8 Загрузка адресовпараметров с номерами от 0 до 255
    0x12 ldloca.s unsigned int8 Загрузка адресов локальных переменных с номерами от 0 до 255
    0xFE 0x0A ldarga unsigned int16 Загрузка адресов параметров с номерами от 0 до 65534
    0xFE 0x0D ldloca unsigned int16 Загрузка адресов локальных переменных с номерами от 0 до 65534

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

    ... , value -> ...
    Инструкции для сохранения значений в параметрах и локальных переменных
    Код Инструкция Встроенный операнд Описание
    0x0A stloc.0 - stloc.3 - Сохранение значений в локальных переменных с номерами от 0 до 3
    0x10 starg.s unsigned int8 Сохранение значений в параметрах с номерами от 0 до 255
    0x13 stloc.s unsigned int8 Сохранение значений в локальных переменных с номерами от 0 до 255
    0xFE 0x0B starg unsigned int16 Сохранение значений в параметрах с номерами от 0 до 65534
    0xFE 0x0E stloc unsigned int16 Сохранение значений в локальных переменных с номерами от 0 до 65534

    Косвенная загрузка и сохранение значений

    При косвенной загрузке и сохранении значений работа с памятью осуществляется через адреса (управляемые и неуправляемые указатели).

    Особенностью инструкций данной группы является наличие разных инструкций для работы со значениями разных типов. Причина в том, что при загрузке или сохранении значения бывает необходимо выполнить его преобразование к другому типу, а так как JIT-компилятор в процессе компиляции не собирает информацию о типах управляемых указателей, ему надо явно указывать тип загружаемых и сохраняемых значений. Необходимость выполнения преобразований объясняется тем, что не все примитивные типы могут находиться на стеке вычислений (поэтому, например, значение типа int8 при загрузке на стек расширяется до int32 ).

    В таблице 3.6 перечислены инструкции для косвенной загрузки значений. Обратите внимание, что инструкции ldind.i8 и ldind.u8 являются псевдонимами (имеют один и тот же код). Дело в том, что загрузка любых 64-разрядных целых значений на стек не вызывает их преобразования, ибо хотя на стеке не предусмотрено наличие беззнаковых 64-разрядных значений, их загрузка все равно сводится к простому побитовому копированию. Вышесказанное справедливо также и для 32-разрядных целых значений, но для их загрузки зачем-то зарезервировано сразу две инструкции.

    Инструкции для косвенной загрузки значений
    Код Инструкция Встроенный операнд Описание
    0x46 ldind.i1 - Косвенная загрузка значения int8
    0x47 ldind.u1 - Косвенная загрузка значения unsigned int8
    0x48 ldind.i2 - Косвенная загрузка значения int16
    0x49 ldind.u2 - Косвенная загрузка значения unsigned int16
    0x4A ldind.i4 - Косвенная загрузка значения int32
    0x4B ldind.u4 - Косвенная загрузка значения unsigned int32
    0x4C ldind.i8(ldind.u8) - Косвенная загрузка значения int64 и unsigned int64
    0x4D ldind.i - Косвенная загрузка значения native int
    0x4E ldind.r4 - Косвенная загрузка значения float32
    0x4 ldind.r8 - Косвенная загрузка значения float64
    0x50 ldind.ref - Косвенная загрузка объектной ссылки

    Диаграмма стека для инструкций косвенной загрузки выглядит следующим образом:

    ... , address -> ... , value

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

    Инструкции для косвенного сохранения значений
    Код Инструкция Встроенный операнд Описание
    0x51 stind.ref - Косвенное сохранение объектной ссылки
    0x52 stind.i1 - Косвенное сохранение значения int8
    0x53 stind.i2 - Косвенное сохранение значения int16
    0x54 stind.i4 - Косвенное сохранение значения int32
    0x55 stind.i8 - Косвенное сохранение значения int64
    0x56 stind.r4 - Косвенное сохранение значения float32
    0x57 stind.r8 - Косвенное сохранение значения float64
    0xDF stind.i - Косвенное сохранение значения native int

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

    ... , address , value -> ...

    Специальные инструкции для работы со стеком

    В отличие от "железных" стековых процессоров, CLI не содержит развитой системы инструкций для чисто стековых манипуляций. В таблице 3.8 представлены две имеющиеся в наличии инструкции.

    Специальные инструкции для работы со стеком
    Код Инструкция Встроенный операнд Описание
    0x25 dup - Копирование значения на вершине стека: ..., value ->..., value, value
    0x26 pop - Удаление значения с вершины стека . .., value ->...

    Арифметические инструкции

    Арифметические инструкции можно разделить на четыре категории:

  • бинарные операции;
  • унарные операции;
  • инструкция ckfinite, проверяющая конечность значений с плавающей точкой;
  • инструкции преобразования значений.
  • Бинарные арифметические операции

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

    ... , value1 , value2 -> ... , result

    Действие бинарных операций можно записать как

    result := value1 op value2,

    то есть например, если op соответствует операции вычитания, то из value1 вычитается value2.

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

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

    В таблице 3.9 представлены базовые инструкции, выполняющие бинарные операции.

    Базовые бинарные арифметические операции
    Код Инструкция Встроенный операнд Описание
    0x58 add - Сложение
    0x59 sub - Вычитание
    0x5A mul - Умножение
    0x5B div - Деление
    0x5C div.un - Деление беззнаковых целых чисел
    0x5D rem - Остаток от деления
    0x5E rem.un - Остаток от деления беззнаковых целых чисел
    0x5F and - Побитовое И
    0x60 or - Побитовое ИЛИ
    0x61 xor - Побитовое ИСКЛЮЧАЮЩЕЕ ИЛИ

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

    Бинарные арифметические операции с контролем переполнения
    Код Инструкция Встроенный операнд Описание
    0xD6 add.ovf - Сложение целых чисел со знаком с контролем переполнения
    0xD7 add.ovf.un - Сложение целых чисел без знака с контролем переполнения
    0xD mul.ovf - Умножение целых чисел со знаком с контролем переполнения
    0xD mul.ovf.un - Умножение целых чисел без знака с контролем переполнения
    0xD sub.ovf - Вычитание целых чисел со знаком с контролем переполнения
    0xD sub.ovf.un - Вычитание целых чисел без знака с контролем переполнения

    Операции сдвига (см. таблицу 3.11) выполняют сдвиг значения первого операнда ( value1 ) в нужную сторону на количество бит, указанное во втором операнде ( value2 ).

    Операции сдвига
    Код Инструкция Встроенный операнд Описание
    0x62 shl - Сдвиг целых чисел влево
    0x63 shr - Сдвиг целых чисел со знаком вправо
    0x64 shr.un - Сдвиг целых чисел без знака вправо

    Операции, приведенные в таблице 3.12, выполняют сравнение значений своих операндов. Результатом сравнения являются числа 0 или 1 (типа int32 ). Число 0 обозначает ложь, а число 1 - истину.

    Операции сравнения
    Код Инструкция Встроенный операнд Описание
    0xFE 0x01 ceq -

    Сравнение на равенство.

    Для целых чисел:

    I ceq I => 1,

    иначе => 0

    Для чисел с плавающей запятой:

    +inf ceq +inf => 1,
    -inf ceq -inf => 1,
    A ceq A => 1,

    иначе => 0

    0xFE 0x02 cgt -

    Сравнение на "больше".

    Для целых чисел:

    J cgt I => 1,

    иначе => 0

    Для чисел с плавающей запятой:

    A cgt -inf => 1,
    +inf cgt A => 1,
    +inf cgt -inf => 1,
    B cgt A => 1,

    иначе => 0

    0xFE 0x04 clt - Сравнение на "меньше".

    Для целых чисел:

    I clt J => 1,

    иначе => 0

    Для чисел с плавающей запятой:

    A clt +inf => 1,
    -inf clt A => 1,
    -inf clt +inf => 1,
    A clt B => 1,

    иначе => 0

    0xFE 0x03 cgt.un -

    Сравнение на "больше" беззнаковых целых чисел или неупорядоченных чисел с плавающей запятой. (Два числа с плавающей запятой называются неупорядоченными, если хотя бы одно из них равно NaN.)

    Для целых чисел:

    L cgt.un K => 1,

    иначе => 0

    Для чисел с плавающей запятой:

    NaN cgt.un C => 1,
    C cgt.un NaN => 1,
    A cgt.un -inf => 1,
    +inf cgt.un A => 1,
    +inf cgt.un -inf => 1,
    B cgt.un A => 1,

    иначе => 0

    0xFE 0x05 clt.un -

    Сравнение на "меньше" беззнаковых целых чисел или неупорядоченных чисел с плавающей запятой.

    Для целых чисел:

    K clt.un L => 1,

    иначе => 0

    Для чисел с плавающей запятой:

    NaN clt.un C => 1,
    C clt.un NaN => 1,
    A clt.un +inf => 1,
    -inf clt.un A => 1,
    -inf clt.un +inf => 1,
    A clt.un B => 1,

    иначе => 0

    Семантика операций сравнения для чисел с плавающей запятой существенно отличается от их семантики для целых чисел. Дело в том, что числа с плавающей запятой могут дополнительно принимать значения +inf (положительная бесконечность), -inf (отрицательная бесконечность) и NaN (Not a Number - не число). Поэтому описание каждой инструкции содержит две части: для целых чисел и для чисел с плавающей запятой. При этом в описании используются следующие обозначения:

  • I и J - целые числа со знаком, причем I < J ;
  • K и L - целые числа без знака, причем K < L ;
  • A и B - конечные числа с плавающей запятой (то есть они не равны NaN, +inf и -inf ), причем A < B ;
  • C - любое число с плавающей запятой (может принимать значения NaN, +inf и -inf ).
  • Унарные арифметические операции

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

    ... , value -> ... , result
    Унарные арифметические операции
    Код Инструкция Встроенный операнд Описание
    0x65 neg - Изменение знака числа
    0x66 not - Побитовое НЕ (для целых чисел)

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

  • Результатом применения этой инструкции к наименьшему отрицательному целому числу (такое число не имеет "парного" положительного числа) является само это наименьшее отрицательное число. Для того чтобы иметь возможность перехватить эту ситуацию, необходимо вместо инструкции neg использовать sub.ovf.
  • Результатом применения этой инструкции к NaN является NaN.
  • Инструкция ckfinite

    Инструкция ckfinite (см. таблицу 3.14) генерирует исключение ArithmeticException, если число с плавающей запятой, находящееся на вершине стека вычислений, равно NaN, +inf или -inf. Если исключение не генерируется, то стек вычислений не меняется, поэтому диаграмма стека выглядит следующим образом:

    ... , value -> ... , value
    Инструкция ckfinite
    Код Инструкция Встроенный операнд Описание
    0xC3 ckfinite - Проверка того, что число с плавающей запятой является конечным

    Преобразование значений

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

    ... , value -> ... , result

    Базовые инструкции преобразования представлены в таблице 3.15. Они обладают следующими особенностями:

  • Преобразование чисел с плавающей запятой к целому типу обрезает дробную часть числа. Если при этом возникает переполнение, то возвращаемый результат неопределен (зависит от реализации).
  • Преобразование значения с плавающей запятой к типу float32 может вызывать потерю точности. Кроме того, если это значение слишком велико для float32, то результатом преобразования является +inf или -inf.
  • Инструкция conv.r.un интерпретирует целое значение, лежащее на вершине стека, как не имеющее знака и преобразует его к вещественному типу (либо float32, либо float64 в зависимости от значения).
  • Если переполнение возникает при преобразовании значения одного целого типа к другому целому типу, то обрезаются старшие биты значения.
  • Преобразование значений без контроля переполнений
    Код Инструкция Встроенный операнд Описание
    0x67 conv.i1 - Преобразовать к int8
    0x68 conv.i2 - Преобразовать к int16
    0x69 conv.i4 - Преобразовать к int32
    0x6A conv.i8 - Преобразовать к int64
    0x6B conv.r4 - Преобразовать к float32
    0x6C conv.r8 - Преобразовать к float64
    0x6D conv.u4 - Преобразовать к unsigned int32
    0x6E conv.u8 - Преобразовать к unsigned int64
    0x76 conv.r.un. - Преобразовать беззнаковое целое число в число с плавающей запятой
    0xD1 conv.u2 - Преобразовать к unsigned int16
    0xD2 conv.u1 - Преобразовать к unsigned int8
    0xD3 conv.i - Преобразовать к native int
    0xE0 conv.u - Преобразовать к unsigned native int

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

    Преобразования значений со знаком с контролем переполнения
    Код Инструкция Встроенный операнд Описание
    0xB3 conv.ovf.i1 - Преобразование к int8
    0xB4 conv.ovf.u1 - Преобразование к unsigned int8
    0xB5 conv.ovf.i2 - Преобразование к int16
    0xB6 conv.ovf.u2 - Преобразование к unsigned int16
    0xB7 conv.ovf.i4 - Преобразование к int32
    0xB8 conv.ovf.u4 - Преобразование к unsigned int32
    0xB9 conv.ovf.i8 - Преобразование к int64
    0xBA conv.ovf.u8 - Преобразование к unsigned int64
    0xD4 conv.ovf.i - Преобразование к native int
    0xD5 conv.ovf.u - Преобразование к unsigned native int

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

    Преобразование беззнаковых значений с контролем переполнения
    Код Инструкция Встроенный операнд Описание
    0x82 conv.ovf.i1.un - Преобразование к int8
    0x83 conv.ovf.i2.un - Преобразование к int16
    0x84 conv.ovf.i4.un - Преобразование к int32
    0x85 conv.ovf.i8.un - Преобразование к int64
    0x86 conv.ovf.u1.un - Преобразование к unsigned int8
    0x87 conv.ovf.u2.un - Преобразование к unsigned int16
    0x88 conv.ovf.u4.un - Преобразование к unsigned int32
    0x89 conv.ovf.u8.un - Преобразование к unsigned int64
    0x8A conv.ovf.i.un - Преобразование к native int
    0x8B conv.ovf.u.un - Преобразование к unsigned native int

    Инструкции для организации передачи управления

    Инструкции для организации передачи управления можно разделить на пять категорий:

  • инструкции безусловного перехода;
  • инструкции условного перехода;
  • инструкция множественного выбора switch;
  • инструкция вызова метода call;
  • инструкция возврата из метода ret.
  • Безусловный переход

    Существуют две инструкции безусловного перехода (см. таблицу 3.18), которые различаются только разрядностью встроенного операнда ( int8 и int32 ). При этом встроенный операнд этих инструкций обозначает относительное смещение цели перехода.

    Инструкции безусловного перехода
    Код Инструкция Встроенный операнд Описание
    0x2B br.s int8 Короткий безусловный переход
    0x38 br. int32 Длинный безусловный переход

    Условный переход

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

    ... , value -> ...
    Базовые инструкции условного перехода
    Код Инструкция Встроенный операнд Описание
    0x2C brfalse.s int8 Короткий условный переход, если значение равно 0 или null
    0x2D brtrue.s int8 Короткий условный переход, если значение не равно 0 или null
    0x39 brfalse int32 Длинный условный переход, если значение равно 0 или null
    0x3A brtrue int32 Длинный условный переход, если значение не равно 0 или null

    Как и в случае инструкций безусловного перехода, существуют короткий и длинный варианты инструкций условного перехода, которые отличаются только разрядностью встроенного операнда ( int8 и int32 ).

    Инструкции brfalse присвоены два псевдонима: brnull и brzero, имеющих одинаковый с ней код. Аналогично, brfalse.s имеет псевдонимы brnull.s и brzero.s.

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

    Таким образом, дополнительные инструкции условного перехода имеют следующую диаграмму стека:

    ... , value1 , value2 -> ...

    Длинные варианты дополнительных инструкций условного перехода перечислены в таблице 3.20, а короткие - в таблице 3.21. В описании каждой инструкции приводится эквивалентная ей комбинация бинарной операции сравнения и базовой инструкции условного перехода. В связи с тем, что семантика операций сравнения для целых чисел существенно отличается от их семантики для чисел с плавающей запятой, для инструкций bge, ble, bge.un и ble.un (и для коротких вариантов этих инструкций) приводится по две эквивалентные им комбинации. Комбинация, помеченная меткой ( int ), используется в случае целых операндов, а комбинация, помеченная меткой ( F ), используется в том случае, если операнды представляют собой числа с плавающей запятой.

    Дополнительные длинные инструкции условного перехода
    Код Инструкция Встроенный операнд Описание
    0x3B beq int32 ceq; brtrue
    0x3C bge int32
    (int): clt; brfalse
    (F) : clt.un; brfalse
    0x3D bgt int32 cgt; brtrue
    0x3E ble int32
    (int): cgt; brfalse
    (F) : cgt.un; brfalse
    0x3F blt int32 clt; brtrue
    0x40 bne.un int32 ceq; brfalse
    0x41 bge.un int32
    (int): clt.un; brfalse
    (F) : clt; brfalse
    0x42 bgt.un int32 cgt.un; brtrue
    0x43 ble.un int32
    (int): cgt.un; brfalse
    (F) : cgt; brfalse
    0x44 blt.un int32 clt.un; brtrue

    Дополнительные короткие инструкции условного перехода
    Код Инструкция Встроенный операнд Описание
    0x2E beq.s int8 ceq; brtrue.s
    0x2F bge.s int8
    (int): clt; brfalse.s
    (F) : clt.un; brfalse.s
    0x30 bgt.s int8 cgt; brtrue.s
    0x31 ble.s int8
    (int): cgt; brfalse.s
    (F) : cgt.un; brfalse.s
    0x32 blt.s int8 clt; brtrue.s
    0x33 bne.un.s int8 ceq; brfalse.s
    0x34 bge.un.s int8
    (int): clt.un; brfalse.s
    (F) : clt; brfalse.s
    0x35 bgt.un.s int8 cgt.un; brtrue.s
    0x36 ble.un.s int8
    (int): cgt.un; brfalse.s
    (F) : cgt; brfalse.s
    0x37 blt.un.s int8 clt.un; brtrue.s

    Инструкция switch

    Инструкция множественного выбора switch представлена в таблице 3.22. Встроенный операнд этой инструкции имеет сложный формат: первое 32-разрядное слово содержит размер N таблицы переходов, после чего следует N 32-рязрядных относительных смещений целей перехода.

    Инструкция switch берет со стека вычислений один операнд. Обозначим этот операнд через I. Значение I интерпретируется как целое число без знака и сравнивается с N. Если I < N, то управление передается на I-тую цель в таблице переходов (нумерация целей осуществляется с 0). Если I >= N, то управление передается на инструкцию, непосредственно следующую за инструкцией switch.

    Для инструкции switch можно записать следующую диаграмму стека:

    ... , value -> ...
    Инструкция switch
    Код Инструкция Встроенный операнд Описание
    0x45 switch
    unsigned
    int32,
    int32,...
    int32,
    Осуществляет переход по таблице переходов в соответствии со значением на вершине стека

    Инструкция call

    Инструкция call (см. таблицу 3.23) выполняет вызов метода, который указан во встроенном операнде инструкции. Встроенный операнд представляет собой токен метаданных, указывающий на описывающую вызываемый метод запись в таблицах метаданных. Непосредственно перед инструкцией call может стоять префикс .tail, который говорит о том, что состояние текущего метода должно быть освобождено перед передачей управления вызываемому методу (хвостовой вызов).

    Инструкция call
    Код Инструкция Встроенный операнд Описание
    0x28 call token Выполняет вызов метода

    Информация о методе, на которую указывает токен метаданных, позволяет JIT-компилятору определить, является ли вызываемый метод статическим, экземплярным, виртуальным или глобальной функцией. Особенностью инструкции call (по сравнению с инструкцией callvirt, которую мы будем рассматривать далее в этой главе) является то, что адрес вызываемого метода вычисляется статически, то есть еще во время JIT-компиляции.

    Параметры вызываемого метода должны быть расположены в стеке слева направо, то есть сначала на стек должен быть загружен первый параметр, затем второй и т.д. При вызове экземплярного метода в качестве первого параметра должна выступать объектная ссылка (параметр this ).

    Если вызываемый метод возвращает значение, то оно загружается на стек вызывающего метода.

    Инструкция ret

    Инструкция ret (см. таблицу 3.24) осуществляет возврат из метода. Если метод возвращает значение, то оно должно быть загружено на вершину стека вычислений.

    Инструкция ret
    Код Инструкция Встроенный операнд Описание
    0x2A ret - Осуществляет возврат из метода
    Страницы:

    Поток инструкций языка 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").

    Варианты встроенных операндов инструкций 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:

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

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

  • Инструкция 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-кода в код процессора.

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

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

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

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

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

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

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

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

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

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

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

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

  • Язык CIL: инструкции общего назначения

    В этом разделе мы рассмотрим ту часть инструкций языка CIL, которая служит для организации вычислений, а именно:

  • инструкции для загрузки и сохранения значений;
  • арифметические инструкции;
  • инструкции для организации передачи управления.
  • Инструкции для загрузки и сохранения значений

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

    Загрузка констант

    Эта группа инструкций (см. таблицу 3.2) служит для загрузки константных значений на стек вычислений. При этом значения кодируются в самих инструкциях в виде их кодов или встроенных операндов.

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

    ... -> ... , constant
    Инструкции для загрузки констант
    Код Инструкция Встроенный операнд Описание
    0x14 ldnull - Загрузка константы null
    0x15 ldc.m1 - Загрузка целого числа -1( int32 )
    0x16 - 0x1E ldc.0 - ldc.8 - Загрузка целых чисел от 0 до 8 ( int32 )
    0x1F ldc.s int8 Загрузка целых чисел от -128 до 127 ( int32 )
    0x20 ldc.i4 int32 Загрузка целых чисел ( int32 )
    0x21 ldc.i8 int64 Загрузка целых чисел ( int64 )
    0x22 ldc.r4 float32 Загрузка чисел с плавающей запятой ( F )
    0x23 ldc.r8 float64 Загрузка чисел с плавающей запятой ( F )

    Работа с переменными и параметрами методов

    Локальные переменные и параметры методов имеют номера от 0 до 65534. Существуют три варианта инструкций для работы с переменными и параметрами:

  • сокращенные инструкции, которые работают с переменными и параметрами, имеющими номера от 0 до 3;
  • сокращенные инструкции, допускающие номера переменных и параметров от 0 до 255;
  • обычные инструкции, работающие с любыми переменными и параметрами.
  • В таблице 3.3 перечислены инструкции, выполняющие загрузку значений переменных и параметров на стек вычислений. Все они имеют следующую диаграмму стека:

    ... -> ... , value
    Инструкции для загрузки параметров и локальных переменных
    Код Инструкция Встроенный операнд Описание
    0x02 - 0x05 ldarg.0 - ldarg.3 - Загрузка параметров с номерами от 0 до 3
    0x06 - 0x09 ldloc.0 - ldloc.3 - Загрузка локальных переменных с номерами от 0 до 3
    0x0E ldarg.s unsigned int8 Загрузка параметров с номерами от 0 до 255
    0x11 ldloc.s unsigned int8 Загрузка локальных переменных с номерами от 0 до 255
    0xFE 0x09 ldarg unsigned int16 Загрузка параметров с номерами от 0 до 65534
    0xFE 0x0C ldloc unsigned int16 Загрузка локальных переменных от 0 до 65534

    Кроме инструкций, загружающих значения переменных и параметров, существуют инструкции, загружающие на вершину стека вычислений адреса переменных и параметров (см. таблицу 3.4). Загружаемые адреса имеют тип управляемых указателей. Диаграмма стека для этих инструкций выглядит следующим образом:

    ... -> ... , address
    Инструкции для загрузки адресов параметров и локальных переменных
    Код Инструкция Встроенный операнд Описание
    0x0F ldarga.s unsigned int8 Загрузка адресовпараметров с номерами от 0 до 255
    0x12 ldloca.s unsigned int8 Загрузка адресов локальных переменных с номерами от 0 до 255
    0xFE 0x0A ldarga unsigned int16 Загрузка адресов параметров с номерами от 0 до 65534
    0xFE 0x0D ldloca unsigned int16 Загрузка адресов локальных переменных с номерами от 0 до 65534

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

    ... , value -> ...
    Инструкции для сохранения значений в параметрах и локальных переменных
    Код Инструкция Встроенный операнд Описание
    0x0A stloc.0 - stloc.3 - Сохранение значений в локальных переменных с номерами от 0 до 3
    0x10 starg.s unsigned int8 Сохранение значений в параметрах с номерами от 0 до 255
    0x13 stloc.s unsigned int8 Сохранение значений в локальных переменных с номерами от 0 до 255
    0xFE 0x0B starg unsigned int16 Сохранение значений в параметрах с номерами от 0 до 65534
    0xFE 0x0E stloc unsigned int16 Сохранение значений в локальных переменных с номерами от 0 до 65534

    Косвенная загрузка и сохранение значений

    При косвенной загрузке и сохранении значений работа с памятью осуществляется через адреса (управляемые и неуправляемые указатели).

    Особенностью инструкций данной группы является наличие разных инструкций для работы со значениями разных типов. Причина в том, что при загрузке или сохранении значения бывает необходимо выполнить его преобразование к другому типу, а так как JIT-компилятор в процессе компиляции не собирает информацию о типах управляемых указателей, ему надо явно указывать тип загружаемых и сохраняемых значений. Необходимость выполнения преобразований объясняется тем, что не все примитивные типы могут находиться на стеке вычислений (поэтому, например, значение типа int8 при загрузке на стек расширяется до int32 ).

    В таблице 3.6 перечислены инструкции для косвенной загрузки значений. Обратите внимание, что инструкции ldind.i8 и ldind.u8 являются псевдонимами (имеют один и тот же код). Дело в том, что загрузка любых 64-разрядных целых значений на стек не вызывает их преобразования, ибо хотя на стеке не предусмотрено наличие беззнаковых 64-разрядных значений, их загрузка все равно сводится к простому побитовому копированию. Вышесказанное справедливо также и для 32-разрядных целых значений, но для их загрузки зачем-то зарезервировано сразу две инструкции.

    Инструкции для косвенной загрузки значений
    Код Инструкция Встроенный операнд Описание
    0x46 ldind.i1 - Косвенная загрузка значения int8
    0x47 ldind.u1 - Косвенная загрузка значения unsigned int8
    0x48 ldind.i2 - Косвенная загрузка значения int16
    0x49 ldind.u2 - Косвенная загрузка значения unsigned int16
    0x4A ldind.i4 - Косвенная загрузка значения int32
    0x4B ldind.u4 - Косвенная загрузка значения unsigned int32
    0x4C ldind.i8(ldind.u8) - Косвенная загрузка значения int64 и unsigned int64
    0x4D ldind.i - Косвенная загрузка значения native int
    0x4E ldind.r4 - Косвенная загрузка значения float32
    0x4 ldind.r8 - Косвенная загрузка значения float64
    0x50 ldind.ref - Косвенная загрузка объектной ссылки

    Диаграмма стека для инструкций косвенной загрузки выглядит следующим образом:

    ... , address -> ... , value

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

    Инструкции для косвенного сохранения значений
    Код Инструкция Встроенный операнд Описание
    0x51 stind.ref - Косвенное сохранение объектной ссылки
    0x52 stind.i1 - Косвенное сохранение значения int8
    0x53 stind.i2 - Косвенное сохранение значения int16
    0x54 stind.i4 - Косвенное сохранение значения int32
    0x55 stind.i8 - Косвенное сохранение значения int64
    0x56 stind.r4 - Косвенное сохранение значения float32
    0x57 stind.r8 - Косвенное сохранение значения float64
    0xDF stind.i - Косвенное сохранение значения native int

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

    ... , address , value -> ...

    Специальные инструкции для работы со стеком

    В отличие от "железных" стековых процессоров, CLI не содержит развитой системы инструкций для чисто стековых манипуляций. В таблице 3.8 представлены две имеющиеся в наличии инструкции.

    Специальные инструкции для работы со стеком
    Код Инструкция Встроенный операнд Описание
    0x25 dup - Копирование значения на вершине стека: ..., value ->..., value, value
    0x26 pop - Удаление значения с вершины стека . .., value ->...

    Арифметические инструкции

    Арифметические инструкции можно разделить на четыре категории:

  • бинарные операции;
  • унарные операции;
  • инструкция ckfinite, проверяющая конечность значений с плавающей точкой;
  • инструкции преобразования значений.
  • Бинарные арифметические операции

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

    ... , value1 , value2 -> ... , result

    Действие бинарных операций можно записать как

    result := value1 op value2,

    то есть например, если op соответствует операции вычитания, то из value1 вычитается value2.

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

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

    В таблице 3.9 представлены базовые инструкции, выполняющие бинарные операции.

    Базовые бинарные арифметические операции
    Код Инструкция Встроенный операнд Описание
    0x58 add - Сложение
    0x59 sub - Вычитание
    0x5A mul - Умножение
    0x5B div - Деление
    0x5C div.un - Деление беззнаковых целых чисел
    0x5D rem - Остаток от деления
    0x5E rem.un - Остаток от деления беззнаковых целых чисел
    0x5F and - Побитовое И
    0x60 or - Побитовое ИЛИ
    0x61 xor - Побитовое ИСКЛЮЧАЮЩЕЕ ИЛИ

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

    Бинарные арифметические операции с контролем переполнения
    Код Инструкция Встроенный операнд Описание
    0xD6 add.ovf - Сложение целых чисел со знаком с контролем переполнения
    0xD7 add.ovf.un - Сложение целых чисел без знака с контролем переполнения
    0xD mul.ovf - Умножение целых чисел со знаком с контролем переполнения
    0xD mul.ovf.un - Умножение целых чисел без знака с контролем переполнения
    0xD sub.ovf - Вычитание целых чисел со знаком с контролем переполнения
    0xD sub.ovf.un - Вычитание целых чисел без знака с контролем переполнения

    Операции сдвига (см. таблицу 3.11) выполняют сдвиг значения первого операнда ( value1 ) в нужную сторону на количество бит, указанное во втором операнде ( value2 ).

    Операции сдвига
    Код Инструкция Встроенный операнд Описание
    0x62 shl - Сдвиг целых чисел влево
    0x63 shr - Сдвиг целых чисел со знаком вправо
    0x64 shr.un - Сдвиг целых чисел без знака вправо

    Операции, приведенные в таблице 3.12, выполняют сравнение значений своих операндов. Результатом сравнения являются числа 0 или 1 (типа int32 ). Число 0 обозначает ложь, а число 1 - истину.

    Операции сравнения
    Код Инструкция Встроенный операнд Описание
    0xFE 0x01 ceq -

    Сравнение на равенство.

    Для целых чисел:

    I ceq I => 1,

    иначе => 0

    Для чисел с плавающей запятой:

    +inf ceq +inf => 1,
    -inf ceq -inf => 1,
    A ceq A => 1,

    иначе => 0

    0xFE 0x02 cgt -

    Сравнение на "больше".

    Для целых чисел:

    J cgt I => 1,

    иначе => 0

    Для чисел с плавающей запятой:

    A cgt -inf => 1,
    +inf cgt A => 1,
    +inf cgt -inf => 1,
    B cgt A => 1,

    иначе => 0

    0xFE 0x04 clt - Сравнение на "меньше".

    Для целых чисел:

    I clt J => 1,

    иначе => 0

    Для чисел с плавающей запятой:

    A clt +inf => 1,
    -inf clt A => 1,
    -inf clt +inf => 1,
    A clt B => 1,

    иначе => 0

    0xFE 0x03 cgt.un -

    Сравнение на "больше" беззнаковых целых чисел или неупорядоченных чисел с плавающей запятой. (Два числа с плавающей запятой называются неупорядоченными, если хотя бы одно из них равно NaN.)

    Для целых чисел:

    L cgt.un K => 1,

    иначе => 0

    Для чисел с плавающей запятой:

    NaN cgt.un C => 1,
    C cgt.un NaN => 1,
    A cgt.un -inf => 1,
    +inf cgt.un A => 1,
    +inf cgt.un -inf => 1,
    B cgt.un A => 1,

    иначе => 0

    0xFE 0x05 clt.un -

    Сравнение на "меньше" беззнаковых целых чисел или неупорядоченных чисел с плавающей запятой.

    Для целых чисел:

    K clt.un L => 1,

    иначе => 0

    Для чисел с плавающей запятой:

    NaN clt.un C => 1,
    C clt.un NaN => 1,
    A clt.un +inf => 1,
    -inf clt.un A => 1,
    -inf clt.un +inf => 1,
    A clt.un B => 1,

    иначе => 0

    Семантика операций сравнения для чисел с плавающей запятой существенно отличается от их семантики для целых чисел. Дело в том, что числа с плавающей запятой могут дополнительно принимать значения +inf (положительная бесконечность), -inf (отрицательная бесконечность) и NaN (Not a Number - не число). Поэтому описание каждой инструкции содержит две части: для целых чисел и для чисел с плавающей запятой. При этом в описании используются следующие обозначения:

  • I и J - целые числа со знаком, причем I < J ;
  • K и L - целые числа без знака, причем K < L ;
  • A и B - конечные числа с плавающей запятой (то есть они не равны NaN, +inf и -inf ), причем A < B ;
  • C - любое число с плавающей запятой (может принимать значения NaN, +inf и -inf ).
  • Унарные арифметические операции

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

    ... , value -> ... , result
    Унарные арифметические операции
    Код Инструкция Встроенный операнд Описание
    0x65 neg - Изменение знака числа
    0x66 not - Побитовое НЕ (для целых чисел)

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

  • Результатом применения этой инструкции к наименьшему отрицательному целому числу (такое число не имеет "парного" положительного числа) является само это наименьшее отрицательное число. Для того чтобы иметь возможность перехватить эту ситуацию, необходимо вместо инструкции neg использовать sub.ovf.
  • Результатом применения этой инструкции к NaN является NaN.
  • Инструкция ckfinite

    Инструкция ckfinite (см. таблицу 3.14) генерирует исключение ArithmeticException, если число с плавающей запятой, находящееся на вершине стека вычислений, равно NaN, +inf или -inf. Если исключение не генерируется, то стек вычислений не меняется, поэтому диаграмма стека выглядит следующим образом:

    ... , value -> ... , value
    Инструкция ckfinite
    Код Инструкция Встроенный операнд Описание
    0xC3 ckfinite - Проверка того, что число с плавающей запятой является конечным

    Преобразование значений

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

    ... , value -> ... , result

    Базовые инструкции преобразования представлены в таблице 3.15. Они обладают следующими особенностями:

  • Преобразование чисел с плавающей запятой к целому типу обрезает дробную часть числа. Если при этом возникает переполнение, то возвращаемый результат неопределен (зависит от реализации).
  • Преобразование значения с плавающей запятой к типу float32 может вызывать потерю точности. Кроме того, если это значение слишком велико для float32, то результатом преобразования является +inf или -inf.
  • Инструкция conv.r.un интерпретирует целое значение, лежащее на вершине стека, как не имеющее знака и преобразует его к вещественному типу (либо float32, либо float64 в зависимости от значения).
  • Если переполнение возникает при преобразовании значения одного целого типа к другому целому типу, то обрезаются старшие биты значения.
  • Преобразование значений без контроля переполнений
    Код Инструкция Встроенный операнд Описание
    0x67 conv.i1 - Преобразовать к int8
    0x68 conv.i2 - Преобразовать к int16
    0x69 conv.i4 - Преобразовать к int32
    0x6A conv.i8 - Преобразовать к int64
    0x6B conv.r4 - Преобразовать к float32
    0x6C conv.r8 - Преобразовать к float64
    0x6D conv.u4 - Преобразовать к unsigned int32
    0x6E conv.u8 - Преобразовать к unsigned int64
    0x76 conv.r.un. - Преобразовать беззнаковое целое число в число с плавающей запятой
    0xD1 conv.u2 - Преобразовать к unsigned int16
    0xD2 conv.u1 - Преобразовать к unsigned int8
    0xD3 conv.i - Преобразовать к native int
    0xE0 conv.u - Преобразовать к unsigned native int

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

    Преобразования значений со знаком с контролем переполнения
    Код Инструкция Встроенный операнд Описание
    0xB3 conv.ovf.i1 - Преобразование к int8
    0xB4 conv.ovf.u1 - Преобразование к unsigned int8
    0xB5 conv.ovf.i2 - Преобразование к int16
    0xB6 conv.ovf.u2 - Преобразование к unsigned int16
    0xB7 conv.ovf.i4 - Преобразование к int32
    0xB8 conv.ovf.u4 - Преобразование к unsigned int32
    0xB9 conv.ovf.i8 - Преобразование к int64
    0xBA conv.ovf.u8 - Преобразование к unsigned int64
    0xD4 conv.ovf.i - Преобразование к native int
    0xD5 conv.ovf.u - Преобразование к unsigned native int

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

    Преобразование беззнаковых значений с контролем переполнения
    Код Инструкция Встроенный операнд Описание
    0x82 conv.ovf.i1.un - Преобразование к int8
    0x83 conv.ovf.i2.un - Преобразование к int16
    0x84 conv.ovf.i4.un - Преобразование к int32
    0x85 conv.ovf.i8.un - Преобразование к int64
    0x86 conv.ovf.u1.un - Преобразование к unsigned int8
    0x87 conv.ovf.u2.un - Преобразование к unsigned int16
    0x88 conv.ovf.u4.un - Преобразование к unsigned int32
    0x89 conv.ovf.u8.un - Преобразование к unsigned int64
    0x8A conv.ovf.i.un - Преобразование к native int
    0x8B conv.ovf.u.un - Преобразование к unsigned native int

    Инструкции для организации передачи управления

    Инструкции для организации передачи управления можно разделить на пять категорий:

  • инструкции безусловного перехода;
  • инструкции условного перехода;
  • инструкция множественного выбора switch;
  • инструкция вызова метода call;
  • инструкция возврата из метода ret.
  • Безусловный переход

    Существуют две инструкции безусловного перехода (см. таблицу 3.18), которые различаются только разрядностью встроенного операнда ( int8 и int32 ). При этом встроенный операнд этих инструкций обозначает относительное смещение цели перехода.

    Инструкции безусловного перехода
    Код Инструкция Встроенный операнд Описание
    0x2B br.s int8 Короткий безусловный переход
    0x38 br. int32 Длинный безусловный переход

    Условный переход

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

    ... , value -> ...
    Базовые инструкции условного перехода
    Код Инструкция Встроенный операнд Описание
    0x2C brfalse.s int8 Короткий условный переход, если значение равно 0 или null
    0x2D brtrue.s int8 Короткий условный переход, если значение не равно 0 или null
    0x39 brfalse int32 Длинный условный переход, если значение равно 0 или null
    0x3A brtrue int32 Длинный условный переход, если значение не равно 0 или null

    Как и в случае инструкций безусловного перехода, существуют короткий и длинный варианты инструкций условного перехода, которые отличаются только разрядностью встроенного операнда ( int8 и int32 ).

    Инструкции brfalse присвоены два псевдонима: brnull и brzero, имеющих одинаковый с ней код. Аналогично, brfalse.s имеет псевдонимы brnull.s и brzero.s.

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

    Таким образом, дополнительные инструкции условного перехода имеют следующую диаграмму стека:

    ... , value1 , value2 -> ...

    Длинные варианты дополнительных инструкций условного перехода перечислены в таблице 3.20, а короткие - в таблице 3.21. В описании каждой инструкции приводится эквивалентная ей комбинация бинарной операции сравнения и базовой инструкции условного перехода. В связи с тем, что семантика операций сравнения для целых чисел существенно отличается от их семантики для чисел с плавающей запятой, для инструкций bge, ble, bge.un и ble.un (и для коротких вариантов этих инструкций) приводится по две эквивалентные им комбинации. Комбинация, помеченная меткой ( int ), используется в случае целых операндов, а комбинация, помеченная меткой ( F ), используется в том случае, если операнды представляют собой числа с плавающей запятой.

    Дополнительные длинные инструкции условного перехода
    Код Инструкция Встроенный операнд Описание
    0x3B beq int32 ceq; brtrue
    0x3C bge int32
    (int): clt; brfalse
    (F) : clt.un; brfalse
    0x3D bgt int32 cgt; brtrue
    0x3E ble int32
    (int): cgt; brfalse
    (F) : cgt.un; brfalse
    0x3F blt int32 clt; brtrue
    0x40 bne.un int32 ceq; brfalse
    0x41 bge.un int32
    (int): clt.un; brfalse
    (F) : clt; brfalse
    0x42 bgt.un int32 cgt.un; brtrue
    0x43 ble.un int32
    (int): cgt.un; brfalse
    (F) : cgt; brfalse
    0x44 blt.un int32 clt.un; brtrue

    Дополнительные короткие инструкции условного перехода
    Код Инструкция Встроенный операнд Описание
    0x2E beq.s int8 ceq; brtrue.s
    0x2F bge.s int8
    (int): clt; brfalse.s
    (F) : clt.un; brfalse.s
    0x30 bgt.s int8 cgt; brtrue.s
    0x31 ble.s int8
    (int): cgt; brfalse.s
    (F) : cgt.un; brfalse.s
    0x32 blt.s int8 clt; brtrue.s
    0x33 bne.un.s int8 ceq; brfalse.s
    0x34 bge.un.s int8
    (int): clt.un; brfalse.s
    (F) : clt; brfalse.s
    0x35 bgt.un.s int8 cgt.un; brtrue.s
    0x36 ble.un.s int8
    (int): cgt.un; brfalse.s
    (F) : cgt; brfalse.s
    0x37 blt.un.s int8 clt.un; brtrue.s

    Инструкция switch

    Инструкция множественного выбора switch представлена в таблице 3.22. Встроенный операнд этой инструкции имеет сложный формат: первое 32-разрядное слово содержит размер N таблицы переходов, после чего следует N 32-рязрядных относительных смещений целей перехода.

    Инструкция switch берет со стека вычислений один операнд. Обозначим этот операнд через I. Значение I интерпретируется как целое число без знака и сравнивается с N. Если I < N, то управление передается на I-тую цель в таблице переходов (нумерация целей осуществляется с 0). Если I >= N, то управление передается на инструкцию, непосредственно следующую за инструкцией switch.

    Для инструкции switch можно записать следующую диаграмму стека:

    ... , value -> ...
    Инструкция switch
    Код Инструкция Встроенный операнд Описание
    0x45 switch
    unsigned
    int32,
    int32,...
    int32,
    Осуществляет переход по таблице переходов в соответствии со значением на вершине стека

    Инструкция call

    Инструкция call (см. таблицу 3.23) выполняет вызов метода, который указан во встроенном операнде инструкции. Встроенный операнд представляет собой токен метаданных, указывающий на описывающую вызываемый метод запись в таблицах метаданных. Непосредственно перед инструкцией call может стоять префикс .tail, который говорит о том, что состояние текущего метода должно быть освобождено перед передачей управления вызываемому методу (хвостовой вызов).

    Инструкция call
    Код Инструкция Встроенный операнд Описание
    0x28 call token Выполняет вызов метода

    Информация о методе, на которую указывает токен метаданных, позволяет JIT-компилятору определить, является ли вызываемый метод статическим, экземплярным, виртуальным или глобальной функцией. Особенностью инструкции call (по сравнению с инструкцией callvirt, которую мы будем рассматривать далее в этой главе) является то, что адрес вызываемого метода вычисляется статически, то есть еще во время JIT-компиляции.

    Параметры вызываемого метода должны быть расположены в стеке слева направо, то есть сначала на стек должен быть загружен первый параметр, затем второй и т.д. При вызове экземплярного метода в качестве первого параметра должна выступать объектная ссылка (параметр this ).

    Если вызываемый метод возвращает значение, то оно загружается на стек вызывающего метода.

    Инструкция ret

    Инструкция ret (см. таблицу 3.24) осуществляет возврат из метода. Если метод возвращает значение, то оно должно быть загружено на вершину стека вычислений.

    Инструкция ret
    Код Инструкция Встроенный операнд Описание
    0x2A ret - Осуществляет возврат из метода
    Вернуться к учебному плану