§ Инструкция NOP и каркас
Пришло время для хотя бы какой-нибудь реализации инструкции. Сегодня будут разобраны следующие инструкции: NOP; XCHG ax, r16; CBW; CWD; FWAIT; SAHF; LAHF; CLC; STC; CLI; STI; CLD; STD; XLAT. Это самые простые инструкции из всех представленных в базовом наборе 8086.NOP реализуется тривиально:
1switch (opcode_id) { 2 3 case 0x90: break; 4 5 // .. другие инструкции 6 7 default: ud_opcode(opcode_id); 8}То есть буквально эта инструкция ничего не делает. Но это не совсем так. На самом деле она делает обмен AX с AX, но в итоге это все равно получается NOP (No OPeration). Здесь есть такая ситуация, что при любом другом неопознанном опкоде будет вызываться процедура ud_opcode.
1// Неизвестный опкод 2void ud_opcode(int data) { 3 4 printf("UNDEFINED OPCODE: %x\n", data); 5 exit(2); // Завершить выполнение машины 6}Данная процедура просто завершает исполнение программы, потому что никаких неизвестных опкодов на пути исполнения эмулятора быть не должно. Если это так, то произошла критическая ошибка.
§ XCHG ax, r16
Эта инструкция просто обменивает регистр AX с другим выбранным 16-битным регистром:1// NOP | XCHG AX, r16 2case 0x90: case 0x91: case 0x92: case 0x93: 3case 0x94: case 0x95: case 0x96: case 0x97: 4 5 i_reg = opcode_id & 7; 6 i_tmp = regs16[REG_AX]; 7 regs16[REG_AX] = regs16[i_reg]; 8 regs16[i_reg] = i_tmp; 9 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.1// CBW, CWD 2case 0x98: regs [REG_AH] = regs [REG_AL] & 0x80 ? 0xFF : 0x00; break; 3case 0x99: regs16[REG_DX] = regs16[REG_AX] & 0x8000 ? 0xFFFF : 0x0000; break; 4 5// FWAIT 6case 0x9B: break;FWAIT на самом деле должен ожидать, пока не будут выполнены все прерывания в модуле FPU, но так как во-первых, FPU у нас нет, а во-вторых, это эмулятор, то ждать FPU вообще не надо.
§ SAHF и LAHF
Эти инструкции загружают 8 бит либо из AH во флаги (SAHF), либо наоборот, из флагов в AH (LAHF). Их программная реализация будет выглядеть так:1// SAHF 2case 0x9E: 3 4 i_tmp = regs[REG_AH]; 5 flags.c = !!(i_tmp & 0x01); 6 flags.p = !!(i_tmp & 0x04); 7 flags.a = !!(i_tmp & 0x10); 8 flags.z = !!(i_tmp & 0x40); 9 flags.s = !!(i_tmp & 0x80); 10 break;Инструкция SAHF просто конвертирует флаги в соответствии с битами в регистре AH.
1 2// LAHF 3case 0x9F: 4 5 regs[REG_AH] = 6 /* 0 */ (!!flags.c) | 7 /* 1 */ 0x02 | 8 /* 2 */ (!!flags.p<<2) | 9 /* 3 */ 0 | 10 /* 4 */ (!!flags.a<<4) | 11 /* 5 */ 0 | 12 /* 6 */ (!!flags.z<<6) | 13 /* 7 */ (!!flags.s<<7); 14 break;Инструкция LAHF преобразует флаги в регистр AH. При этом бит 1 (0x02) всегда установлен. Это особенность архитектуры x86.
§ Установка и сброс флагов
Эти инструкции настолько простые, что не требуется даже описывать их:1// Установка и сброс флагов 2case 0xD6: regs[REG_AL] = flags.c ? 0xFF : 0x00; break; // SALC 3case 0xF4: regs16[REG_IP]--; break; // HLT 4case 0xF5: flags.c = !flags.c; break; // CMC 5case 0xF8: flags.c = 0; break; // CLC 6case 0xF9: flags.c = 1; break; // STC 7case 0xFA: flags.i = 0; break; // CLI 8case 0xFB: flags.i = 1; break; // STI 9case 0xFC: flags.d = 0; break; // CLD 10case 0xFD: flags.d = 1; break; // STDЗдесь стоит только разве что пояснить насчет инструкции HLT. Эта инструкция занимается тем, что останавливает процессор. То есть, процессор просто застревает на этом месте и все. Единственное, что может сдвинуть процессор с "мертвой точки" - это вызов аппаратного прерывания.
А еще есть интересная инструкция SALC. Дело в том, что она недокументированная, однако все её используют, в том числе и компиляторы. Она настолько популярна, что пора бы уже Intel добавить пару строк в документацию. Тем более все процессоры ее поддерживают. Эта инструкция выставляет 0xFF в AL если CF=1 и 0, если CF=0.
§ XLAT
Существует весьма полезная инструкция, которая извлекает данные по адресу DS:BX + AL. Здесь DS можно заменить на другой сегмент с помощью сегментного префикса.1case 0xD7: // XLAT 2 regs[REG_AL] = rd(16*regs16[segment_id] + regs16[REG_BX] + regs[REG_AL], 1); 3 break;Логика работы инструкции XLAT очень проста. К DS:BX добавляется значение AL, и из полученного адреса извлекается обратно в AL. Эта инструкция позволяет делать различные табличные преобразования (256 байтные). Между прочим интересный факт. На самом деле XLAT может выйти за пределы HIMEM, поскольку если сегмент DS=FFFFh, BX=FFFFh и AL=FFh, то максимальный адрес будет равен FFFF0h + FFFFh + FFh = 1100EEh
Исходные коды к статье здесь.
Следующий материал