Об одной недокументированной особенности умножения и деления на процессорах x86
Начиная с процессора 80286 компания Intel поддерживала полную совместимость «снизу-вверх» в системе команд. То есть если какая-то из команд процессора дает такой-то результат на 8086, то и на более поздних процессорах результат будет точно таким же (сейчас не будем рассматривать ошибки типа неправильного деления в Pentium I).
Но так ли это? Что за вопрос! Ведь если бы совместимость не сохранялась, то старые программы не могли бы выполняться, а ведь до сих пор на любом компьютере можно поностальгировать запустив Norton Commander или Tetris. Однако не все так просто… Начиная с 8080 в процессорах Intel есть регистр флагов, состояние которого определяется результатом последней команды вычисления данных. Все флаги в нем давно описаны и поведение их строго зафиксировано. Кроме двух исключений.
AF — бит вспомогательного переноса. Он определял произошел ли перенос из младшей тетрады в старшую. Для 8-разрядного 8080 это было логично и для всех его команд состояние однозначно определялось. При переходе на 16-разрядные процессоры x86 появилась двойственность — в каком байте нужно учитывать вспомогательный перенос ? На что Intel однозначно дала ответ — флаг AF меняет свое состояние только по результатам переноса внутри младшего байта. Но процессоре 8080 не было команд умножения и деления. И при переходе на 16-разрядную архитектуру поведение AF при командах умножения и деления так и осталось не определенным!
Дальше еще интереснее, оказалось что по-разному ведет себя еще и флаг четности! На разных поколениях процессоров и у разных производителей в результате операций умножения и деления флаги AF и PF разные!
Чтобы убедиться в этом можно выполнить коротенькую программу:
#include
#include
int main()
{
using namespace std;
const int min_i = -20, max_i = 50,min_j=-10, max_j = 50;
short int i, j, k, pf;
cout << "Name;i;j;Result;AF;PF;Calculated parity\n";
for (i = min_i; i < max_i; i++) {
for (j = min_j; j < max_j; j++) {
__asm {
mov ax, i
mov dx, j
imul dx
pushf
pop ax
mov k, ax
}
pf = i * j; pf = ((pf >> 7) ^ (pf >> 6) ^ (pf >> 5) ^ (pf >> 4) ^ (pf >> 3) ^ (pf >> 2) ^ (pf >> 1) ^ pf ^ 1) & 1;
cout << "Mul ;" << setw(4) << i << "; " << setw(4) << j << "; " << setw(4) << i * j << "; " << ((k & 0x10) ? "1" : "0") << "; " << ((k & 0x04) ? "1" : "0") << "; " << pf <<"\n";
}
}
for (i = min_i; i < max_i; i++) {
for (j = min_j; j < max_j; j++) {
if (j == 0) continue;
__asm {
mov ax, i
mov cx, j
cwd
idiv cx
pushf
pop ax
mov k, ax
}
pf = i / j; pf = ((pf >> 7) ^ (pf >> 6) ^ (pf >> 5) ^ (pf >> 4) ^ (pf >> 3) ^ (pf >> 2) ^ (pf >> 1) ^ pf ^ 1) & 1;
std::cout << "Div ;" << setw(4) << i << "; " << setw(4) << j << "; " << setw(4) << i / j << "; " << ((k & 0x10) ? "1" : "0") << "; " << ((k & 0x04) ? "1" : "0") << "; " << pf << "\n";
}
}
}
Эта программа выводит на экран результат проверки флагов AF и PF после операций умножения и деления. А так же (для удобства) рассчитанный результат четности последнего байта результата операции.
Я протестировал этой программой 16 разных процессоров. Вкратце результат такой:
Wolfdale | Sandy Bridge | Coffee Lake | AMD | ||||||||
Pentium Dual-Core E6600 | Core i5–2300 | Xeon E-2278G | AMD Ryzen 7 2700 | ||||||||
i | j | Result | Calс parity | AF | PF | AF | PF | AF | PF | AF | PF |
-20 | -10 | 200 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | -9 | 180 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | -8 | 160 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | -7 | 140 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
-20 | -6 | 120 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | -5 | 100 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | -4 | 80 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | -3 | 60 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | -2 | 40 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | -1 | 20 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 1 | -20 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 |
-20 | 2 | -40 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 3 | -60 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 4 | -80 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 5 | -100 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 6 | -120 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | 7 | -140 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 8 | -160 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 9 | -180 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 10 | -200 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 11 | -220 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 12 | -240 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
-20 | 13 | -260 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | 14 | -280 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 15 | -300 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | 16 | -320 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 1 |
-20 | 17 | -340 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 18 | -360 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
-20 | 19 | -380 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | 20 | -400 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 21 | -420 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 22 | -440 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | 23 | -460 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
-20 | 24 | -480 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
-20 | 25 | -500 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | 26 | -520 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 27 | -540 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 28 | -560 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
-20 | 29 | -580 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 30 | -600 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
-20 | 31 | -620 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 32 | -640 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 |
-20 | 33 | -660 | 1 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 34 | -680 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 35 | -700 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 36 | -720 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 37 | -740 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 38 | -760 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
-20 | 39 | -780 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 40 | -800 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 41 | -820 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 42 | -840 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 |
-20 | 43 | -860 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 44 | -880 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
-20 | 45 | -900 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
-20 | 46 | -920 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
-20 | 47 | -940 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
-20 | 48 | -960 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 49 | -980 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 1 |
Результат команды умножения |
Yonah | Conroe | Coffee Lake | AMD | ||||||||
Core Duo T2450 | Core 2 Duo E6750 | Xeon_E-2278G | AMD Ryzen 7 2700 | ||||||||
i | j | Result | Calc parity | AF | PF | AF | PF | AF | PF | AF | PF |
-20 | -10 | 2 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | -9 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | -8 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | -7 | 2 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | -6 | 3 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | -5 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | -4 | 5 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | -3 | 6 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | -2 | 10 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | -1 | 20 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 1 | -20 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 2 | -10 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 3 | -6 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 4 | -5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 5 | -4 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 6 | -3 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 7 | -2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 8 | -2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 9 | -2 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 10 | -2 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 11 | -1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 12 | -1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 13 | -1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 14 | -1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 15 | -1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 16 | -1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 17 | -1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 18 | -1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 19 | -1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 20 | -1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 21 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 22 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 23 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 24 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 25 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 26 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 27 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 28 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 29 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 30 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 31 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 32 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 33 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 34 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 35 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 36 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 37 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 38 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 39 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 40 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 41 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 42 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 43 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 44 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 45 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 46 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 47 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
-20 | 48 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
-20 | 49 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
Результат команды деления |
Даже по такой небольшой выборке, можно сделать следующие выводы:
Операции умножения различаются на современных и более старых процессорах Intel (до Wolfdale включительно — можно отнести к «старому» поколению, начиная с Sandy Bridge — «новое» (Nehalem мне не попадался)). На «новых» процессорах бит AF всегда устанавливается после умножения в 0, а PF — в соответствии с младшим байтом результата (как и следовало бы делать раньше). Логика же установки битов в «старых» процессорах мне не понятна.
Процессоры AMD выполняют умножение с установкой битов, в точности как «старое» поколение от Intel.
На операциях деления все процессоры Intel устанавливают бит четности одинаково (но он не совпадает с четностью битов последнего байтов результатов деления и остатка по модулю).
Бит AF после деления в процессорах Intel устанавливается в 0, за исключением семейств Yonah и более ранних.
AMD после деления устанавливает AF=1 и PF=0.
Полную таблицу тестов можно скачать
Если у кого-то есть доступ к невошедшим в тестирование процессорам — прошу принять участие.