§ Инструкция 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
Исходные коды к статье здесь.
Следующий материал