V předchozím článku věnovaném použití assembleru v Linuxu jsme se seznámili se základními vlastnosti matematického koprocesoru využívaného na platformách i386 a x86-64. Dnes si vyzkoušíme aplikaci některých vybraných instrukcí v trojici demonstračních příkladů. Ukážeme si i některé mezní případy a výjimky, které mohou při výpočtech nastat – dělení nulou atd.
Obsah
1. Použití assembleru v Linuxu: práce s matematickým koprocesorem (pokračování)
2. Načtení FP konstanty do registru mikroprocesoru s následným uložením konstanty do paměti
3. První demonstrační příklad: vytištění hodnot 0.0, 1.0 a Pi v hexadecimálním tvaru
4. Jak přečíst a dekódovat vytištěné výsledky?
5. Pomocný program pro převod FPU hodnot do jejich hexadecimální podoby
6. Základní aritmetické operace v praxi
7. Složitější výrazy a práce se zásobníkem operandů
8. Druhý demonstrační příklad: základní aritmetické operace
9. Dělení kladnou a zápornou nulou
10. Dělení nuly nulou aneb práce s NaN
11. Třetí demonstrační příklad: dělení nulou
13. Repositář s demonstračními příklady
1. Použití assembleru v Linuxu: práce s matematickým koprocesorem (pokračování)
V předchozí části seriálu o použití assembleru v Linuxu jsme se seznámili se základními koncepty, na nichž je postaven matematický koprocesor používaný na architekturách i386 a x86-64. Připomeňme si, že matematický koprocesor obsahuje osm pracovních registrů, každý o šířce osmdesáti bitů. Tyto registry jsou doplněny o řídicí registr a stavový registr. Dnes si ve třech demonstračních příkladech ukážeme, jakým způsobem je možné matematický koprocesor použít pro základní výpočty, zejména pro aritmetické operace. Taktéž si ukážeme, co se stane při dělení nenulového čísla nulou (což je zcela legální operace) i při pokusu o dělení nuly nulou. Demonstrační příklady jsou primárně odladěny pro 32bitovou platformu i386 (běží ovšem samozřejmě i v 64bitovém systému), ovšem po nepatrné úpravě je lze použít i v čistém 64bitovém režimu.
2. Načtení FP konstanty do registru mikroprocesoru s následným uložením konstanty do paměti
Matematický koprocesor obsahuje několik instrukcí, které je možné použít pro uložení konstanty do pracovního registru procesoru. Mezi základní konstanty patří kladná nula +0,0, kladná jednička +1,0, konstanta π a taktéž na první pohled možná poněkud zbytečné, ale v praxi používané konstanty log210, log2e, log102 a loge2 (u některých výpočtů totiž může být výhodné nejprve operandy zlogaritmovat a posléze je namísto násobení pouze sečíst atd.). Všechny instrukce, které do pracovního registru matematického koprocesoru, konkrétně do registru, jenž aktuálně leží na vrcholu zásobníku, uloží příslušnou konstantu, jsou vypsány v následující tabulce:
# | Instrukce | Význam |
---|---|---|
1 | FLDZ | načtení konstanty +0,0 (kladná nula) |
2 | FLD1 | načtení konstanty +1,0 |
3 | FLDPI | načtení konstanty π |
4 | FLDL2T | načtení konstanty log210 |
5 | FLDL2E | načtení konstanty log2e |
6 | FLDLG2 | načtení konstanty log102 |
7 | FLDLN2 | načtení konstanty loge2 |
Jakým způsobem se však přesvědčíme o tom, že skutečně došlo k uložení zvolené konstanty do vybraného pracovního registru matematického koprocesoru? Kupodivu není k dispozici žádná instrukce, která by data přímo převedla z matematického koprocesoru do hlavního procesoru, ovšem můžeme využít toho, že obsah libovolného pracovního registru matematického koprocesoru je možné převést do podporovaného formátu (single, double, extended) a uložit zkonvertovanou hodnotu na zvolenou adresu operační paměti. Z této adresy se pak může hodnota načíst do pracovního registru hlavního procesoru a následně se tato hodnota může vypsat například v hexadecimálním tvaru, což již umíme, neboť jsme se touto problematikou již zabývali v předchozích článcích.
Nejdříve je nutné pojmenovat adresu, na kterou se budou hodnoty ukládat. Pro jednoduchost se bude jednat o čtyři bajty alokované v sekci BSS (tato sekce není součástí přeloženého binárního souboru). Čtyři bajty jsou zvoleny proto, že se původně 80bitová hodnota převede do formátu single (číslo s plovoucí řádovou čárkou s jednoduchou přesností). Pro pojmenování (label) zvolíme jméno number:
.section .bss
.lcomm number, 4 # na toto misto se bude ukladat konstanta typu float
Samotná sekvence instrukcí určených pro vložení konstanty do pracovního registru matematického koprocesoru, následné uložení této hodnoty do operační paměti, načtení uložené hodnoty do pracovního registru hlavního procesoru a vypsání hodnoty v hexadecimální tvaru vypadá následovně:
fldz # nacteni FP konstanty 0.0
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
Povšimněte si, že při ukládání do operační paměti musíme přes dword ptr přesně určit způsob konverze (zde na čtyři bajty = single). Totéž platí při zpětném načítání hodnoty do 32bitového pracovního registru EAX. Následně voláme makro printHexNumber, jehož zdrojový kód naleznete ve dvanácté kapitole.
Poznámka: instrukce FSTP provádí kombinaci operací FST+POP, tedy konverzi a uložení obsahu pracovního registru následovanou odstraněním hodnoty ze zásobníku (což se provádí pouhým posunem indexu vrcholu zásobníku o jedničku).
3. První demonstrační příklad: vytištění hodnot 0.0, 1.0 a Pi v hexadecimálním tvaru
Dnešní první demonstrační příklad je založen na sekvenci tří instrukcí FLDX, FSTP adresa a MOV eax, adresa následovaných voláním makra printHexNumber. Tímto způsobem jsou postupně vypsány hodnoty +0,0, +1,0 a π, a to v hexadecimálním tvaru. Nejprve se podívejme na zdrojový kód, který tvoří hlavní část programu, zbylé tři moduly jsou opět popsány ve dvanácté kapitole:
# asmsyntax=as
# Program pro otestovani zakladnich FP operaci
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky
.intel_syntax noprefix
# Nacteni definice makra pro ukonceni aplikace
.include "exit.s"
# Nacteni maker pro (opakovany) tisk zpravy i prislusne subrutiny
.include "writeMessage.s"
# Nacteni makra pro vytisteni hexadecimalni 32bitove hodnoty
# spolecne s makrem je nactena i prislusna subrutina
.include "printHexNumber.s"
#-----------------------------------------------------------------------------
.section .data
fpuValueZeroMessage:
.string "0.0: " # zprava
fpuValueZeroMessageLength = $ - fpuValueZeroMessage # delka zpravy
fpuValueOneMessage:
.string "1.0: " # zprava
fpuValueOneMessageLength = $ - fpuValueOneMessage # delka zpravy
fpuValuePiMessage:
.string "Pi: " # zprava
fpuValuePiMessageLength = $ - fpuValuePiMessage # delka zpravy
#-----------------------------------------------------------------------------
.section .bss
.lcomm number, 4 # na toto misto se bude ukladat konstanta typu float
#-----------------------------------------------------------------------------
.section .text
.global _start # tento symbol ma byt dostupny i linkeru
_start:
writeMessage fpuValueZeroMessage, fpuValueZeroMessageLength
fldz # nacteni FP konstanty 0.0
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
writeMessage fpuValueOneMessage, fpuValueOneMessageLength
fld1 # nacteni FP konstanty 1.0
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
writeMessage fpuValuePiMessage, fpuValuePiMessageLength
fldpi # nacteni FP konstanty Pi
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
println # odradkovani
exit # ukonceni aplikace
Překlad a slinkování se provede následujícím způsobem:
as -g --32 main.s -o main.o
ld -m elf_i386 -s main.o
Výsledkem je binární soubor spustitelný jak na 32bitovém, tak i na 64bitovém systému.
Pro překlad upraveného kódu určeného pouze pro 64bitový systém použijte příkazy:
as -g main_x86_64.s -o main.o
ld -s main.o
Pro zajímavost se podívejme na to, jakým způsobem se FPU instrukce přeložily do nativního kódu:
32bitový systém i386:
00000026 <_start>:
26: b9 1a 00 00 00 mov ecx,0x1a
2b: ba 06 00 00 00 mov edx,0x6
30: e8 cb ff ff ff call 0 <write_message>
<strong> 35: d9 ee fldz </strong>
<strong> 37: d9 1d 00 00 00 00 fstp DWORD PTR ds:0x0</strong>
3d: a1 00 00 00 00 mov eax,ds:0x0
42: 60 pusha
43: 89 c2 mov edx,eax
45: bb 10 00 00 00 mov ebx,0x10
4a: e8 be ff ff ff call d <hex2string>
4f: b9 02 00 00 00 mov ecx,0x2
54: ba 18 00 00 00 mov edx,0x18
59: e8 a2 ff ff ff call 0 <write_message>
5e: 61 popa
5f: b9 20 00 00 00 mov ecx,0x20
64: ba 06 00 00 00 mov edx,0x6
69: e8 92 ff ff ff call 0 <write_message>
<strong> 6e: d9 e8 fld1 </strong>
<strong> 70: d9 1d 00 00 00 00 fstp DWORD PTR ds:0x0</strong>
76: a1 00 00 00 00 mov eax,ds:0x0
7b: 60 pusha
7c: 89 c2 mov edx,eax
7e: bb 10 00 00 00 mov ebx,0x10
83: e8 85 ff ff ff call d <hex2string>
88: b9 02 00 00 00 mov ecx,0x2
8d: ba 18 00 00 00 mov edx,0x18
92: e8 69 ff ff ff call 0 <write_message>
97: 61 popa
98: b9 26 00 00 00 mov ecx,0x26
9d: ba 06 00 00 00 mov edx,0x6
a2: e8 59 ff ff ff call 0 <write_message>
<strong> a7: d9 eb fldpi </strong>
<strong> a9: d9 1d 00 00 00 00 fstp DWORD PTR ds:0x0</strong>
af: a1 00 00 00 00 mov eax,ds:0x0
b4: 60 pusha
b5: 89 c2 mov edx,eax
b7: bb 10 00 00 00 mov ebx,0x10
bc: e8 4c ff ff ff call d <hex2string>
c1: b9 02 00 00 00 mov ecx,0x2
c6: ba 18 00 00 00 mov edx,0x18
cb: e8 30 ff ff ff call 0 <write_message>
d0: 61 popa
d1: b9 00 00 00 00 mov ecx,0x0
d6: ba 02 00 00 00 mov edx,0x2
db: e8 20 ff ff ff call 0 <write_message>
e0: b8 01 00 00 00 mov eax,0x1
e5: bb 00 00 00 00 mov ebx,0x0
ea: cd 80 int 0x80
64bitový systém x86-64:
0000000000000028 <_start>:
28: b9 00 00 00 00 mov ecx,0x0
2d: ba 06 00 00 00 mov edx,0x6
32: e8 c9 ff ff ff call 0 <write_message>
<strong> 37: d9 ee fldz </strong>
<strong> 39: d9 1c 25 00 00 00 00 fstp DWORD PTR ds:0x0</strong>
40: 8b 04 25 00 00 00 00 mov eax,DWORD PTR ds:0x0
47: 50 push rax
48: 53 push rbx
49: 51 push rcx
4a: 52 push rdx
4b: 89 c2 mov edx,eax
4d: bb 00 00 00 00 mov ebx,0x0
52: e8 b6 ff ff ff call d <hex2string>
57: b9 00 00 00 00 mov ecx,0x0
5c: ba 18 00 00 00 mov edx,0x18
61: e8 9a ff ff ff call 0 <write_message>
66: 5a pop rdx
67: 59 pop rcx
68: 5b pop rbx
69: 58 pop rax
6a: b9 00 00 00 00 mov ecx,0x0
6f: ba 06 00 00 00 mov edx,0x6
74: e8 87 ff ff ff call 0 <write_message>
<strong> 79: d9 e8 fld1 </strong>
<strong> 7b: d9 1c 25 00 00 00 00 fstp DWORD PTR ds:0x0</strong>
82: 8b 04 25 00 00 00 00 mov eax,DWORD PTR ds:0x0
89: 50 push rax
8a: 53 push rbx
8b: 51 push rcx
8c: 52 push rdx
8d: 89 c2 mov edx,eax
8f: bb 00 00 00 00 mov ebx,0x0
94: e8 74 ff ff ff call d <hex2string>
99: b9 00 00 00 00 mov ecx,0x0
9e: ba 18 00 00 00 mov edx,0x18
a3: e8 58 ff ff ff call 0 <write_message>
a8: 5a pop rdx
a9: 59 pop rcx
aa: 5b pop rbx
ab: 58 pop rax
ac: b9 00 00 00 00 mov ecx,0x0
b1: ba 06 00 00 00 mov edx,0x6
b6: e8 45 ff ff ff call 0 <write_message>
<strong> bb: d9 eb fldpi </strong>
<strong> bd: d9 1c 25 00 00 00 00 fstp DWORD PTR ds:0x0</strong>
c4: 8b 04 25 00 00 00 00 mov eax,DWORD PTR ds:0x0
cb: 50 push rax
cc: 53 push rbx
cd: 51 push rcx
ce: 52 push rdx
cf: 89 c2 mov edx,eax
d1: bb 00 00 00 00 mov ebx,0x0
d6: e8 32 ff ff ff call d <hex2string>
db: b9 00 00 00 00 mov ecx,0x0
e0: ba 18 00 00 00 mov edx,0x18
e5: e8 16 ff ff ff call 0 <write_message>
ea: 5a pop rdx
eb: 59 pop rcx
ec: 5b pop rbx
ed: 58 pop rax
ee: b9 00 00 00 00 mov ecx,0x0
f3: ba 02 00 00 00 mov edx,0x2
f8: e8 03 ff ff ff call 0 <write_message>
fd: b8 01 00 00 00 mov eax,0x1
102: bb 00 00 00 00 mov ebx,0x0
107: cd 80 int 0x80
V obou případech si povšimněte, že všechny FPU operace začínají prefixovým bajtem s hodnotou D9, za nímž u jednodušších instrukcí následuje jediný bajt s operačním kódem a u instrukcí FLD a FST navíc ještě adresa ze které se mají přečíst data popř. na kterou se mají data naopak uložit.
4. Jak přečíst a dekódovat vytištěné výsledky?
Pokud přeložený program spustíme, měly by se na standardní výstup vypsat následující tři řádky:
0.0: Hex value: 0x00000000
1.0: Hex value: 0x3F800000
Pi: Hex value: 0x40490FDB
Vidíme, že jsme pro každou hodnotu reprezentovanou v systému plovoucí řádové čárky dostali hexadeciální 32bitové číslo, které reprezentuje binární „otisk“ 32bitové hodnoty typu single. S formátem single jsme se seznámili v předchozím článku, takže jen stručně:
bit | 31 | 30 29 ... 24 23 | 22 21 ... 3 2 1 0 |
---|---|---|---|
význam | s | exponent (8 bitů) | mantisa (23 bitů) |
Exponent je posunutý o bias nastavený na hodnotu 127. Mantisa u normalizovaných čísel obsahuje jen čísla za (binární) řádovou čárkou, tudíž je k hodnotě mantisy nutné přičíst jedničku. Získané hodnoty tedy můžeme dekódovat:
Hexa | Binárně | s | Exponent | Mantisa |
---|---|---|---|---|
0x00000000 | 00000000000000000000000000000000 | + | 0 (spec) | 0 |
0x3F800000 | 00111111100000000000000000000000 | + | 127-127=0 | 1,0 + 0,0 |
0x40490FDB | 01000000010010010000111111011011 | + | 128-127=1 | 1,0 + 0,57079637050628662109375 |
První hodnota je zcela jednoznačně kladná nula, druhá hodnota je rovna 1,0×20=1, třetí hodnota je pak rovna 1,57079637050628662109375×21=π (zde konkrétně přibližná hodnota 3.14159274101257324218750). Vidíme, že jak způsob uložení hodnot, tak i jejich zpětné ruční dekódování pracuje spolehlivě.
5. Pomocný program pro převod FPU hodnot do jejich hexadecimální podoby
Hodnoty samozřejmě není nutné převádět ručně. Pro zpětný převod FP hodnot, tedy hodnot reprezentovaných v systému plovoucí řádové čárky do hexadecimální 32bitové reprezentace je možné použít i následující jednoduchý program napsaný v céčku. Pokud při spuštění programu specifikujete na příkazové řádce FP hodnotu, například 0.5, vypíše se ihned její obraz v paměti, tj. to, jak je číslo interně reprezentováno (logiku programu lze v případě potřeby jednoduše i obrátit tak, aby převádět hexadecimální reprezentaci na FP hodnotu):
#include <stdio.h>
#include <stdlib.h>
union {
float flt;
int hex;
} float_hex;
int main(int argc, char **argv)
{
if (argc == 2) {
float_hex.flt = atof(argv[1]);
printf("%08x\n", float_hex.hex);
}
return 0;
}
Funkci programu si můžeme jednoduše odzkoušet:
<strong>gcc -o fp2hex fp2hex.c </strong>
<strong>./fp2hex 0</strong>
00000000
<strong>./fp2hex 1</strong>
3f800000
<strong>./fp2hex -1</strong>
bf800000
<strong>./fp2hex 3.14</strong>
40490fdb
6. Základní aritmetické operace v praxi
Minule jsme se taktéž seznámili s instrukcemi pro základní aritmetické operace. Jedná se o tyto instrukce:
# | Instrukce | Význam |
---|---|---|
1 | FADD | součet |
2 | FSUB | rozdíl |
3 | FSUBR | rozdíl, ale operandy jsou prohozeny |
4 | FMUL | součin |
5 | FDIV | podíl |
6 | FDIVR | podíl, ale operandy jsou prohozeny |
U všech těchto instrukcí lze navíc specifikovat příponu P, podobně jako u FST/FSTP. Pokud je tato přípona uvedena, budou ze zásobníku tvořeném pracovními registry odstraněny oba vstupní operandy, a teprve až poté se na zásobník uloží výsledek aritmetické operace. To mj. znamená, že součet dvou hodnot (zde konkrétně součet dvou jedniček) je možné implementovat následujícím způsobem:
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
faddp # soucet obou hodnot (1.0+1.0)
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
Ukázka použití základních aritmetických operací tvoří základ pro dnešní druhý demonstrační příklad, jehož zdrojový kód bude uveden v osmé kapitole.
7. Složitější výrazy a práce se zásobníkem operandů
Způsob načtení konstant do pracovních registrů (a tím pádem i do zásobníku) matematického koprocesoru již známe, takže si nyní již můžeme ukázat, jak se provádí základní aritmetické operace. Začneme podobně jako žáčci v první třídě – součtem dvou jedniček. To se v assembleru provede velmi jednoduše: nejdříve se na vrchol zásobníku, tj. do pracovního registru st(0), uloží první konstanta 1.0, a ve druhém kroku se na posunutý vrchol zásobníku (tj. do sousedního pracovního registru) uloží druhá konstanta 1.0. V kroku třetím se provede instrukce FADDP, která nejenže obě hodnoty uložené na vrcholu zásobníku a těsně pod ním sečte, ale navíc je ještě ze zásobníku odstraní (odstranění druhého operandu je zajištěno použitím FADDP namísto FADD). Celý výpočet tedy může vypadat takto:
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
faddp # soucet obou hodnot (1.0+1.0)
Podívejme se nyní na součet tří hodnot. Zde můžeme postupovat několika způsoby, z nichž nejjednodušší způsob spočívá v uložení všech tří hodnot na zásobník s následným použitím dvojice instrukcí FADDP. První instrukce sečte poslední dvě hodnoty uložené na zásobníku, takže jeho nový obsah bude [1.0, 2.0], druhá instrukce pak sečte 1.0+2.0 s uložením celkového výsledku zpět:
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
faddp
faddp # soucet vsech tri hodnot (1.0+(1.0+1.0))
Pokud budeme chtít vynásobit hodnoty 2.0 a 3.0, můžeme oba výpočty spojit dohromady a díky tomu, že zásobník má kapacitu pro osm hodnot, vyhneme se jakýmkoli přesunům dat mezi pracovními registry
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
faddp # soucet obou hodnot (1.0+1.0)
# nyni je na zasobniku ulozena hodnota 2
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
faddp
faddp # soucet vsech tri hodnot (1.0+(1.0+1.0))
# vysledek 3.0 je ulozen zpet na zasobnik
fmulp # nyni jsou na zasobniku ulozeny hodnoty 2 a 3 ktere vynasobime
fstp dword ptr number # ulozeni do pameti (4 bajty)
8. Druhý demonstrační příklad: základní aritmetické operace
Ve druhém demonstračním příkladu je ukázán způsob práce se zásobníkem tvořeným osmi pracovními registry matematického koprocesoru. Nejprve je vytištěna hodnota 1.0 způsobem, který již známe z předchozího příkladu. Následně je proveden součet dvou hodnot 1.0 s vytištěním výsledku této operace. Poslední část příkladu provádí výpočet popsaný v předchozí kapitole, tj. 2.0*3.0, ve skutečnosti se však počítá (1.0+1.0)*(1.0+(1.0+1.0)). Po překladu a spuštění by se měly na standardním výstupu objevit následující řádky:
1.0: Hex value: 0x3F800000
1.0+1.0: Hex value: 0x40000000
2.0*3.0: Hex value: 0x40C00000
Zdrojový kód druhého demonstračního příkladu vypadá takto:
# asmsyntax=as
# Program pro otestovani zakladnich FPU operaci
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky
.intel_syntax noprefix
# Nacteni definice makra pro ukonceni aplikace
.include "exit.s"
# Nacteni maker pro (opakovany) tisk zpravy i prislusne subrutiny
.include "writeMessage.s"
# Nacteni makra pro vytisteni hexadecimalni 32bitove hodnoty
# spolecne s makrem je nactena i prislusna subrutina
.include "printHexNumber.s"
#-----------------------------------------------------------------------------
.section .data
fpuValueOneMessage:
.string "1.0: " # zprava
fpuValueOneMessageLength = $ - fpuValueOneMessage # delka zpravy
fpuAddResultMessage:
.string "1.0+1.0: " # zprava
fpuAddResultMessageLength = $ - fpuAddResultMessage # delka zpravy
fpuMulResultMessage:
.string "2.0*3.0: " # zprava
fpuMulResultMessageLength = $ - fpuMulResultMessage # delka zpravy
#-----------------------------------------------------------------------------
.section .bss
.lcomm number, 4 # na toto misto se bude ukladat konstanta typu float
#-----------------------------------------------------------------------------
.section .text
.global _start # tento symbol ma byt dostupny i linkeru
_start:
writeMessage fpuValueOneMessage, fpuValueOneMessageLength
fld1 # nacteni FP konstanty 1.0
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
writeMessage fpuAddResultMessage, fpuAddResultMessageLength
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
faddp # soucet obou hodnot (1.0+1.0)
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
writeMessage fpuMulResultMessage, fpuMulResultMessageLength
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
faddp # soucet obou hodnot (1.0+1.0)
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
fld1 # nacteni FP konstanty 1.0
faddp
faddp # soucet vsech tri hodnot (1.0+(1.0+1.0))
fmulp # nyni jsou na zasobniku ulozeny hodnoty 2 a 3 ktere vynasobime
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
println # odradkovani
exit # ukonceni aplikace
9. Dělení kladnou a zápornou nulou
V předchozím demonstračním příkladu jsme si mj. ukázali instrukci FMULP sloužící k vynásobení obou vstupních operandů. Dá se tedy předpokládat, že bude existovat i opačná instrukce určená pro dělení operandů. Taková instrukce skutečně existuje a její varianty se jmenují FDIV a FDIVP. Samotné dělení je prováděno obvyklým způsobem, ovšem zaměřme se nyní na to, co se stane, pokud se provádí dělení nulou. Ve formátech single i double je znaménko reprezentováno samostatným bitem, což mj. znamená, že existuje kladná a záporná nula.
Dělení kladnou nulou je jednoduché a lze ho realizovat například takto:
fld1 # nacteni FP konstanty 1.0
fldz # nacteni FP konstanty 0.0
fdivp # deleni nulou
Dělení zápornou nulou vyžaduje použití instrukce FCHS, která otočí znaménko svého operandu (zde konkrétně oné nuly):
fld1 # nacteni FP konstanty 1.0
fldz # nacteni FP konstanty 0.0
fchs # zmena znamenka nuly
fdivp # deleni zapornou nulou
Jak budou vypadat výsledky dělení?
1/0: Hex value: 0x7F800000
-1/0: Hex value: 0xFF800000
Po převodu na binární tvar a rozdělení bitových polí:
Hexa | Binárně | s | Exponent | Mantisa |
---|---|---|---|---|
0x7F800000 | 01111111100000000000000000000000 | + | 255 (spec) | 0 |
0xFF800000 | 11111111100000000000000000000000 | - | 255 (spec) | 0 |
Oba výsledky se od sebe liší jen nejvyšším bitem, což je znaménko. Dále následuje osm bitů exponentu. Tyto bity jsou v obou případech nastaveny na samé jedničky, samotný exponent je pak nulový. To odpovídá tabulce speciálních hodnot, s níž jsme se seznámili minule:
s-bit | exponent | mantisa | význam | šestnáctkově |
---|---|---|---|---|
0 | 255 | 0 | kladné nekonečno | 0x7F800000 |
1 | 255 | 0 | záporné nekonečno | 0xFF800000 |
10. Dělení nuly nulou aneb práce s NaN
Ještě jsme si však nevyzkoušeli další speciální případ – dělení 0/0. I to lze realizovat velmi snadno následujícími třemi instrukcemi:
fldz # nacteni FP konstanty 0.0
fldz # nacteni FP konstanty 0.0
fdivp # vypocet 0.0/0.0
Výsledkem bude hodnota:
0/0: Hex value: 0xFFC00000
Po převodu na binární tvar a rozdělení bitových polí:
Hexa | Binárně | s | Exponent | Mantisa |
---|---|---|---|---|
0xFFC00000 | 11111111110000000000000000000000 | + | 255 (spec) | dva nejvyšší bity jednička |
Opět nahlédněme do tabulky zveřejněné minule, abychom zjistili, co tato hodnota znamená:
s-bit | exponent | mantisa | význam |
---|---|---|---|
0 | 255 | >0 | NaN – not a number |
1 | 255 | >0 | NaN – not a number |
Vidíme, že vydělením nuly nulou (ať již kladnou či zápornou) získáme speciální hodnotu NaN. Většina dalších instrukcí sice NaN může akceptovat, ale výsledkem operace bude opět NaN, na což je zapotřebí dávat při programování (nejenom v assembleru) pozor.
11. Třetí demonstrační příklad: dělení nulou
Dělení nenulové hodnoty kladnou i zápornou nulou jakož i dělení nuly nulou je ukázáno v dnešním třetím a současně i posledním demonstračním příkladu, jehož zdrojový kód je zobrazen pod tímto odstavcem. Ve všech třech případech se pro dělení používá instrukce FDIVP:
# asmsyntax=as
# Program pro otestovani deleni nulou
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky
.intel_syntax noprefix
# Nacteni definice makra pro ukonceni aplikace
.include "exit.s"
# Nacteni maker pro (opakovany) tisk zpravy i prislusne subrutiny
.include "writeMessage.s"
# Nacteni makra pro vytisteni hexadecimalni 32bitove hodnoty
# spolecne s makrem je nactena i prislusna subrutina
.include "printHexNumber.s"
#-----------------------------------------------------------------------------
.section .data
fpuDivideByZeroMessage:
.string "1/0: " # zprava
fpuDivideByZeroMessageLength = $ - fpuDivideByZeroMessage # delka zpravy
fpuDivideByNegativeZeroMessage:
.string "-1/0: " # zprava
fpuDivideByNegativeZeroMessageLength = $ - fpuDivideByNegativeZeroMessage # delka zpravy
fpuDivideZeroByZeroMessage:
.string "0/0: " # zprava
fpuDivideZeroByZeroMessageLength = $ - fpuDivideZeroByZeroMessage # delka zpravy
#-----------------------------------------------------------------------------
.section .bss
.lcomm number, 4 # na toto misto se bude ukladat konstanta typu float
#-----------------------------------------------------------------------------
.section .text
.global _start # tento symbol ma byt dostupny i linkeru
_start:
writeMessage fpuDivideByZeroMessage, fpuDivideByZeroMessageLength
fld1 # nacteni FP konstanty 1.0
fldz # nacteni FP konstanty 0.0
fdivp # deleni nulou
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
writeMessage fpuDivideByNegativeZeroMessage, fpuDivideByNegativeZeroMessageLength
fld1 # nacteni FP konstanty 1.0
fldz # nacteni FP konstanty 0.0
fchs # zmena znamenka nuly
fdivp # deleni zapornou nulou
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
writeMessage fpuDivideZeroByZeroMessage, fpuDivideZeroByZeroMessageLength
fldz # nacteni FP konstanty 0.0
fldz # nacteni FP konstanty 0.0
fdivp # vypocet 0.0/0.0
fstp dword ptr number # ulozeni do pameti (4 bajty)
mov eax, dword ptr number # nacteni hodnoty, tentokrat to celociselneho registru
printHexNumber eax # vytiskneme celociselnou hodnotu v hexa tvaru
println # odradkovani
exit # ukonceni aplikace
12. Pomocné zdrojové soubory
Demonstrační příklady popsané v předchozích kapitolách používají makra a subrutiny (procedury), s nimiž jsme se již seznámili minule a předminule. Tyto makra a subrutiny jsou uloženy v samostatných souborech přidávaných do hlavního programu s využitím direktivy .include.
exit.s
# asmsyntax=as
# Makro pro ukonceni procesu v Linuxu.
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky
sys_exit = 1 # cislo syscallu pro ukonceni procesu
# Deklarace makra pro ukonceni aplikace
.macro exit
mov eax, sys_exit # cislo sycallu pro funkci "exit"
mov ebx, 0 # exit code = 0
int 0x80 # volani Linuxoveho kernelu
.endm
writeMessage.s
# asmsyntax=as
# Makro pro tisk zpravy na standardni vystup.
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky
# Linux kernel system call table
sys_write = 4
std_output = 1
# Deklarace makra pro vytisteni zpravy na standardni vystup
.macro writeMessage message,messageLength
mov ecx, offset \message # adresa retezce, ktery se ma vytisknout
mov edx, \messageLength # pocet znaku, ktere se maji vytisknout
call write_message # vytisknout zpravu "Zero flag not set"
.endm
# Podprogram pro vytisteni zpravy na standardni vystup
# Ocekava se, ze v ecx bude adresa zpravy a v edx jeji delka
write_message:
mov eax, sys_write # cislo syscallu pro funkci "write"
mov ebx, std_output # standardni vystup
int 0x80
ret
# Deklarace makra pro vytisteni znaku konce radku (provede se tedy odradkovani)
.macro println
writeMessage printlnMessage,printlnLength
.endm
#-----------------------------------------------------------------------------
.section .data
# Miniretezec pouzivany makrem println
printlnMessage:
.string "\n"
printlnLength = $ - printlnMessage
printHexNumber.s (32bitová varianta)
# asmsyntax=as
# Makro pro pripravu a tisk hexadecimalni hodnoty na standardni vystup.
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky
.intel_syntax noprefix
# Makro pro vypis 32bitove hexadecimalni hodnoty na standardni vystup
# Jedinym parametrem makra je hodnota (konstanta)
.macro printHexNumber value
pusha # uschovat vsechny registry na zasobnik
mov edx, \value # hodnotu pro tisk ulozit do registru EDX
mov ebx, offset hexValueTemplate # adresu pro retezec ulozit do registru EBX
call hex2string # zavolani prislusne subrutiny pro prevod na string
writeMessage hexValueMessage, hexValueMessageLen # retezec je naplnen, tak ho muzeme vytisknout
popa # obnovit obsah vsech registru
.endm
#-----------------------------------------------------------------------------
.section .data
hexValueMessage:
.string "Hex value: 0x" # prvni cast zpravy
hexValueTemplate: # druha cast zpravy ma vlastni navesti
.string "????????\n" # otazniky budou prepsany
hexValueMessageLen = $ - hexValueMessage # delka zpravy
#-----------------------------------------------------------------------------
.section .text
# Subrutina urcena pro prevod 32bitove hexadecimalni hodnoty na retezec
# Vstup: EDX - hodnota, ktera se ma prevest na retezec
# EBX - adresa jiz drive alokovaneho retezce (resp. osmice bajtu)
hex2string:
mov cl, 8 # pocet opakovani smycky
print_one_digit: rol edx, 4 # rotace doleva znamena, ze se do spodnich 4 bitu nasune dalsi cifra
mov al, dl # nechceme porusit obsah vstupni hodnoty v EDX, proto pouzijeme AL
and al, 0x0f # maskovani, potrebujeme pracovat jen s jednou cifrou
cmp al, 10 # je cifra vetsi nebo rovna 10?
jl store_digit # neni, pouze prevest 0..9 na ASCII hodnotu '0'..'9'
alpha_digit: add al, 'A'-10-'0' # prevod hodnoty 10..15 na znaky 'A'..'F'
store_digit: add al, '0'
mov byte ptr [ebx], al # ulozeni cifry do retezce
inc ebx # dalsi ulozeni v retezci o znak dale
dec cl # snizeni pocitadla smycky
jnz print_one_digit # a opakovani smycky, dokud se nedosahlo nuly
ret # navrat ze subrutiny
printHexNumber_64.s (64bitová varianta)
# asmsyntax=as
# Makro pro pripravu a tisk hexadecimalni hodnoty na standardni vystup.
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky
.intel_syntax noprefix
# Makro pro vypis 32bitove hexadecimalni hodnoty na standardni vystup
# Jedinym parametrem makra je hodnota (konstanta)
.macro printHexNumber value
push rax
push rbx
push rcx
push rdx # uschovat vsechny registry na zasobnik
mov edx, \value # hodnotu pro tisk ulozit do registru EDX
mov ebx, offset hexValueTemplate # adresu pro retezec ulozit do registru EBX
call hex2string # zavolani prislusne subrutiny pro prevod na string
writeMessage hexValueMessage, hexValueMessageLen # retezec je naplnen, tak ho muzeme vytisknout
pop rdx
pop rcx
pop rbx
pop rax # obnovit obsah vsech registru
.endm
#-----------------------------------------------------------------------------
.section .data
hexValueMessage:
.string "Hex value: 0x" # prvni cast zpravy
hexValueTemplate: # druha cast zpravy ma vlastni navesti
.string "????????\n" # otazniky budou prepsany
hexValueMessageLen = $ - hexValueMessage # delka zpravy
#-----------------------------------------------------------------------------
.section .text
# Subrutina urcena pro prevod 32bitove hexadecimalni hodnoty na retezec
# Vstup: EDX - hodnota, ktera se ma prevest na retezec
# EBX - adresa jiz drive alokovaneho retezce (resp. osmice bajtu)
hex2string:
mov cl, 8 # pocet opakovani smycky
print_one_digit: rol edx, 4 # rotace doleva znamena, ze se do spodnich 4 bitu nasune dalsi cifra
mov al, dl # nechceme porusit obsah vstupni hodnoty v EDX, proto pouzijeme AL
and al, 0x0f # maskovani, potrebujeme pracovat jen s jednou cifrou
cmp al, 10 # je cifra vetsi nebo rovna 10?
jl store_digit # neni, pouze prevest 0..9 na ASCII hodnotu '0'..'9'
alpha_digit: add al, 'A'-10-'0' # prevod hodnoty 10..15 na znaky 'A'..'F'
store_digit: add al, '0'
mov byte ptr [ebx], al # ulozeni cifry do retezce
inc ebx # dalsi ulozeni v retezci o znak dale
dec cl # snizeni pocitadla smycky
jnz print_one_digit # a opakovani smycky, dokud se nedosahlo nuly
ret # navrat ze subrutiny
13. Repositář s demonstračními příklady
Všechny tři dnes popisované demontrační příklady byly, podobně jako ve všech předchozích částech tohoto seriálu, společně s podpůrnými skripty určenými pro jejich překlad či naopak pro disassembling, uloženy do GIT repositáře dostupného na adrese https://github.com/tisnik/presentations/. Všechny příklady jsou určeny pro GNU Assembler a používají Intel syntaxi, která je pro mnoho programátorů čitelnější, než původní AT&T syntaxe. Následují tabulky obsahující odkazy na zdrojové kódy příkladů i na již zmíněné podpůrné skripty:
První demonstrační příklad: vytištění hodnot 0.0, 1.0 a Pi v hexadecimálním tvaru
Druhý demonstrační příklad: základní aritmetické operace
Třetí demonstrační příklad: dělení nulou
14. Odkazy na Internetu
- Simply FPU
http://www.website.masmforum.com/tutorials/fptute/ - Art of Assembly language programming: The 80x87 Floating Point Coprocessors
https://courses.engr.illinois.edu/ece390/books/artofasm/CH14/CH14-3.html - Art of Assembly language programming: The FPU Instruction Set
https://courses.engr.illinois.edu/ece390/books/artofasm/CH14/CH14-4.html - INTEL 80387 PROGRAMMER'S REFERENCE MANUAL
http://www.ragestorm.net/downloads/387intel.txt - x86 Instruction Set Reference: FLD
http://x86.renejeschke.de/html/file_module_x86_id_100.html - x86 Instruction Set Reference: FLD1/FLDL2T/FLDL2E/FLDPI/FLDLG2/FLDLN2/FLDZ
http://x86.renejeschke.de/html/file_module_x86_id_101.html - x86 Instruction Set Reference: FLD
http://x86.renejeschke.de/html/file_module_x86_id_100.html - x86 Instruction Set Reference: FST/FSTP
http://x86.renejeschke.de/html/file_module_x86_id_117.html - x86 Instruction Set Reference: FADD/FADDP/FIADD
http://x86.renejeschke.de/html/file_module_x86_id_81.html - x86 Instruction Set Reference: FSUB/FSUBP/FISUB
http://x86.renejeschke.de/html/file_module_x86_id_121.html - x86 Instruction Set Reference: FDIV/FDIVP/FIDIV
http://x86.renejeschke.de/html/file_module_x86_id_91.html - x86 Instruction Set Reference: BT
http://x86.renejeschke.de/html/file_module_x86_id_22.html - x86 Instruction Set Reference: BTC
http://x86.renejeschke.de/html/file_module_x86_id_23.html - x86 Instruction Set Reference: BTR
http://x86.renejeschke.de/html/file_module_x86_id_24.html - x86 Instruction Set Reference: BTS
http://x86.renejeschke.de/html/file_module_x86_id_25.html - x86 Instruction Set Reference: BSF
http://x86.renejeschke.de/html/file_module_x86_id_19.html - x86 Instruction Set Reference: BSR
http://x86.renejeschke.de/html/file_module_x86_id_20.html - x86 Instruction Set Reference: BSWAP
http://x86.renejeschke.de/html/file_module_x86_id_21.html - x86 Instruction Set Reference: XCHG
http://x86.renejeschke.de/html/file_module_x86_id_328.html - x86 Instruction Set Reference: SETcc
http://x86.renejeschke.de/html/file_module_x86_id_288.html - X86 Assembly/Arithmetic
https://en.wikibooks.org/wiki/X86_Assembly/Arithmetic - Art of Assembly - Arithmetic Instructions
http://oopweb.com/Assembly/Documents/ArtOfAssembly/Volume/Chapter_6/CH06-2.html - The GNU Assembler Tutorial
http://tigcc.ticalc.org/doc/gnuasm.html - The GNU Assembler - macros
http://tigcc.ticalc.org/doc/gnuasm.html#SEC109 - ARM subroutines & program stack
http://www.toves.org/books/armsub/ - Generating Mixed Source and Assembly List using GCC
http://www.systutorials.com/240/generate-a-mixed-source-and-assembly-listing-using-gcc/ - Calling subroutines
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.kui0100a/armasm_cihcfigg.htm - ARM Assembly Language Programming
http://peter-cockerell.net/aalp/html/frames.html - ASM Flags
http://www.cavestory.org/guides/csasm/guide/asm_flags.html - Status Register
https://en.wikipedia.org/wiki/Status_register - Intel x86 JUMP quick reference
http://unixwiz.net/techtips/x86-jumps.html - Linux assemblers: A comparison of GAS and NASM
http://www.ibm.com/developerworks/library/l-gas-nasm/index.html - Programovani v assembleru na OS Linux
http://www.cs.vsb.cz/grygarek/asm/asmlinux.html - Is it worthwhile to learn x86 assembly language today?
https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1 - Why Learn Assembly Language?
http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language - Is Assembly still relevant?
http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant - Why Learning Assembly Language Is Still a Good Idea
http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html - Assembly language today
http://beust.com/weblog/2004/06/23/assembly-language-today/ - Assembler: Význam assembleru dnes
http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz - Assembler pod Linuxem
http://phoenix.inf.upol.cz/linux/prog/asm.html - AT&T Syntax versus Intel Syntax
https://www.sourceware.org/binutils/docs-2.12/as.info/i386-Syntax.html - Linux Assembly website
http://asm.sourceforge.net/ - Using Assembly Language in Linux
http://asm.sourceforge.net/articles/linasm.html - vasm
http://sun.hasenbraten.de/vasm/ - vasm – dokumentace
http://sun.hasenbraten.de/vasm/release/vasm.html - The Yasm Modular Assembler Project
http://yasm.tortall.net/ - 680x0:AsmOne
http://www.amigacoding.com/index.php/680x0:AsmOne - ASM-One Macro Assembler
http://en.wikipedia.org/wiki/ASM-One_Macro_Assembler - ASM-One pages
http://www.theflamearrows.info/documents/asmone.html - Základní informace o ASM-One
http://www.theflamearrows.info/documents/asminfo.html - Linux Syscall Reference
http://syscalls.kernelgrok.com/ - Programming from the Ground Up Book - Summary
http://savannah.nongnu.org/projects/pgubook/ - IBM System 360/370 Compiler and Historical Documentation
http://www.edelweb.fr/Simula/ - IBM 700/7000 series
http://en.wikipedia.org/wiki/IBM_700/7000_series - IBM System/360
http://en.wikipedia.org/wiki/IBM_System/360 - IBM System/370
http://en.wikipedia.org/wiki/IBM_System/370 - Mainframe family tree and chronology
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_FT1.html - 704 Data Processing System
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP704.html - 705 Data Processing System
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP705.html - The IBM 704
http://www.columbia.edu/acis/history/704.html - IBM Mainframe album
http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_album.html - Osmibitové muzeum
http://osmi.tarbik.com/ - Tesla PMI-80
http://osmi.tarbik.com/cssr/pmi80.html - PMI-80
http://en.wikipedia.org/wiki/PMI-80 - PMI-80
http://www.old-computers.com/museum/computer.asp?st=1&c=1016 - The 6502 overflow flag explained mathematically
http://www.righto.com/2012/12/the-6502-overflow-flag-explained.html - X86 Opcode and Instruction Reference
http://ref.x86asm.net/coder