§ Об инструкциях

Они пришли из древних процессоров, когда числа представлялись как в калькуляторах, в десятично-двоичном представлении, то есть в каждых 4 битах байта было число от 0 до 9. Это представление было наглядным, но с точки зрения машины - совершенно неоптимальным. Потому что при сложении 9+1 получается 10, а не 16, так как 16 это 10h, и поэтому надо было как-то корректировать это.
Например, если сложить числа 4+7 то получим 11, то есть, в шестнадцатеричном виде это 0Bh, в двоичном это 1011. Но нам надо получить 11h. Чтобы это сделать, придется добавить +6 к результату 0Bh, и получится 11h (или 17 в десятичном виде). Этот функционал, на самом деле, уже устарел. Никто не использует двоично-десятичную запись, но инструкции остались для совместимости. Приведу их программный код с краткими пояснениями.
Еще интересный момент. При сложении 9+9 получается число 18, а это 12h. Кажется, будто результат верный, поскольку в нижних 4 битах не число от 10 до 15. Но, однако, существует так называемый Auxiliary Flag (AF), который сигнализирует, что на самом деле, был перенос с 3 на 4 разряд. Допустим инструкция ADD
MOV AL, 9
ADD AL, 9
DAA - коррекция
после выполнения устанавливает флаг AF=1. Есть такая инструкция, DAA, она как раз проверяет наличие флага AF, и если он есть, то прибавляет +6 к результату, то есть результат будет не 12h, а 18h - корректный.
Поэтому коррекция (нижних 4 бит) при сложении может произойти в двух случаях - при установленном флаге A, или при значении, большем чем 9.

§ DAA

DAA (Decimal Adjust after Addition) инструкция выполняет коррекцию двух нибблов (4-х битных значений), то есть, фактически, она выравнивает результат для 2-х цифр, которые можно записать в десятичном виде в регистре AL.
case 0x27:

    // Раунд 1: Нижний ниббл
    if (((regs[REG_AL] & 0xF) > 9) || flags.a) {

        i_tmp = regs[REG_AL] + 6;
        regs[REG_AL] = i_tmp;
        flags.c = !!(i_tmp & 0xFF00);
        flags.a = 1;
    }

    // Раунд 2: Верхний ниббл
    if ((regs[REG_AL] > 0x9F) || flags.c) {

        regs[REG_AL] += 0x60;
        flags.c = 1;
    }

    /* Установка флагов */
    i_tmp = regs[REG_AL];

    flags.s = !!(i_tmp & 0x80);
    flags.z = !i_tmp;
    flags.p = parity(i_tmp);
    break;
С нижним нибблом все понятно. Если он более 9 или есть флаг AF, то прибавляется +6. В случае, если был произведен перенос с 7 на 8 бит, устанавливается флаг CF, который нужен будет далее. И также ставится флаг AF в любом случае.
Старший ниббл действует точно таким же методом. При значении от 10 до 15 (A-F), то есть, начиная с A0h, будет также добавлен +60h, чтобы выровнять код. Если же был флаг переноса, то в этом случае тоже так же добавляется +60h для выравнивания. Флаг переноса CF играет ту же роль, что и флаг AF для нижнего ниббла. Ну и соответственно, если такая коррекция произошла для старшего ниббла, то ставится CF=1 в любом случае.
В конце операции вычисляются флаги S, Z и P по стандартному алгоритму для регистра AL.

§ DAS

Decimal Adjust after Subtract (DAS), десятичная коррекция после вычитания. Эта инструкция уже посложнее для понимания.
case 0x2F:

    old_cf  = flags.c;
    flags.c = 0;
    i_op1   = regs[REG_AL];

    // Раунд 1: Нижний ниббл
    if (((i_op1 & 0x0F) > 9) || flags.a) {

        i_tmp = regs[REG_AL] - 6;

        regs[REG_AL] = i_tmp;
        flags.c = !!(i_tmp & 0xFF00) | old_cf;
        flags.a = 1;

    } else {
        flags.a = 0;
    }

    // Раунд 2: Верхний ниббл
    if ((i_op1 > 0x99) || flags.c) {

        regs[REG_AL] -= 0x60;
        flags.c = 1;
    }

    // Установка флагов
    i_tmp = regs[REG_AL];

    flags.s = !!(i_tmp & 0x80);
    flags.z = !i_tmp;
    flags.p = parity(i_tmp);
    break;
