Это восьмая и последняя часть цикла статей "Say hello to x86_64 Assembly", и здесь мы рассмотрим, как работать с нецелыми числами в ассемблере. Есть несколько способов работы с данными с плавающей точкой:
- fpu
- sse
Сначала давайте посмотрим, как числа с плавающей точкой хранятся в памяти. Существует три типа данных с плавающей точкой:
- одинарной точности
- двойной точности
- двойной расширенной точности
Как описано в руководстве Intel 64-IA-32-architecture-software-developer-vol-1-manual
:
Форматы данных для этих типов данных напрямую соответствуют форматам, указанным в стандарте IEEE 754 для двоичной арифметики с плавающей точкой.
Данные типа float(с плавающей точкой) одинарной точности, представленны в памяти следующим образом:
- знак - 1 bit
- экспонента - 8 bits
- мантисса - 23 bits
Например, если у нас есть следующее число:
| sign | exponent | mantissa
|--------- |----------|-------------------------
| 0 | 00001111 | 110000000000000000000000
Экспонента — это либо 8-битное целое число со знаком, принимающе значения от −128 до 127, либо 8-битное целое число без знака, принимающее значения от 0 до 255. Знаковый бит равен нулю, поэтому у нас положительное число. Экспонента равна 00001111b или 15 в десятичной системе счисления. Для смещения одинарной точности, число равно 127, что значит, что нам нужно вычислить экспоненту - 127 или 15 - 127 = -112. Поскольку нормализованная двоичная целая часть мантиссы всегда равна единице, то в мантиссе записывается только ее дробная часть, поэтому мантисса или наше число равно 1,11000000000000000000000000. Значение результата будет:
value = mantissa * 2^-112
Число двойной точности — это 64 бита памяти, где:
- знак - 1 bit
- экспонента - 11 bit
- мантисса - 52 bit
Результат мы можем получить с помощью следующего выражения:
value = (-1)^sign * (1 + mantissa / 2 ^ 52) * 2 ^ exponent - 1023)
Повышенная точность — это 80-битные числа, где:
- знак - 1 bit
- экспонента - 15 bit
- мантисса - 112 bit
Подробнее почитать расширенной точности можете здесь.
Модуль Floating-Point Unit (FPU) x87 обеспечивает высокопроизводительную обработку с плавающей точкой. Он поддерживает типы данных с плавающей точкой, целые числа и упакованные целые числа BCD, а также алгоритмы обработки с аргумента в виде числ с плавающей точкой. x87 предоставляет следующий набор инструкций:
- Инструкции по передаче данных
- Базовые арифметические инструкции
- Инструкции сравнения
- Трансцендентные инструкции
- Инструкции по загрузке констант
- Инструкции управления FPU x87
Конечно, мы не увидим здесь всех инструкций, предоставляемых набором инструкций x87, для получения дополнительной информации см. 64-ia-32-architecture-software-developer-vol-1-manual, Глава 8
. Есть несколько инструкций по передаче данных:
FDL
- загрузка плавающей точкиFST
- сохранение плавающей точки (в регистре ST(0))FSTP
- сохранение плавающей точки и извлечение (в регистре ST(0))
Арифметические инструкции:
FADD
- сложение чисел с плавающей точкойFIADD
- сложение целого числа с число с плавающей точкойFSUB
- вычитание чисел с плавающей точкойFISUB
- вычитание целого числа из числа с плавающей точкойFABS
- получение абсолютного значенияFIMUL
- умножение целого числа и числа с плавающей точкойFIDIV
- деление целого числа и числа с плавающей точкой
и т. д... FPU имеет восемь 10-байтовых регистров, организованных в кольцевой стек. Верх стека - регистр ST(0), остальные регистры - ST(1), ST(2) ... ST(7). Обычно мы используем их, когда работаем с данными с плавающей точкой.
К примеру следующий код
section .data
x dw 1.0
fld dword [x]
помещает значение x в этот стек. Оператор может быть 32, 64 или 80-битным. Этот стек работает также как и обычный стек, если мы помещаем другое значение с помощью fld
, значение x будет в ST(1), а новое значение будет в ST(0). Инструкции FPU
могут использовать эти регистры, например:
;;
;; adds st0 value to st3 and saves it in st0
;;
fadd st0, st3
;;
;; adds x and y and saves it in st0
;;
fld dword [x]
fld dword [y]
fadd
Давайте рассмотрим на простой пример. У нас есть радиус круга, а мы хотим вычислить площадь круга и вывести ее на печать:
extern printResult
section .data
radius dq 1.7
result dq 0
SYS_EXIT equ 60
EXIT_CODE equ 0
global _start
section .text
_start:
fld qword [radius]
fld qword [radius]
fmul
fldpi
fmul
fstp qword [result]
mov rax, 0
movq xmm0, [result]
call printResult
mov rax, SYS_EXIT
mov rdi, EXIT_CODE
syscall
Давайте попробуем понять, как код работает: прежде всего, есть раздел данных с предопределенными данными радиуса и результатом, который мы будем использовать для сохранения результата.
Дальше идут 2 константы для вызова системного вызова exit
. Далее мы видим точку входа программы - _start. Там мы сохраняем значение радиуса в st0
и st1
с помощью инструкции fld
и умножаем эти два значения с помощью инструкции fmul
. После этих операций у нас будет результат умножения радиуса на радиус(возведение радиуса в квадрат) в регистре st0
. Затем мы загружаем число π с помощью инструкции fldpi
в регистр st0
, а после этого значение квадрата радиуса будет в регистре st1
.
После этого выполняем умножение с помощью fmul
на st0
(пи) и st1
(значение радиуса * радиус), результат будет в регистре st0
.
Хорошо, теперь у нас есть квадрат радиуса в регистре st0
и мы можем извлечь его с помощью инструкции fstp
в результат. Следующее действие - передать результат в функцию C и вызвать ее. Помните, как мы вызывали функцию C из ассемблерного кода в предыдущей статье блога. Нам нужно знать соглашение о вызовах x86_64
. Обычно мы передаем параметры функции через регистры rdi
(arg1), rsi
(arg2) и т. д., но здесь данные с плавающей точкой. Существуют специальные регистры: xmm0
- xmm15
, предоставляемые набором инструкций sse
. Для начала нам нужно поместить номер регистра xmmN
в регистр rax
(0 в нашем случае), а результат поместить в регистр xmm0
. Теперь мы можем вызвать функцию C для печати результата:
#include <stdio.h>
extern int printResult(double result);
int printResult(double result) {
printf("Circle radius is - %f\n", result);
return 0;
}
Компилируем и собирем исполняемый файл с помощью команд:
build:
gcc -g -c circle_fpu_87c.c -o c.o
nasm -f elf64 circle_fpu_87.asm -o circle_fpu_87.o
ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -lc circle_fpu_87.o c.o -o testFloat1
clean:
rm -rf *.o
rm -rf testFloat1
И запускаем.