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

Они пришли из древних процессоров, когда числа представлялись как в калькуляторах, в десятично-двоичном представлении, то есть в каждых 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.
1case 0x27:
2
3    // Раунд 1: Нижний ниббл
4    if (((regs[REG_AL] & 0xF) > 9) || flags.a) {
5
6        i_tmp = regs[REG_AL] + 6;
7        regs[REG_AL] = i_tmp;
8        flags.c = !!(i_tmp & 0xFF00);
9        flags.a = 1;
10    }
11
12    // Раунд 2: Верхний ниббл
13    if ((regs[REG_AL] > 0x9F) || flags.c) {
14
15        regs[REG_AL] += 0x60;
16        flags.c = 1;
17    }
18
19    /* Установка флагов */
20    i_tmp = regs[REG_AL];
21
22    flags.s = !!(i_tmp & 0x80);
23    flags.z = !i_tmp;
24    flags.p = parity(i_tmp);
25    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), десятичная коррекция после вычитания. Эта инструкция уже посложнее для понимания.
1case 0x2F:
2
3    old_cf  = flags.c;
4    flags.c = 0;
5    i_op1   = regs[REG_AL];
6
7    // Раунд 1: Нижний ниббл
8    if (((i_op1 & 0x0F) > 9) || flags.a) {
9
10        i_tmp = regs[REG_AL] - 6;
11
12        regs[REG_AL] = i_tmp;
13        flags.c = !!(i_tmp & 0xFF00) | old_cf;
14        flags.a = 1;
15    }
16
17    // Раунд 2: Верхний ниббл
18    if ((i_op1 > 0x99) || flags.c) {
19
20        regs[REG_AL] -= 0x60;
21        flags.c = 1;
22    }
23
24    // Установка флагов
25    i_tmp = regs[REG_AL];
26
27    flags.s = !!(i_tmp & 0x80);
28    flags.z = !i_tmp;
29    flags.p = parity(i_tmp);
30    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 в понимании, так как она использует лишь одно число вместо двух.
1case 0x37:
2
3    if ((regs[REG_AL] & 0x0F) > 9 || flags.a) {
4        regs16[REG_AX] += 0x106;
5        flags.a = 1;
6        flags.c = 1;
7    } else {
8        flags.a = 0;
9        flags.c = 0;
10    }
11    regs[REG_AL] &= 0x0F;
12    break;
Особо тут комментировать нечего. Если происходит превышение 9 или перенос, то добавляется 6 в нижний байт, а также +1 в AH, поскольку был перенос и коррекция. Например при сложении 9+9 получится ответ 0x0108 или AH=1, AL=8.
1case 0x3F: // AAS
2
3    if ((regs[REG_AL] & 0x0F) > 9 || flags.a) {
4        regs16[REG_AX] -= 6;
5        regs[REG_AH] -= 1;
6        flags.a = 1;
7        flags.c = 1;
8    } else {
9        flags.a = 0;
10        flags.c = 0;
11    }
12    regs[REG_AL] &= 0x0F;
13    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.
1case 0xD4:
2
3    i_tmp = fetch(1);
4    if (i_tmp == 0) interrupt(0);
5    else {
6        regs[REG_AH]  = regs[REG_AL] / i_tmp;
7        regs[REG_AL] %= i_tmp;
8    }
9    break;
Кстати интересный момент. После опкода AAA и AAD идет 1 байт, который будет определять разрядность. Обычно это байт 10 - десятичная система, но можно установить любой. Если будет установлен 0, то выдаст ошибку #DE (Деления на 0).

§ AAD

Эта инструкция выполняет обратную операцию, она собирает из AH*10 + AL в регистр AL обратно число.
1case 0xD5:
2
3    i_tmp = fetch(1);
4    regs[REG_AL] = regs[REG_AL] + i_tmp*regs[REG_AH];
5    regs[REG_AH] = 0;
6    break;
Исходные коды скачать тут.
Следующий материал