Сохраняются старое значение флагов и регистра AL. Также как и DAA, если был флаг переноса (заема) из 4-го бита в 3-й (AF), или значение младшего ниббла больше 9, то вычитается -6, чтобы скорректировать результат.
Давайте проверим. Допустим, вычитаем 11h - 03h, получается результат 0Eh, что точно неверно, потому что должен получиться 8. Вычитаем 0Eh - 6 = 8, записывается в AL следующее значение 08h, это верно. Теперь второй случай. Вычитаем 10h - 09h, получаем 07h, установлен флаг AF=1. Поскольку 7 меньше 10, то казалось бы, все нормально, но есть флаг AF=1, который сообщает, что произошел перенос из 4-го бита. Корректируем: 10h-09h-06h = 1, получается правильный ответ. Выставляется флаг CF если он был уже ранее, или произошел заем из 8-го бита.
Для старшего ниббла тоже так же, как и для младшего. В случае превышения 99h в оригинальном AL, или в случае если флаг CF был установлен, делается вычет 60h, чтобы выровнять старший ниббл.
Флаги S,Z,P устанавливаются так же как и в DAA, и везде. Далее я не буду о них говорить по причине очевидности происходящего.

§ AAA, AAS

Инструкция AAA выполняет почти что тоже самое что и DAA, но работает не с упакованными нибблами, а именно байтом AL и AH. Эта инструкция проще чем DAA в понимании, так как она использует лишь одно число вместо двух.
case 0x37:

    if ((regs[REG_AL] & 0x0F) > 9 || flags.a) {
        regs16[REG_AX] += 0x106;
        flags.a = 1;
        flags.c = 1;
    } else {
        flags.a = 0;
        flags.c = 0;
    }
    regs[REG_AL] &= 0x0F;
    break;
Особо тут комментировать нечего. Если происходит превышение 9 или перенос, то добавляется 6 в нижний байт, а также +1 в AH, поскольку был перенос и коррекция. Например при сложении 9+9 получится ответ 0x0108 или AH=1, AL=8.
case 0x3F: // AAS

    if ((regs[REG_AL] & 0x0F) > 9 || flags.a) {
        regs16[REG_AX] -= 6;
        regs[REG_AH] -= 1;
        flags.a = 1;
        flags.c = 1;
    } else {
        flags.a = 0;
        flags.c = 0;
    }
    regs[REG_AL] &= 0x0F;
    break;
В AAS происходит примерно тоже самое, но тут сначала из регистра AX вычитается 6, а потом из регистра AH вычитается 1.

§ AAM

Эта инструкция занимается тем, что просто разделяет число AL на два байта - AH и AL по такому принципу
AH = AL DIV 10 Целочисленное значение деления на 10
AL = AL MOD 10 Остаток от деления на 10
Такая ситуация возникает, когда число AL умножается на другое число. Пример, AL=8, умножим на 7, получится 56 или 0x38. Поскольку надо получить AX=0506h, а не AL=0x38h, то как раз выполняется инструкция AAA, которая помещает в регистр AH=5 и AL=6.
case 0xD4:

    i_tmp = fetch(1);
    if (i_tmp == 0) interrupt(0);
    else {
        regs[REG_AH]  = regs[REG_AL] / i_tmp;
        regs[REG_AL] %= i_tmp;
    }
    break;
Кстати интересный момент. После опкода AAA и AAD идет 1 байт, который будет определять разрядность. Обычно это байт 10 - десятичная система, но можно установить любой. Если будет установлен 0, то выдаст ошибку #DE (Деления на 0).

§ AAD

Эта инструкция выполняет обратную операцию, она собирает из AH*10 + AL в регистр AL обратно число.
case 0xD5:

    i_tmp = fetch(1);
    regs[REG_AL] = regs[REG_AL] + i_tmp*regs[REG_AH];
    regs[REG_AH] = 0;
    break;
Исходные коды скачать тут.
Следующий материал
7 окт, 2020
© 2007-2022 Том плющит отлично