Оглавление
§ Схема инструкции
Пожалуй, это самая интересная и достаточно сложная тема, которая касается разбора операндов (обычно, двух).
Общую схему для одной инструкции можно представить так:
1..4 | 1..2 | 1 | 1 | 1..4 | 1..4
Префиксы | Опкод | ModRM | SIB | Смещение | Непосредственное значение
Значение каждого из компонент я буду разбирать очень подробно далее в последующих материалах, однако стоит заметить, что размер одной инструкции может достигать до 16 байт.
- Префиксы: находятся перед кодом инструкции и меняют поведение инструкции. Есть префиксы, которые выбирают рабочий сегмент, метод адресации, размер операнда, необходимость повторов для строковых инструкции. Эти коды идут только перед самим опкодом. Теоретически префиксов можно установить сколько угодно, но процессор при достижении размера инструкции свыше 16 байт выдаст ошибку (выбросит исключение недопустимой инструкции) для предотвращения злоупотреблений, да и потому что инструкция просто не влазит в кеш инструкции
- Опкод: самая важная часть инструкции. Слово "опкод" образовано от двух слов "оп"ерация и "код", то есть, код операции. Опкод это непосредственно сама команда, номер команды, которая должна быть исполнена.
- ModRM / SIB: это два байта, которые определенным образом кодируют операнды, то есть те значения, которые должна прочитать инструкция из памяти или из регистров для исполнения. Эти модификаторы используются только с определенными опкодами. Существуют опкоды, которые кодируют очень простые инструкции, такие как например
CLI или LAHF, и таким инструкциям не нужны указания операндов, потому что они их уже содержат в самом номере инструкции. То как работают эти два модификатора (modrm,sib) – уже отдельная глава.
- Смещение: используется как дополнение для
modrm+sib при вычислениях смещения адреса в памяти. Смещение может быть и 1, и 2 и 4 байтным. Это зависит от того, что указано в модификаторах.
- Непосредственное значение: некоторые опкоды помимо двух операндов, которые кодируются через
modrm+sib, могут потребовать еще и третьего операнда, которым будет являться некоторая константа, идущая сразу же за модификаторами операндов. Непосредственное значение идет всегда в конце инструкции.
Минимальное количество байт, которые тратятся на инструкцию, равно 1, а максимальное 16. Это некоторые ограничения. Префиксы могут идти перед опкодом и в целом, их можно применить почти ко всем опкодам, а вот операнды указываются в строгом соответствии с тем, какой именно опкод будет.
Допустим, нам нужно сложить 2 регистра. Для этого выбираем опкод 00 (сложение 8-битных операндов), после опкода пусть будет значение C0, что означает выбор регистра AL слева и регистра AL справа, тем самым получая следующее:
00 C0 | ADD AL, AL
02 C0 | ADD AL, AL
Слева записан машинный код в виде 2х байт, идущих друг за другом, а справа – человеко-читаемая расшифровка, которая называется языком ассемблера, в данном случае это диалект Intel Assembler. Если машинный код всегда одинаковый, то вот ассемблер для этого кода может выглядеть совершенно по-разному.
Как можно заметить из примера, одну и ту же инструкцию, которая делает абсолютно одинаковое действие, можно записать разными опкодами (00 или 02). Понимание того, почему это так происходит, придет только после тщательного изучения того, как именно байт ModRM (+SIB если требуется) кодирует операнды. Сейчас это можно просто рассматривать как любопытный факт.
§ Кодирование операндов
В 386-м процессоре операнды кодируются двумя способами:
- Устаревший 16-битный метод с помощью только ModRM-байт
- Новый 32-х битный метод при помощи ModRM в связке с SIB
Оба этих метода работают и не конфликтуют друг с другом, но способы формирования адреса и регистров сильно отличаются. Я начну разговор именно о кодировании 16-битного операнда, а 32-х битный метод оставим на следующую главу.
Как и говорилось выше, некоторые опкоды требуют наличия байта, который будет указывать на то, какие операнды использовать. Тот операнд, который находится слева – он называется операндом-назначением (destination), а справа – операндом-источником (source). Арифметические инструкции работают слева направо, к примеру:
Здесь операнд-источник – это DX, а операнд-назначение – AX. Инструкция вычитает из AX значение DX и записывает результат обратно в AX. То, какие именно операнды и откуда их взять, как раз и определяет байт ModRM. Однако его структура такова, что можно использовать только 3 метода:
- Регистр, Регистр – пример:
ADD AX, BX
- Регистр, [Память] – пример:
SUB AL, BYTE [1234h]
- [Память], Регистр – пример:
XOR DWORD [BX], EBX
Здесь видно, что невозможно указать одновременно указатель на память в качестве операнда. Кстати говоря, в одном из примеров я указал в качестве операнда 32-х битное значение. В 386-м можно указывать с помощью двух префиксов 66h (размер операнда) и 67h (размер адреса) то, как именно будут кодироваться размер операнда (16 или 32 бита), и размер адреса (16-битный или 32-х битный указатель).
Это значит, что перед опкодом можно установить два префикса расширения операнда или адреса, причем в любом порядке и в любой комбинации. Без использования этих двух префиксов операнды и адрес будут строго 16-битными. В этом и есть суть обратной совместимости: любой код от старых программ легко запустится на 32-х битном процессоре.
Также, в качестве операнда используется указатель на память, который заключается в [квадратные скобки]. Что значит указатель? Это определенный адрес, который либо указывается прямо так, в виде константы (пример [1234h] – прямая адресация), либо в виде некоторого регистра [BX], например. Такой способ задания адреса называется косвенной адресацией. Когда процессор узнал адрес – вычислил его или получил константу, то он обращается к памяти и читает оттуда 1, 2 или 4 байта, в соответствии с размером операнда и использует далее в вычислениях. Если в качестве операнда-назначения указана память, то после завершения обработки инструкции результат записывается обратно в память.
§ Структура 16-битного ModRM
После прочтения байта, процессор использует 3 битовых поля, которые располагаются по следующей схеме:
+------+------+------+
| MOD | REG | MEM | ТИП
+------+------+------+
| 7..6 | 5..3 | 2..0 | БИТЫ
+------+------+------+
Здесь MOD – двухбитовое поле, указывает стратегию использования поля MEM:
00 – эффективный MEM декодируется обычно через сумму регистров (или просто указания регистр), либо же вместо него используется 16-битная константа
01 – к MEM добавляется 8 битное знаковое смещение (DISP)
10 – к MEM добавляется 16 битное знаковое смещение (DISP)
11 – вместо MEM указывается один из 8 регистров общего назначения (8/16/32-битные).
Здесь MEM – это именно указатель на память. Но может возникнуть вопрос: если структура полей всегда одинакова, то каким образом мы узнаем, где будет располагаться указатель на память – слева или справа? Ответ на этот вопрос простой: то, где именно будет располагаться указатель на память (или регистр вместо памяти), указано в коде операции (опкод). В том числе в опкоде указано также не только расположение слева или справа, а также то, с каким размером операнда мы работаем. Это может быть только 2 типа: 8 или 16/32 бита.
Почему именно 16 или 32? Дело в том, что когда опкод явно указывает что мы работаем с 8-битными операндам, то префикс 66h (расширение операнда до 32 бит) не сработает. Он срабатывает тогда и только тогда, когда опкод явно указывает что мы работаем с 16-битными числами. В этом случае префикс меняет 16 бит на 32 битный размер операнда и работает с ним.
Приведу примеры.
PX OP MRM MNEMONICS
00 00 | ADD [BX+SI], AL
01 C4 | ADD SP, AX
66 02 08 | ADD CL, [BX+SI]
66 03 A0 | ADD ESP, [BX+SI]
- Из первого примера с опкодом
00 известно, что если используется операнд, который указывает на память, то он будет располагаться слева, а операнд-регистр располагается справа.
- Во втором примере (
01) операнд, который указывает на память, тоже находится слева, но вместо него указан регистр (так как MOD=11), а также используется 16-битный размер операнда
- В третьем примере (
02) операнд, указывающий на память, теперь располагается справа, и, стоит отдельно заметить, префикс 66h никак не меняет размер операнда, потому что 8-битные операнды не расширяются до 32х битных
- И в последнем примере (
03) используется 16-битный операнд, который уже расширяется с SP до ESP, соответственно, справа находится указатель на 32-х битный операнд в памяти (там используется косвенный метод адресации, которая считывается путем складывания двух регистров BX + SI).
§ Поле REG
Из названия поля известно, что оно может указывать только на регистр. Однако, это не всегда так. Некоторые опкоды используют поле REG как номер функции, а не только как указание регистра. Пример такой групповой инструкции, как ее называют, будет опкод под номером 80h, который лишь указывает что:
- В качестве левого операнда используется указатель на память или регистр, поле
MEM в сочетании с MOD-полем
- В качестве правого операнда выступает 8-битная константа (непосредственное значение), которая находится в конце инструкции
- В поле
REG вместо регистра указывается номер арифметическо-логической функции (например, ADD, XOR, CMP и другие)
На самом деле, в базовом наборе инструкции (00-FF) таких групповых инструкции не так много, если не считать те, которые отвечают за работу математического сопроцессора (сразу скажу – рассматривать мы его не будем, это крайне сложно). Некоторые групповые инструкции подобные опкоду 80h используют непосредственное значение, а некоторые не используют – это зависит от опкода. Конечно, каждый из них мы будем разбирать и реализовывать позднее, и я думаю, это будет достаточно сложно и утомительно.
Значение поля REG расшифровывается разными способами. Я приведу несколько его возможных вариации:
+=====+=====+=====+=====+=====+=====+=====+=====+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | РЕГИСТР
+-----+-----+-----+-----+-----+-----+-----+-----+
| AL | CL | DL | BL | AH | CH | DH | BH | 8 БИТ
| AX | CX | DX | BX | SP | BP | SI | DI | 16 БИТ
| EAX | ECX | EDX | EBX | ESP | EBP | ESI | EDI | 32 БИТ
| ES | CS | SS | DS | FS | GS | - | - | СЕГМЕНТЫ
| CR0 | - | CR2 | CR3 | CR4 | - | - | - | УПРАВЛЯЮЩИЕ
| DR0 | DR1 | DR2 | DR3 | DR4 | DR5 | DR6 | DR7 | ОТЛАДКА
+=====+=====+=====+=====+=====+=====+=====+=====+
Помимо регистров, могут использоваться также сегменты, управляющие регистры и регистры отладки там, где это необходимо. Но это даже не всё, поскольку существуют регистры MMX и SSE, реализации которых тоже не будет, ибо они появились гораздо позже 386-го процессора, начиная с Pentium и старше.
Я упоминал еще то, что вместо номера регистра разных вариации вместо этого может использоваться функция:
+======+======+=======+=======+======+======+======+======+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | ФУНКЦИЯ
+------+------+-------+-------+------+------+------+------+
| ADD | OR | ADC | SBB | AND | SUB | XOR | CMP | 80-83
| ROL | ROR | RCL | RCR | SHL | SHR | SAL | SAR | C0-C1,D0-D3
| TEST | TEST | NOT | NEG | MUL | IMUL | DIV | IDIV | F6-F7
| INC | DEC | - | - | - | - | - | - | FE
| INC | DEC | CALLN | CALLF | JMPN | JMPF | PUSH | - | FF
+======+======+=======+=======+======+======+======+======+
Некоторые инструкции имеют неполную дешифрацию функции, такие как FEh или FFh. Если в первом случае с FEh всё ясно, эти инструкции оперируют 8-битными операндами, то инструкции опкода FFh не имеют 7-й функции и вместо нее процессор вызывает исключение #UD (UndefineD), то есть, ошибку раскодировки номера инструкции.
Таблица, на самом деле, весьма полезная и будет потом активно использоваться в качестве указателя и справочника при разработке этих инструкции.
§ Поле MEM
В отличии от поля REG, здесь всегда будет использоваться только операнд, который может быть извлечен либо из памяти, либо из разного рода регистров. Поле MEM всегда идёт совместно с полем MOD, потому необходимо рассматривать их в паре.
Если поле MEM используется в качестве регистра, то поле MOD равно двоичному 11 (или десятичному 3). В этом случае вычисление эффективного адреса не происходит и передается операнд в виде регистра на вход/выход. Если же поле MOD в байте ModRM не равно 11, то тогда в 16-битном режиме происходит вычисление эффективного адреса по правилам, указанным в нижеследующей таблице.
MEM | 00 | 01 | 10 MOD
----+-------+---------+---------
000 | BX+SI | BX+SI+b | BX+SI+w
001 | BX+DI | BX+DI+b | BX+DI+w
010 | BP+SI | BP+SI+b | BP+SI+w
011 | BP+DI | BP+DI+b | BP+DI+w
100 | SI | SI+b | SI+w
101 | DI | DI+b | DI+w
110 | A16 | BP+b | BP+w
111 | BX | BX+b | BX+w
Как и обычно, стоит внести несколько пояснений насчет того что тут написано.
MEM=110, MOD=00: это особый случай, когда вместо косвенной адресации указывается 16-битное значение адреса. Именно 16-битное и никакое другое. Если нам необходим будет 32-х битный адрес в памяти, то для этого существует 32-х битный ModRM, а о нем будет позже беседа.
- Здесь
+b означает 8-битное знаковое смещение. Это значит, что после того как рассчитался допустим, адрес BP+SI, то к нему еще добавляется число в диапазоне [-128,127], идущее сразу же за байтом modrm.
- Аналогично с
+w, но число это уже будет 16-битное, оно также идет за байтом modrm и его диапазон составляет [-32768,32767] включительно
- Можно заметить, что у нас нет простого указателя на
[BP] и это правда, но это не беда. Можно просто использовать вариацию MOD=01 со значением +b равным 0, что по итогу дает [BP+0]=[BP]. Это было сделано намеренно, чтобы освободить место для прямой адресации в памяти.
Замечу что при чтении или записи из памяти по умолчанию берется сегмент DS, если не указано иное через префикс, но в случае, когда происходит выбор эффективного адреса с присутствием в качестве компоненты регистра BP, то по умолчанию выбирается не DS, а сегмент стека SS, опять же, если не было явного указания сегментного префикса. В случае если указан префикс, то он имеет больший приоритет и выбирается в первую очередь, но если не указан, то будет выбран либо DS, либо же SS сегмент.