§ Какие есть флаги

Архитектура x86 - флагово-ориентированная, то есть после выполнения инструкции выставляются флаги в регистр флагов, и на основе тех данных, что находятся во флагах, делается вывод о том, что делать процессору дальше. Например, после сложения получается результат и устанавливаются флаги
  • O (Overflow, переполнение)
  • S (Sign, знак)
  • Z (Zero, ноль)
  • A (Aux, переполнение 4 знака)
  • P (Parity, четность результата)
  • C (Carry, перенос)
Помимо флагов, которые устанавливаются АЛУ, есть еще и системные и флаги управления
  • D (Direction) флаг направления - вверх или вниз для строковых инструкции
  • I (Interrupt) разрешение прерываний
  • T (Trace) отладочный, после каждой инструкции вызывается прерывание 1

§ Виды условий

Всего существует 16 типов условий. В самом деле их 8, а еще 8 - это отрицательные к 8, то есть и получается 16.
0000 O   OF=1 Переполнение
0001 NO  OF=0 Нет переполнения
0010 C   CF=1 Перенос
0011 NC  CF=0 Нет переноса
0100 Z   ZF=1 Результат нуль
0101 NZ  ZF=0 Не нуль
0110 BE  CF=1 OR  ZF=1 Целочисленное меньше или равно
0111 A   CF=0 AND ZF=0 Целочисленное больше
1000 S   SF=1 Есть знак (отрицательное число)
1001 NS  SF=0 Нет знака (положительное)
1010 P   PF=1 Нижние 8 бит результата имеют четное количество бит
1011 NP  PF=0 Или нечетное
1100 L   SF!=OF Если флаг S не равно флагу O, считается, что знаковое число меньше (<)
1101 NL  SF=OF Или знаковое число больше или равно (>=)
1110 LE  (ZF=1) OR (SF!=OF) Знаковое меньше или равно (<=)
1111 G   (ZF=0) AND (SF=OF) Знаковое "больше" (>)
Теперь представлю это в виде кода
// Проверка условий
int cond(int cond_id) {

    switch (cond_id & 15) {

        case 0:  return  flags.o; // O
        case 1:  return !flags.o; // NO
        case 2:  return  flags.c; // C
        case 3:  return !flags.c; // NC
        case 4:  return  flags.z; // Z
        case 5:  return !flags.z; // NZ
        case 6:  return  flags.c ||  flags.z; // BE
        case 7:  return !flags.c && !flags.z; // A
        case 8:  return  flags.s; // S
        case 9:  return !flags.s; // NS
        case 10: return  flags.p; // P
        case 11: return !flags.p; // NP
        case 12: return  flags.s != flags.o; // L
        case 13: return  flags.s == flags.o; // NL
        case 14: return (flags.s != flags.o) ||  flags.z; // LE
        case 15: return (flags.s == flags.o) && !flags.z; // G
    }

    return 0;
}
Код же для условных переходов становится совсем несложным:
case 0x70: case 0x71: case 0x72: case 0x73:
case 0x74: case 0x75: case 0x76: case 0x77:
case 0x78: case 0x79: case 0x7A: case 0x7B:
case 0x7C: case 0x7D: case 0x7E: case 0x7F:

    i_tmp = (signed char) fetch(1);
    if (cond(opcode_id & 15))
        regs16[REG_IP] += i_tmp;

    break;
Сначала читается знаковый байт после опкода, после чего, если условие срабатывает (выбор условия идет из номера опкода), то тогда знаковое число добавляется к регистру IP. В этот момент регистр IP должен указывать на следующий байт после инструкции условного перехода.
Поскольку иногда требуются 16-битные смещения, то использую расширенные опкоды тоже
case 0x180: case 0x181: case 0x182: case 0x183:
case 0x184: case 0x185: case 0x186: case 0x187:
case 0x188: case 0x189: case 0x18A: case 0x18B:
case 0x18C: case 0x18D: case 0x18E: case 0x18F:

    i_tmp = fetch(2);
    if (cond(opcode_id & 15))
        regs16[REG_IP] += i_tmp;

    break;
Отличий тут совсем немного - только вместо 1 байта - 2 байта.

§ Безусловные переходы

