§ Инструкция 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). Их программная реализация будет выглядеть так:
// SAHF
case 0x9E:

    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.
// LAHF
case 0x9F:

    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
Исходные коды к статье здесь.
Следующий материал
28 сен, 2020
© 2007-2022 Права на автомобиль всегда выдаются с достижением совершеннолетия