§ Префиксы

Вот и настал тот день и час, когда мы начнем декодирование инструкции 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:) рассматривать не будем.
unsigned char   regs[32];
unsigned short* regs16;
Не забудем про флаги. Для флагов вообще структуру другую сделаю
struct flags_struct {
    unsigned char o; // 11 overflow
    unsigned char d; // 10 direction
    unsigned char i; //  9 interrupt
    unsigned char t; //  8 trap
    unsigned char s; //  7 sign
    unsigned char z; //  6 zero
    unsigned char a; //  4 aux
    unsigned char p; //  2 parity
    unsigned char c; //  0 carry
};
struct flags_struct flags;
Также сделаю обозначения для указания регистров в памяти, это хоть и не так важно, но будет удобно
enum regs_name {

    // 16 bit
    REG_AX = 0, REG_CX = 1, REG_DX = 2, REG_BX = 3,
    REG_SP = 4, REG_BP = 5, REG_SI = 6, REG_DI = 7,

    // 8 bit
    REG_AL = 0, REG_CL = 2, REG_DL = 4, REG_BL = 6,
    REG_AH = 1, REG_CH = 3, REG_DH = 5, REG_BH = 7,

    // Segment
    REG_ES = 8, REG_CS = 9, REG_SS = 10, REG_DS = 11,

    // System
    REG_IP = 12
};
А теперь важный момент. Перед запуском машины, поскольку я буду использовать BIOS от другого эмулятора, то надо чтобы были заполнены следующие поля:
  • DL = 00h (FD) или 80h (HD)
  • CX:AX = размер диска в секторах (по 512 байт)
  • CS = F000h
  • IP = 100h
Вот тут есть один интересный лайфхак. Дело в том, что regs16 указывает на regs, но с другим типом. Сам по себе regs состоит из следующих байт:
regs[1] = AH  regs[0] = AL
regs[3] = CH  regs[2] = CL
regs[5] = DH  regs[4] = DL
regs[7] = BH  regs[6] = BL

regs[ 9: 8] = SP
regs[11:10] = BP
regs[13:12] = SI
regs[15:14] = DI
regs[17:16] = ES
regs[19:18] = CS
regs[21:20] = SS
regs[23:22] = DS
regs[25:24] = IP
Чтобы regs16 указывал на regs, это надо в программе написать:
regs16  = (unsigned short*) & regs;
flags.t = 0;

regs16[REG_CS] = 0xF000;  // CS = 0xF000
regs16[REG_IP] = 0x0100;  // IP = 0x0100
regs  [REG_DL] = 0x00;    // Загружаем с FD
Тут на всякий случай также выставляется флаг TF=0, чтобы он не срабатывал.

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

Для начала надо написать вспомогательную функцию, которая извлекает следующий байт (или слово) из потока по адресу CS:IP
// Считывание очередного byte/word из CS:IP
unsigned int fetch(unsigned char wsize) {

    int address = regs16[REG_CS]*16 + regs16[REG_IP];
    regs16[REG_IP] += wsize;
    return rd(address, wsize);
}
Рассмотрю процедуру считывания опкода с префиксами
// Считывание опкода 000h - 1FFh
unsigned int fetch_opcode() {

    i_size = 0;
    i_rep  = 0;
    segment_over_en = 0;
    segment_id = REG_DS;

    while (i_size < 16) {

        uint8_t data = fetch(1);

        switch (data) {

            // Получен расширенный опкод
            case 0x0F: return 0x100 + fetch(1);

            // Сегментные префиксы
            case 0x26: segment_over_en = 1; segment_id = REG_ES; break;
            case 0x2E: segment_over_en = 1; segment_id = REG_CS; break;
            case 0x36: segment_over_en = 1; segment_id = REG_SS; break;
            case 0x3E: segment_over_en = 1; segment_id = REG_DS; break;
            case 0x64:
            case 0x65:
            case 0x66:
            case 0x67:
                /* undefined opcode */
                break;
            case 0xF0: break; // lock:
            case 0xF2: i_rep = REPNZ; break;
            case 0xF3: i_rep = REPZ; break;
            default:
                return data;
        }
    }

    /* undefind opcode */
    return 0;
}
На самом деле тут все просто. Необходимо всего лишь считать префиксы сегментов и префиксы REP, остальные не понадобятся. При необходимости там где сейчас undefined code, можно вызывать GP(0) к примеру (GP = General Protection).
Итак, считываем байт. Если это байт префикса расширения опкода (0Fh), то дочитываем следующий байт, который и будет опкодом, и тут же выходим из функции. Если это префикс сегмента, то указывается какой именно сегмент будет замещен и установлен флаг segment_over_en. Если это REPNZ, REPZ, то в переменную i_rep будет указан либо REPNZ(1), либо REPZ(2).
Все переменные требуется, конечно же, указать:
int i_size;
int i_rep;
int opcode_id;
int segment_over_en;
int segment_id;
enum rep_prefix {
    REPNZ = 1,
    REPZ = 2,
};
Поскольку статья у нас только про декодирование опкода, то этого будет пока что достаточно:
// Выполнение инструкции
void step() {
    opcode_id = fetch_opcode();
}
В этой процедуре происходит выборка префиксов и опкода. И на этом пока что все.
Коды, как обычно, прикреплены в файле.
Следующая статья "Декодирование операндов байта modrm"