Лисья Нора

Оглавление


§ Схема инструкции

Пожалуй, это самая интересная и достаточно сложная тема, которая касается разбора операндов (обычно, двух).
Общую схему для одной инструкции можно представить так:
1..4 | 1..2 | 1 | 1 | 1..4 | 1..4
Префиксы | Опкод | ModRM | SIB | Смещение | Непосредственное значение
Значение каждого из компонент я буду разбирать очень подробно далее в последующих материалах, однако стоит заметить, что размер одной инструкции может достигать до 16 байт.
Минимальное количество байт, которые тратятся на инструкцию, равно 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-битного операнда, а 32-х битный метод оставим на следующую главу.
Как и говорилось выше, некоторые опкоды требуют наличия байта, который будет указывать на то, какие операнды использовать. Тот операнд, который находится слева – он называется операндом-назначением (destination), а справа – операндом-источником (source). Арифметические инструкции работают слева направо, к примеру:
SUB AX, DX
Здесь операнд-источник – это DX, а операнд-назначение – AX. Инструкция вычитает из AX значение DX и записывает результат обратно в AX. То, какие именно операнды и откуда их взять, как раз и определяет байт ModRM. Однако его структура такова, что можно использовать только 3 метода:
Здесь видно, что невозможно указать одновременно указатель на память в качестве операнда. Кстати говоря, в одном из примеров я указал в качестве операнда 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:
Здесь 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]

§ Поле REG

Из названия поля известно, что оно может указывать только на регистр. Однако, это не всегда так. Некоторые опкоды используют поле REG как номер функции, а не только как указание регистра. Пример такой групповой инструкции, как ее называют, будет опкод под номером 80h, который лишь указывает что:
На самом деле, в базовом наборе инструкции (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
Как и обычно, стоит внести несколько пояснений насчет того что тут написано.
Замечу что при чтении или записи из памяти по умолчанию берется сегмент DS, если не указано иное через префикс, но в случае, когда происходит выбор эффективного адреса с присутствием в качестве компоненты регистра BP, то по умолчанию выбирается не DS, а сегмент стека SS, опять же, если не было явного указания сегментного префикса. В случае если указан префикс, то он имеет больший приоритет и выбирается в первую очередь, но если не указан, то будет выбран либо DS, либо же SS сегмент.