Оглавление
§ Префиксы
Вот и настал тот день и час, когда мы начнем декодирование инструкции 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;
unsigned char d;
unsigned char i;
unsigned char t;
unsigned char s;
unsigned char z;
unsigned char a;
unsigned char p;
unsigned char c;
};
struct flags_struct flags;
Также сделаю обозначения для указания регистров в памяти, это хоть и не так важно, но будет удобно
enum regs_name {
REG_AX = 0, REG_CX = 1, REG_DX = 2, REG_BX = 3,
REG_SP = 4, REG_BP = 5, REG_SI = 6, REG_DI = 7,
REG_AL = 0, REG_CL = 2, REG_DL = 4, REG_BL = 6,
REG_AH = 1, REG_CH = 3, REG_DH = 5, REG_BH = 7,
REG_ES = 8, REG_CS = 9, REG_SS = 10, REG_DS = 11,
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;
regs16[REG_IP] = 0x0100;
regs [REG_DL] = 0x00;
Тут на всякий случай также выставляется флаг TF=0, чтобы он не срабатывал.
§ Считывание опкода
Для начала надо написать вспомогательную функцию, которая извлекает следующий байт (или слово) из потока по адресу 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);
}
Рассмотрю процедуру считывания опкода с префиксами
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:
break;
case 0xF0: break;
case 0xF2: i_rep = REPNZ; break;
case 0xF3: i_rep = REPZ; break;
default:
return data;
}
}
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();
}
В этой процедуре происходит выборка префиксов и опкода. И на этом пока что все.