§ Префиксы

Вот и настал тот день и час, когда мы начнем декодирование инструкции 8086! Для начала теория, которую кто-то прекрасно изложил в книгах, но я скажу кратко. Инструкция x86 состоит из следующих частей:
  • Префиксы или REX (от 0 до 15 байт)
  • Опкод (1 байт)
  • Байт ModRM (1 байт)
  • Байт SIB - если это 32х битный адрес (1 байт)
  • Смещение (от 0 до 8 байт)
  • Непосредственные операнды (от 0 до 8 байт)
Необходимо сначала декодировать префиксы и опкод. После этого мы можем точно сказать, нужен ли будет байт modrm/sib и непосредственные операнды после них.
Префиксов существует не так много, а именно:
  • 0Fh - Расширенный опкод
  • 26h - Префикс сегмента ES:
  • 2Eh - Префикс сегмента CS:
  • 36h - Префикс сегмента SS:
  • 3Eh - Префикс сегмента DS:
  • 64h - Префикс сегмента FS: только для 386
  • 65h - Префикс сегмента GS: только для 386
  • 66h - Размер операнда (16/32 бит) 386+
  • 67h - Размер адреса (16/32 бит) 386+
  • F0h - LOCK: для эксклюзивной блокировки шины (без понятия зачем это нужно)
  • F2h - REPNZ (либо REP) префикс для повтора строковых инструкции
  • F3h - REPZ (либо REP) аналогично, но повтор до тех пор пока будет Z
Вот и все префиксы. Однако их комбинации в сочетании с некоторыми опкодами дают разный эффект, но это не касается 8086 процессора, а гораздо старше процессоры, начиная с Pentium.

§ Инициализация

Так что же, приступим писать код. Для начала надо создать регистры и сегментные регистры. Регистров у нас 8 штук, сегментных регистров только 4, остальные 2 (fs: gs:) рассматривать не будем.
1unsigned char   regs[32];
2unsigned short* regs16;
Не забудем про флаги. Для флагов вообще структуру другую сделаю
1struct flags_struct {
2    unsigned char o; // 11 overflow
3    unsigned char d; // 10 direction
4    unsigned char i; //  9 interrupt
5    unsigned char t; //  8 trap
6    unsigned char s; //  7 sign
7    unsigned char z; //  6 zero
8    unsigned char a; //  4 aux
9    unsigned char p; //  2 parity
10    unsigned char c; //  0 carry
11};
12struct flags_struct flags;
Также сделаю обозначения для указания регистров в памяти, это хоть и не так важно, но будет удобно
1enum regs_name {
2
3    // 16 bit
4    REG_AX = 0, REG_CX = 1, REG_DX = 2, REG_BX = 3,
5    REG_SP = 4, REG_BP = 5, REG_SI = 6, REG_DI = 7,
6
7    // 8 bit
8    REG_AL = 0, REG_CL = 2, REG_DL = 4, REG_BL = 6,
9    REG_AH = 1, REG_CH = 3, REG_DH = 5, REG_BH = 7,
10
11    // Segment
12    REG_ES = 8, REG_CS = 9, REG_SS = 10, REG_DS = 11,
13
14    // System
15    REG_IP = 12
16};
А теперь важный момент. Перед запуском машины, поскольку я буду использовать BIOS от другого эмулятора, то надо чтобы были заполнены следующие поля:
  • DL = 00h (FD) или 80h (HD)
  • CX:AX = размер диска в секторах (по 512 байт)
  • CS = F000h
  • IP = 100h
Вот тут есть один интересный лайфхак. Дело в том, что regs16 указывает на regs, но с другим типом. Сам по себе regs состоит из следующих байт:
1regs[1] = AH  regs[0] = AL
2regs[3] = CH  regs[2] = CL
3regs[5] = DH  regs[4] = DL
4regs[7] = BH  regs[6] = BL
5
6regs[ 9: 8] = SP
7regs[11:10] = BP
8regs[13:12] = SI
9regs[15:14] = DI
10regs[17:16] = ES
11regs[19:18] = CS
12regs[21:20] = SS
13regs[23:22] = DS
14regs[25:24] = IP
Чтобы regs16 указывал на regs, это надо в программе написать:
1regs16  = (unsigned short*) & regs;
2flags.t = 0;
3
4regs16[REG_CS] = 0xF000;  // CS = 0xF000
5regs16[REG_IP] = 0x0100;  // IP = 0x0100
6regs  [REG_DL] = 0x00;    // Загружаем с FD
Тут на всякий случай также выставляется флаг TF=0, чтобы он не срабатывал.

§ Считывание опкода

Для начала надо написать вспомогательную функцию, которая извлекает следующий байт (или слово) из потока по адресу CS:IP
1// Считывание очередного byte/word из CS:IP
2unsigned int fetch(unsigned char wsize) {
3
4    int address = regs16[REG_CS]*16 + regs16[REG_IP];
5    regs16[REG_IP] += wsize;
6    return rd(address, wsize);
7}
Рассмотрю процедуру считывания опкода с префиксами
1// Считывание опкода 000h - 1FFh
2unsigned int fetch_opcode() {
3
4    i_size = 0;
5    i_rep  = 0;
6    segment_over_en = 0;
7    segment_id = REG_DS;
8
9    while (i_size < 16) {
10
11        uint8_t data = fetch(1);
12
13        switch (data) {
14
15            // Получен расширенный опкод
16            case 0x0F: return 0x100 + fetch(1);
17
18            // Сегментные префиксы
19            case 0x26: segment_over_en = 1; segment_id = REG_ES; break;
20            case 0x2E: segment_over_en = 1; segment_id = REG_CS; break;
21            case 0x36: segment_over_en = 1; segment_id = REG_SS; break;
22            case 0x3E: segment_over_en = 1; segment_id = REG_DS; break;
23            case 0x64:
24            case 0x65:
25            case 0x66:
26            case 0x67:
27                /* undefined opcode */
28                break;
29            case 0xF0: break; // lock:
30            case 0xF2: i_rep = REPNZ; break;
31            case 0xF3: i_rep = REPZ; break;
32            default:
33                return data;
34        }
35    }
36
37    /* undefind opcode */
38    return 0;
39}
На самом деле тут все просто. Необходимо всего лишь считать префиксы сегментов и префиксы REP, остальные не понадобятся. При необходимости там где сейчас undefined code, можно вызывать GP(0) к примеру (GP = General Protection).
Итак, считываем байт. Если это байт префикса расширения опкода (0Fh), то дочитываем следующий байт, который и будет опкодом, и тут же выходим из функции. Если это префикс сегмента, то указывается какой именно сегмент будет замещен и установлен флаг segment_over_en. Если это REPNZ, REPZ, то в переменную i_rep будет указан либо REPNZ(1), либо REPZ(2).
Все переменные требуется, конечно же, указать:
1int i_size;
2int i_rep;
3int opcode_id;
4int segment_over_en;
5int segment_id;
6enum rep_prefix {
7    REPNZ = 1,
8    REPZ = 2,
9};
Поскольку статья у нас только про декодирование опкода, то этого будет пока что достаточно:
1// Выполнение инструкции
2void step() {
3    opcode_id = fetch_opcode();
4}
В этой процедуре происходит выборка префиксов и опкода. И на этом пока что все.
Коды, как обычно, прикреплены в файле.
Следующая статья "Декодирование операндов байта modrm"