Skip to content

Latest commit

 

History

History
175 lines (124 loc) · 10.4 KB

asm_8.md

File metadata and controls

175 lines (124 loc) · 10.4 KB

Это восьмая и последняя часть цикла статей "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

Подробнее почитать расширенной точности можете здесь.

x87 FPU

Модуль 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

И запускаем.

result