Рабочим названием платформы .NET было |
Common Intermediate Language
Язык CIL: инструкции общего назначения
В этом разделе мы рассмотрим ту часть инструкций языка CIL, которая служит для организации вычислений, а именно:
- инструкции для загрузки и сохранения значений;
- арифметические инструкции;
- инструкции для организации передачи управления.
Инструкции для загрузки и сохранения значений
Инструкции для загрузки и сохранения значений предназначены главным образом для обмена значениями между стеком вычислений и памятью, то есть они выполняют копирование значений на стек вычислений и сохранение значений со стека вычислений в память.
Загрузка констант
Эта группа инструкций (см. таблицу 3.2) служит для загрузки константных значений на стек вычислений. При этом значения кодируются в самих инструкциях в виде их кодов или встроенных операндов.
Диаграмма стека для всех инструкций этой группы выглядит следующим образом:
... -> ... , constant
Работа с переменными и параметрами методов
Локальные переменные и параметры методов имеют номера от 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 представлены базовые инструкции, выполняющие бинарные операции.
Инструкции, представленные в таблице 3.10, используются только для целочисленных операндов. Они отличаются от базовых бинарных операций тем, что осуществляют контроль переполнения (при переполнении генерируется исключение OverflowException ).
Операции сдвига (см. таблицу 3.11) выполняют сдвиг значения первого операнда ( value1 ) в нужную сторону на количество бит, указанное во втором операнде ( value2 ).
Операции, приведенные в таблице 3.12, выполняют сравнение значений своих операндов. Результатом сравнения являются числа 0 или 1 (типа int32 ). Число 0 обозначает ложь, а число 1 - истину.
Семантика операций сравнения для чисел с плавающей запятой существенно отличается от их семантики для целых чисел. Дело в том, что числа с плавающей запятой могут дополнительно принимать значения +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
Код | Инструкция | Встроенный операнд | Описание |
---|---|---|---|
0xC3 | ckfinite | - | Проверка того, что число с плавающей запятой является конечным |
Преобразование значений
Инструкции преобразования значений потребляют один операнд со стека вычислений и преобразуют его к нужному типу. Диаграмма стека для этих инструкций выглядит следующим образом:
... , value -> ... , result
Базовые инструкции преобразования представлены в таблице 3.15. Они обладают следующими особенностями:
- Преобразование чисел с плавающей запятой к целому типу обрезает дробную часть числа. Если при этом возникает переполнение, то возвращаемый результат неопределен (зависит от реализации).
- Преобразование значения с плавающей запятой к типу float32 может вызывать потерю точности. Кроме того, если это значение слишком велико для float32, то результатом преобразования является +inf или -inf.
- Инструкция conv.r.un интерпретирует целое значение, лежащее на вершине стека, как не имеющее знака и преобразует его к вещественному типу (либо float32, либо float64 в зависимости от значения).
- Если переполнение возникает при преобразовании значения одного целого типа к другому целому типу, то обрезаются старшие биты значения.
В таблице 3.16 приведены инструкции для преобразования значений, имеющих знак, к целым типам с контролем переполнения. В случае возникновения переполнения эти инструкции генерируют исключение OverflowException.
Инструкции, представленные в таблице 3.17, используются для преобразования беззнаковых значений к нужному типу и генерируют исключение OverflowException в случае переполнения.