Лисья Нора

Оглавление


§ Инструкция NOP и каркас

Пришло время для хотя бы какой-нибудь реализации инструкции. Сегодня будут разобраны следующие инструкции: NOP; XCHG ax, r16; CBW; CWD; FWAIT; SAHF; LAHF; CLC; STC; CLI; STI; CLD; STD; XLAT. Это самые простые инструкции из всех представленных в базовом наборе 8086.
NOP реализуется тривиально:
switch (opcode_id) {
 
case 0x90: break;
 
// .. другие инструкции
 
default: ud_opcode(opcode_id);
}
То есть буквально эта инструкция ничего не делает. Но это не совсем так. На самом деле она делает обмен AX с AX, но в итоге это все равно получается NOP (No OPeration). Здесь есть такая ситуация, что при любом другом неопознанном опкоде будет вызываться процедура ud_opcode.
// Неизвестный опкод
void ud_opcode(int data) {
 
printf("UNDEFINED OPCODE: %x\n", data);
exit(2); // Завершить выполнение машины
}
Данная процедура просто завершает исполнение программы, потому что никаких неизвестных опкодов на пути исполнения эмулятора быть не должно. Если это так, то произошла критическая ошибка.

§ XCHG ax, r16

Эта инструкция просто обменивает регистр AX с другим выбранным 16-битным регистром:
// NOP | XCHG AX, r16
case 0x90: case 0x91: case 0x92: case 0x93:
case 0x94: case 0x95: case 0x96: case 0x97:
 
i_reg = opcode_id & 7;
i_tmp = regs16[REG_AX];
regs16[REG_AX] = regs16[i_reg];
regs16[i_reg] = i_tmp;
break;
Сначала выбирается i_reg = 0..7, поскольку выбранный регистр зависит от самого опкода. Потом в i_tmp помещается старое значение AX, следующий шаг заменяет AX на новый регистр, и последний шаг помещает старое значение AX в выбранный регистр.

§ CBW, CWD, FWAIT

Это очень простая инструкция, которая занимается тем, что выставляет все биты либо в AH регистре в 0 или 1, или в DX. Инструкция CBW устанавливает FFh в AH, если старший бит AL равен 1, и в 00h – если 0. Соответственно так же работает CWD, но старший бит смотрится в регистре AX, а установка идет в DX.
// CBW, CWD
case 0x98: regs [REG_AH] = regs [REG_AL] & 0x80 ? 0xFF : 0x00; break;
case 0x99: regs16[REG_DX] = regs16[REG_AX] & 0x8000 ? 0xFFFF : 0x0000; break;
 
// FWAIT
case 0x9B: break;
FWAIT на самом деле должен ожидать, пока не будут выполнены все прерывания в модуле FPU, но так как во-первых, FPU у нас нет, а во-вторых, это эмулятор, то ждать FPU вообще не надо.

§ SAHF и LAHF

Эти инструкции загружают 8 бит либо из AH во флаги (SAHF), либо наоборот, из флагов в AH (LAHF). Их программная реализация будет выглядеть так:
case 0x9E: // SAHF
 
i_tmp = regs[REG_AH];
flags.c = !!(i_tmp & 0x01);
flags.p = !!(i_tmp & 0x04);
flags.a = !!(i_tmp & 0x10);
flags.z = !!(i_tmp & 0x40);
flags.s = !!(i_tmp & 0x80);
break;
Инструкция SAHF просто конвертирует флаги в соответствии с битами в регистре AH.
case 0x9F: // LAHF
 
regs[REG_AH] =
/* 0 */ (!!flags.c) |
/* 1 */ 0x02 |
/* 2 */ (!!flags.p<<2) |
/* 3 */ 0 |
/* 4 */ (!!flags.a<<4) |
/* 5 */ 0 |
/* 6 */ (!!flags.z<<6) |
/* 7 */ (!!flags.s<<7);
break;
Инструкция LAHF преобразует флаги в регистр AH. При этом бит 1 (0x02) всегда установлен. Это особенность архитектуры x86.

§ Установка и сброс флагов

Эти инструкции настолько простые, что не требуется даже описывать их:
// Установка и сброс флагов
case 0xD6: regs[REG_AL] = flags.c ? 0xFF : 0x00; break; // SALC
case 0xF4: regs16[REG_IP]--; break; // HLT
case 0xF5: flags.c = !flags.c; break; // CMC
case 0xF8: flags.c = 0; break; // CLC
case 0xF9: flags.c = 1; break; // STC
case 0xFA: flags.i = 0; break; // CLI
case 0xFB: flags.i = 1; break; // STI
case 0xFC: flags.d = 0; break; // CLD
case 0xFD: flags.d = 1; break; // STD
Здесь стоит только разве что пояснить насчет инструкции HLT. Эта инструкция занимается тем, что останавливает процессор. То есть, процессор просто застревает на этом месте и все. Единственное, что может сдвинуть процессор с "мертвой точки" – это вызов аппаратного прерывания.
А еще есть интересная инструкция SALC. Дело в том, что она недокументированная, однако все её используют, в том числе и компиляторы. Она настолько популярна, что пора бы уже Intel добавить пару строк в документацию. Тем более все процессоры ее поддерживают. Эта инструкция выставляет 0xFF в AL если CF=1 и 0, если CF=0.

§ XLAT

Существует весьма полезная инструкция, которая извлекает данные по адресу DS:BX + AL. Здесь DS можно заменить на другой сегмент с помощью сегментного префикса.
case 0xD7: // XLAT
regs[REG_AL] = rd(16*regs16[segment_id] + regs16[REG_BX] + regs[REG_AL], 1);
break;
Логика работы инструкции XLAT очень проста. К DS:BX добавляется значение AL, и из полученного адреса извлекается обратно в AL. Эта инструкция позволяет делать различные табличные преобразования (256 байтные). Между прочим интересный факт. На самом деле XLAT может выйти за пределы HIMEM, поскольку если сегмент DS=FFFFh, BX=FFFFh и AL=FFh, то максимальный адрес будет равен FFFF0h + FFFFh + FFh = 1100EEh.