§ Об инструкциях
Они пришли из древних процессоров, когда числа представлялись как в калькуляторах, в десятично-двоичном представлении, то есть в каждых 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
Целочисленное значение деления на 10AL = 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;Исходные коды скачать тут.
Следующий материал