X86_64 Assembly'a merhaba deyin'in sekinci ve son bölümüdür ve burada assembly içinde tam sayı olmayan sayılarla nasıl çalışılacağına bakacağız. Floating point(kayan nokta) verilerle çalışmanın birkaç yolu vardır.
• fpu
• sse
Öncelikle, floating point(kayan nokta) sayıların hafızada nasıl saklandığına bakalım. Üç floating point(kayan nokta) veri tipi vardır:
• single-precision (tek hassasiyet)
• double-precision(çift hassasiyet)
• double-extended precision(çift genişletilmiş hassasiyet)
Intel'in 64-ia-32-architecture-software-developer-vol-1-manual'inde anlatıldığı gibi:
Bu veri türleri için veri biçimleri doğrudan Binary Floating-Point Arithmetic(İkili Kayan Nokta Aritmetiği) için IEEE Standardı 754'te belirtilen biçimlere karşılık gelir.
Bellekte varolan single-precision floating-point(tek hassasiyetli kayan nokta) float point veriler:
• sign(işaret) - 1 bit
• exponent(üs) - 8 bit
• mantissa(ondalık kısım) - 23 bit
Örneğin, eğer aşağıdaki sayıya sahipsek:
| işaret | üst | mantissa
|---------|-----------|-----------------------------------
| 0 | 00001111 | 110000000000000000000000
Üs −128 ila 127 arasında 8 bit işaretli bir tam sayı veya 0 ila 255 arasında 8 bit işaretsiz bir tam sayıdır.İşaret biti sıfır bu yüzden pozitif bir sayıya sahibiz.Üs 00001111b veya ondalık olarak 15'tir.Tek hassasiyetli yer değiştirme ( single-precision displacement) 127'dir bu, üs - 127 veya 15 - 127 = -112'yi hesaplamamız gerektiği anlamına gelir.Mantissa'nın normalleştirilmiş ikili tamsayı kısmı her zaman bire eşit olduğundan, o zaman mantissa'ya sadece kesirli kısmı kaydedilir, yani mantissa veya sayımız 1.110000000000000000000000 olur.Sonuç değeri:
değer = mantissa * 2^-112
Çift hassasiyetli sayı 64 bit hafızada:
• işaret - 1 bit
• üs - 11 bit
• mantissa - 52 bit
Alabileceğimiz sonuç sayısı:
değer = (-1)^işaret * (1 + mantissa / 2 ^ 52) * 2 ^ üs - 1023)
Genişletilmiş hassasiyet 80 bit sayılarda:
• işaret - 1 bit
• üs - 15 bit
• mantissa - 112 bit
Bu konuda daha fazla okuyun - burada.Basit örneğe bakalım.
X87 Floating-Point Unit (Kayan Nokta Ünitesi), yüksek performanslı floating-point işleme sağlar.Floating point, tam sayı ve packed BCD tam sayı veri türlerini ve floating point işleme algoritmalarını destekler.x87 aşağıdaki komut setini sağlar:
Veri transfer komutları
Basit aritmetik komutlar
Kıyaslama komutları
Transandantal komutlar
Load constant instructions
x87 FPU kontrol komutları
Tabii ki burada x87 tarafından sağlanan tüm komutları görmeyeceğiz, ek bilgi için 64-ia-32-architecture-software-developer-vol-1-manual Bölüm 8'e bakınız. Birkaç veri aktarımı komutu vardır:
FDL - load floating point
FST - store floating point (in ST(0) register)
FSTP - store floating point and pop (in ST(0) register)
Aritmetik komutlar
FADD - floating point ekle
FIADD - tam sayıyı floating point'e ekle
FSUB - floating point çıkar
FISUB - tam sayıyı floating point'den çıkar
FABS - mutlak değer al
FIMUL - floating point ve tam sayıyı çarp
FIDIV - floating point ve tam sayıyı böl
ve diğerleri… FPU, bir ring stack'de düzenlenmiş sekiz adet 10 baytlık registera sahiptir. Stack'in tepesi - register ST (0), diğer registerlar ST (1), ST (2)… ST (7). Genellikle floating poin verileriyle çalışırken kullanıyoruz.
Örneğin:
section .data
x dw 1.0
fld dword [x]
x'in değerini bu stack'e pushlar.Operatör 32bit, 64bit veya 80bit olabilir.Her zamanki gibi stack olarak çalışır, fld ile başka bir değere pushlarsak, x değeri ST (1) 'de ve yeni değer ST (0) olur. FPU komutları bu registerları kullanabilir, örneğin:
; st0 değerini st3 değerine ekler ve st0 içine kaydeder
fadd st0, st3
; x ve y'yi ekler ve st0'a kaydeder
fld dword [x]
fld dword [y]
fadd
Basit bir örneğe bakalım. Daire yarıçapına sahip olacağız ve daire karesini hesaplayacağız ve yazdıracağız:
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
Nasıl çalıştığını anlamaya çalışalım:İlk olarak, önceden tanımlanmış yarıçap verisi ve sonucu depolamak için kullanacağımız sonuç içeren veri bölümü var. Bundan sonra exit system call'u çağırmak için bu 2 sabit.Daha sonra programın giriş noktasını görüyoruz - _start.Burada radius değerini st0 ve st1'de fld komutu ile saklıyoruz ve bu iki değeri fmul komutu ile çarpıyoruz.Bu işlemlerden sonra st0 register'ındaki yarıçap çarpmanın sonucu olacaktır.Ardından fldpi komutuyla π sayısını st0 registerına yüklüyoruz ve sonra yarıçap* yarıçap değeri st1 registerında olacaktır.st0 (pi) ve st1 (yarıçap * yarıçap değeri) üzerinde fmul ile çarpma işlemini gerçekleştirdikten sonra, sonuç st0 registerı olacaktır.Tamam, şimdi st0 register'ında dairenin karesi var ve fstp komutuyla sonucu çıkarabiliriz.Bir sonraki nokta, C fonksiyonuna sonucu aktarmak ve onu çağırmaktır. Hatırlayın, önceki blog yazısında C kodunu assembly kodundan çağırmıştık.X86_64 calling convention bilmemiz gerekiyor.Her zamanki gibi fonksiyon parametrelerini rdi (arg1), rsi (arg2) ve vb. registerları üzerinden geçiririz fakat burada floating point(kayan nokta) verileri var.Burada özel registerlar var: xmm0 - xmm15 sse tarafından sağlanan.Öncelikle, sayıyı xmmN rax registerına koymalı (bizim durumumuz için 0) ve sonucu xmm0 registerına koymalıyız.Şimdi yazdırmak sonucu için C fonksiyonunu çağırabiliriz:
#include <stdio.h>
extern int printResult(double result);
int printResult(double result) {
printf("Circle radius is - %f\n", result);
return 0;
}
Bununla build edebiliriz:
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