Помимо условных переходов, которые выполняются в зависимости от того, какие установлены или сброшены флаги, существуют также и безусловные, которые выполняются всегда. Существуют 3 вида перехода - short (1 байт), near (2 или 4 байта) и far (4 или 6 байт). Количество байтов зависит от того, какой режим сейчас работает - 16 или 32 битный.
// JMP near
case 0xE9: i_tmp = fetch(2); regs16[REG_IP] += i_tmp; break;

// JMP far offset:segment
case 0xEA:

    i_tmp  = fetch(2);
    i_tmp2 = fetch(2);
    regs16[REG_IP] = i_tmp;
    regs16[REG_CS] = i_tmp2;
    break;

// JMP short
case 0xEB: i_tmp = fetch(1); regs16[REG_IP] += (signed char) i_tmp; break;
JMP near выполняет относительный переход (относительно начала следующей инструкции за JMP) на 16 бит. Поскольку regs16 имеет тип unsigned short, то приводить i_tmp к signed short не требуется, чтобы вычислить следующий адрес.
JMP far получает 2 байта нового IP и еще 2 байта нового CS и выполняет переход по ним.
JMP short аналогично безусловному варианту условных переходов, используется 1 байт для вычисления адреса.
Есть еще одна инструкция, которая хитро запрятана в групповых инструкциях, которая тоже выполняет переход, но другим методом, косвенно. Она находится в опкоде FFh под номером i_reg = 4 и i_reg = 5.

§ Переходы в групповой инструкции

Когда была получена групповая инструкция, то решение о том, какую инструкцию выполнить, зависит от указания i_reg, регистровой части в байте modrm. Получается, что сначала сканируется байт modrm, оттуда получается адрес r/m и reg-часть. Номер reg-части решает то, какая будет инструкция выполнена:
case 0xFF:

    switch (i_reg) {

        // JMP r/m
        case 4: reg_ip = get_rm(1); break;

        // JMP far [bx] как пример
        case 5:

            i_tmp  = SEGREG(segment_id, i_ea);
            reg_ip = rd(i_tmp, 2);
            regs16[REG_CS] = rd(i_tmp + 2, 2);
            break;
    }

    break;
При i_reg=4 в регистр IP будет записан операнд rm (это либо значение из памяти, либо их регистра).
При i_reg=5 будет получен эффективный адрес совместно с тем сегментом, что был выбран (ds, ss, или другой, который был выбран префиксом), прочитаны из памяти 2 байта, записаны в IP, потом прочитаны еще 2 байта и записаны в CS. В Protected Mode тут идут строгие проверки на наличие нарушения диапазонов, но 8086 процессор этими проверками не обладает.

§ LOOP, JCXZ

Существует еще один вид коротких переходов, которые достаточно удобны для организации циклов. Это инструкции LOOP. Эта инструкция сначала уменьшает значение CX на 1, после чего проверяется на 0. Если получен 0, то инструкция не выполняется и переходит к следующей за ней инструкции. Если получен CX не равным 0, тогда, если это инструкция LOOP, то выполняется короткий переход (в пределах от -128 до 127), если LOOPNZ, то выполняется переход только если ZF=0, если LOOPZ, то если ZF=1.
case 0xE0:
case 0xE1:
case 0xE2:

    i_tmp = (signed char) fetch(1);

    // Сперва CX уменьшается
    regs16[REG_CX]--;

    // Если CX <> 0, то можно выполнить переход
    if (regs16[REG_CX]) {

        if ((/* LOOPNZ */ opcode_id == 0xE0 && !flags.z) ||
            (/* LOOPZ  */ opcode_id == 0xE1 &&  flags.z) ||
            (/* LOOP   */ opcode_id == 0xE2))
                reg_ip += i_tmp;
    }

    break;
Инструкция JCXZ проверяет на ноль регистр CX. Если он равен 0, то происходит короткий переход.
case 0xE3:

    i_tmp = (signed char) fetch(1);
    if (!regs16[REG_CX])
        reg_ip += i_tmp;

    break;
Исходные коды прикреплены в файле.
Следующий материал
29 сен, 2020
© 2007-2022 Муть в том, что Чичерина взлетает котом