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

Common Intermediate Language

< Лекция 4 || Лекция 5: 123 || Лекция 6 >

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

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

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

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

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

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

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

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

... -> ... , constant
Таблица 3.2. Инструкции для загрузки констант
Код Инструкция Встроенный операнд Описание
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
Таблица 3.3. Инструкции для загрузки параметров и локальных переменных
Код Инструкция Встроенный операнд Описание
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
Таблица 3.4. Инструкции для загрузки адресов параметров и локальных переменных
Код Инструкция Встроенный операнд Описание
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 -> ...
Таблица 3.5. Инструкции для сохранения значений в параметрах и локальных переменных
Код Инструкция Встроенный операнд Описание
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-разрядных целых значений, но для их загрузки зачем-то зарезервировано сразу две инструкции.

Таблица 3.6. Инструкции для косвенной загрузки значений
Код Инструкция Встроенный операнд Описание
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) меньше, чем инструкций для косвенной загрузки (можно заметить, что инструкции для сохранения значений беззнаковых целых типов отсутствуют). Причина в том, что сохранение беззнаковых целых ничем не отличается от сохранения знаковых целых.

Таблица 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 представлены две имеющиеся в наличии инструкции.

Таблица 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.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 ).

Таблица 3.10. Бинарные арифметические операции с контролем переполнения
Код Инструкция Встроенный операнд Описание
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 ).

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

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

Таблица 3.12. Операции сравнения
Код Инструкция Встроенный операнд Описание
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
Таблица 3.13. Унарные арифметические операции
Код Инструкция Встроенный операнд Описание
0x65 neg - Изменение знака числа
0x66 not - Побитовое НЕ (для целых чисел)

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

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

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

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

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

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

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

  • Преобразование чисел с плавающей запятой к целому типу обрезает дробную часть числа. Если при этом возникает переполнение, то возвращаемый результат неопределен (зависит от реализации).
  • Преобразование значения с плавающей запятой к типу float32 может вызывать потерю точности. Кроме того, если это значение слишком велико для float32, то результатом преобразования является +inf или -inf.
  • Инструкция conv.r.un интерпретирует целое значение, лежащее на вершине стека, как не имеющее знака и преобразует его к вещественному типу (либо float32, либо float64 в зависимости от значения).
  • Если переполнение возникает при преобразовании значения одного целого типа к другому целому типу, то обрезаются старшие биты значения.
Таблица 3.15. Преобразование значений без контроля переполнений
Код Инструкция Встроенный операнд Описание
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.

Таблица 3.16. Преобразования значений со знаком с контролем переполнения
Код Инструкция Встроенный операнд Описание
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 в случае переполнения.

Таблица 3.17. Преобразование беззнаковых значений с контролем переполнения
Код Инструкция Встроенный операнд Описание
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
< Лекция 4 || Лекция 5: 123 || Лекция 6 >
Анастасия Булинкова
Анастасия Булинкова
Рабочим названием платформы .NET было
Bogdan Drumov
Bogdan Drumov
Молдова, Республика
Azamat Nurmanbetov
Azamat Nurmanbetov
Киргизия, Bishkek