Об одной недокументированной особенности умножения и деления на процессорах x86

Начиная с процессора 80286 компания Intel поддерживала полную совместимость «снизу-вверх» в системе команд. То есть если какая-то из команд процессора дает такой-то результат на 8086, то и на более поздних процессорах результат будет точно таким же (сейчас не будем рассматривать ошибки типа неправильного деления в Pentium I).

Но так ли это? Что за вопрос! Ведь если бы совместимость не сохранялась, то старые программы не могли бы выполняться, а ведь до сих пор на любом компьютере можно поностальгировать запустив Norton Commander или Tetris. Однако не все так просто… Начиная с 8080 в процессорах Intel есть регистр флагов, состояние которого определяется результатом последней команды вычисления данных. Все флаги в нем давно описаны и поведение их строго зафиксировано. Кроме двух исключений.

02dcdf266b81887ab511c1dc37f12394.jpg

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

Результат команды деления

Даже по такой небольшой выборке, можно сделать следующие выводы:

  1. Операции умножения различаются на современных и более старых процессорах Intel (до Wolfdale включительно — можно отнести к «старому» поколению, начиная с Sandy Bridge — «новое» (Nehalem мне не попадался)). На «новых» процессорах бит AF всегда устанавливается после умножения в 0, а PF — в соответствии с младшим байтом результата (как и следовало бы делать раньше). Логика же установки битов в «старых» процессорах мне не понятна.

  2. Процессоры AMD выполняют умножение с установкой битов, в точности как «старое» поколение от Intel.

  3. На операциях деления все процессоры Intel устанавливают бит четности одинаково (но он не совпадает с четностью битов последнего байтов результатов деления и остатка по модулю).

  4. Бит AF после деления в процессорах Intel устанавливается в 0, за исключением семейств Yonah и более ранних.

  5. AMD после деления устанавливает AF=1 и PF=0.

Полную таблицу тестов можно скачать

Если у кого-то есть доступ к невошедшим в тестирование процессорам — прошу принять участие.

© Habrahabr.ru