Ve dvanácté části seriálu o použití assembleru v Linuxu použijeme znalosti a aritmetických a logických instrukcích i o bitových posunech, které jsme získali v předchozích článcích. Vytvoříme si totiž několik maker a k nim přidružených subrutin určených pro výpis hexadecimálních i desítkových hodnot na standardní výstup. Uvidíme, že i tyto na první pohled jednoduché funkce vyžadují poměrně podrobnou znalost assembleru, způsobu adresování buněk operační paměti apod.
Obsah
1. Použití assembleru v Linuxu: aritmetické a logické instrukce i bitové posuny v praxi
2. Makro pro vytištění hexadecimální hodnoty na standardní výstup
3. Subrutina pro převod celého 32bitového čísla na osmici hexadecimálních cifer
4. První demonstrační příklad: použití bitové rotace, maskování a součtu
5. Výsledky vygenerované prvním demonstračním příkladem
6. Disassemblovaný strojový kód subrutiny hex2string
7. Druhý demonstrační příklad: optimalizace spočívající v eliminaci nepodmíněného skoku
8. Výsledky vygenerované druhým demonstračním příkladem
9. Disassemblovaný strojový kód upravené subrutiny hex2string
10. Vytištění desítkové hodnoty na standardní výstup
11. Třetí demonstrační příklad: použití instrukce DIV a adresování typu registr+registr+offset
12. Výsledky vygenerované třetím demonstračním příkladem
13. Disassemblovaný strojový kód subrutiny decimal2string
14. Repositář s demonstračními příklady
1. Použití assembleru v Linuxu: aritmetické a logické instrukce i bitové posuny v praxi
V úvodních jedenácti částech seriálu o využití assembleru v Linuxu (na různých platformách) jsme si popsali většinu nejdůležitějších strojových instrukcí, které můžeme nalézt jak na mikroprocesorových architekturách i386 a x86-64, tak i na 32bitových mikroprocesorech ARM. Jedná se o instrukce pro relativní či absolutní podmíněné a nepodmíněné skoky, skoky do podprogramů (subrutin) a návrat z podprogramů, aritmetické instrukce, logické instrukce, instrukce pro provedení bitových posunů a rotací, instrukce pro aritmetické posuny a samozřejmě též o instrukce určené pro přenos dat mezi pracovními registry mikroprocesoru popř. pro přenos dat mezi vybraným pracovním registrem a operační pamětí. Taktéž jsme si popsali význam čtyř stavových bitů nazvaných Carry flag, Overflow flag, Sign/Negative flag a Zero flag a jejich použití při operacích s celými čísly bez znaménka i se znaménkem.
Ovšem samotná znalost těchto instrukcí popř. znalost, jak jednotlivé operace ovlivní příznakové bity, je sama o sobě nedůležitá, pokud nejsme schopni tyto instrukce použít v reálných programech popř. v jednotlivých podprogramech (subrutinách). Z tohoto důvodu si dnes ukážeme trojici demonstračních příkladů, v nichž budou implementována makra nazvaná hex2string a decimal2string. Tato makra slouží, jak jejich název napovídá, k převodu celého čísla na řetězec obsahující buď hexadecimální reprezentaci tohoto čísla nebo reprezentaci v desítkové soustavě. Při implementaci těchto maker si procvičíme použití aritmetických instrukcí (součet, celočíselné dělení), bitových rotací, maskování pomocí instrukce AND, adresování buněk v operační paměti, a samozřejmě též použití nepodmíněných a podmíněných skoků (konkrétně JGE, JNE a JNZ).
2. Makro pro vytištění hexadecimální hodnoty na standardní výstup
Prvním makrem, kterým se dnes budeme zabývat, je makro určené pro vytištění hexadecimální hodnoty na standardní výstup. Makro by mělo akceptovat jediný parametr – celočíselnou hodnotu, která má být pro převedení do hexadecimální podoby vytištěna. Při implementaci samozřejmě můžeme použít již existující makro určené pro tisk zprávy nazvané writeMessage. Připomeňme si, že makro writeMessage očekává počáteční adresu řetězce a počet znaků v řetězci:
# 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
Před vytvořením makra určeného pro tisk hexadecimální hodnoty si v datovém segmentu připravíme „šablonu“ použitou jako základ pro zprávu, která se zobrazí na standardním výstupu. Povšimněte si, že řetězec, který šablonu tvoří, je rozdělen na dvě části, protože potřebujeme znát adresu otazníků, které budou následně přepsány hexadecimální hodnotou (namísto otazníků je samozřejmě možné použít libovolný jiný znak). Celá šablona tedy vypadá takto: "Hex value: 0x????????\n", přičemž návěští (label) hexValueTemplate obsahuje adresu prvního otazníku:
.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
Samotné makro printHexNumber je vlastně velmi jednoduché, protože na jeho začátku pouze naplníme 32bitový pracovní registr EDX hodnotou, která se má vytisknout, do dalšího pracovního registru EBX vložíme adresu prvního otazníku v šabloně, posléze zavoláme subrutinu hex2string popsanou dále a konečně zavoláme makro writeMessage, které upravenou zprávu vytiskne na standardní výstup:
# Makro pro vypis 32bitove hexadecimalni hodnoty na standardni vystup
# Jedinym parametrem makra je hodnota (konstanta)
.macro printHexNumber value
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
.endm
3. Subrutina pro převod celého 32bitového čísla na osmici hexadecimálních cifer
Makro, které má vytisknout hexadecimální hodnotu 32bitového celého čísla na standardní výstup, již máme vytvořené, takže nám „pouze“ zbývá implementace subrutiny hex2string. Tato subrutina očekává v registru EDX 32bitovou hodnotu a v registru EBX adresu prvního otazníku v šabloně zprávy. Pro převod 32bitové hodnoty na osmici hexadecimálních cifer můžeme použít následující algoritmus (existují však i další možnosti):
- Do osmibitového registru CL, který bude fungovat jako počitadlo, uložíme konstantu 8, protože 32bitová hodnota je převedena na osmici hexadecimálních cifer.
- Hodnota v registru EDX se zrotuje doleva o čtyři bity. Touto operací se původní nejvyšší čtyři bity přesunou do spodních čtyř bitů (musíme tedy skutečně použít rotaci a nikoli aritmetický posun).
- Spodních osm bitů registru EDX se přesune do osmibitového registru AL. Ten tedy bude obsahovat čtyři bity s nezajímavým obsahem a spodní čtyři bity s cifrou.
- Horní čtyři bity registru AL maskujeme operací AND AL, 0x0f, takže registr AL bude obsahovat číslo od 0 do 15.
- Pokud je v registru AL uložena hodnota menší než 10, přičteme k této hodnotě '0', tedy ASCII kód znaku nula. Touto operací tedy jednoduše provedeme převod na ASCII znak.
- Pokud je naopak hodnota v registru AL větší nebo rovna deseti, provedeme přičtení konstanty takovým způsobem, aby byl výsledkem znak 'A' až 'F'.
- Přepíšeme první otazník hodnotou uloženou v registru AL (jedná se o nejvyšší cifru výsledku).
- Zvýšíme adresu v registru EBX, aby se v dalším kroku přepsal druhý otazník atd.
- Snížíme počitadlo v registru CL o jedničku a pokud je nenulové, bude se celá smyčka opakovat.
Konkrétní implementace algoritmu popsaného výše může vypadat následovně. Povšimněte si použití podmíněného skoku JGE při rozlišení cifry 0..9 od cifry A..F a taktéž nám již známého typického ukončení počítané programové smyčky dvojicí instrukcí DEC+JNZ:
# 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?
jge alpha_digit # ano je, prevest na znak 'A'..'F'
numeric_digit: add al, '0' # neni, prevest na znak '0'..'9' (postacuje pricist ASCII hodnotu '0')
jmp store_digit
alpha_digit: add al, 'A'-10 # prevod hodnoty 10..15 na znaky 'A'..'F'
store_digit: 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
Poznámka: při ukládání znaku (jedné hexadecimální cifry) do řetězce je nutné použít mov byte ptr [ebx], al, protože jinak by GNU Assembler nevěděl, zda se má zapisovat bajt, 16bitové slovo, 32bitové slovo či 64bitové slovo.
4. První demonstrační příklad: použití bitové rotace, maskování a součtu
Pro větší přehlednost je první demonstrační příklad rozdělen do čtyř samostatných souborů, z nichž tři soubory obsahují makra a k nim pomocné subrutiny:
Soubor | Význam |
---|---|
main.s | hlavní část aplikace, tento soubor je předán GNU Assembleru pro překlad, ostatní soubory jsou do něho vkládány direktivou .include |
exit.s | obsahuje pouze makro exit pro ukončení procesu |
writeMessage.s | obsahuje makro writeMessage a taktéž příslušnou subrutinu write_message, taktéž zde nalezneme makro println |
printHexNumber.s | obsahuje makro printHexNumber, subrutinu hex2string a taktéž šablonu zprávy hexValueMessage |
Obsah souboru main.s
V tomto souboru se přes direktivy .include vkládají další soubory a volají se makra printHexNumber, println a exit:
# asmsyntax=as
# Program pro otestovani makra urceneho pro vytisteni hexadecimalni hodnoty.
# - 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
#-----------------------------------------------------------------------------
.section .bss
#-----------------------------------------------------------------------------
.section .text
.global _start # tento symbol ma byt dostupny i linkeru
_start:
printHexNumber 0 # otestujme zakladni hodnoty
printHexNumber 1
printHexNumber 2
println
printHexNumber 9 # dulezity je i prechod '9'->'A'
printHexNumber 10
println
printHexNumber 15 # dalsi dulezity prechod pro otestovani 'F'->'10'
printHexNumber 16
println
printHexNumber 127 # dalsi zajimave hodnoty pro otestovani
printHexNumber 128
printHexNumber 255
printHexNumber 256
println
printHexNumber -1 # aritmetika se znamenkem - dvojkovym doplnkem
printHexNumber -2
printHexNumber -256
exit # ukonceni aplikace
Obsah souboru exit.s
Zde nedochází k žádným podstatnějším změnám oproti předchozím demonstračním příkladům, v nichž jsme tento soubor taktéž využívali:
# 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
Obsah souboru writeMessage.s
Nalezneme zde definici maker writeMessage a println společně se subrutinou write_message:
# 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
Obsah souboru printHexNumber.s
Nejdůležitější část celého programu s deklarací makra printHexNumber a subrutinou hex2string:
# 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
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
.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?
jge alpha_digit # ano je, prevest na znak 'A'..'F'
numeric_digit: add al, '0' # neni, prevest na znak '0'..'9' (postacuje pricist ASCII hodnotu '0')
jmp store_digit
alpha_digit: add al, 'A'-10 # prevod hodnoty 10..15 na znaky 'A'..'F'
store_digit: 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
5. Výsledky vygenerované prvním demonstračním příkladem
Pro jistotu se podívejme, jaké výsledky jsou naším prvním demonstračním příkladem vytištěny:
<strong>./a.out</strong>
Hex value: 0x00000000
Hex value: 0x00000001
Hex value: 0x00000002
Hex value: 0x00000009
Hex value: 0x0000000A
Hex value: 0x0000000F
Hex value: 0x00000010
Hex value: 0x0000007F
Hex value: 0x00000080
Hex value: 0x000000FF
Hex value: 0x00000100
Hex value: 0xFFFFFFFF
Hex value: 0xFFFFFFFE
Hex value: 0xFFFFFF00
Vše se zdá být v pořádku, a to i v případě záporných čísel, protože záporná čísla jsou, jak již víme z předchozích částí tohoto seriálu, reprezentovaná v dvojkovém doplňku.
6. Disassemblovaný strojový kód subrutiny hex2string
Pro zajímavost si v této kapitole ukažme, jak vypadá přeložený strojový kód subrutiny hex2string, kterou jsme si popsali v předchozích kapitolách. Celá subrutina byla přeložena do dvaceti sedmi bajtů a pro výpis každé cifry dojde k minimálně dvěma skokům (JGE+JNE popř. JMP+JNE). Povšimněte si, že všechny instrukce skoků mají délku pouhé dva bajty, a to z toho důvodu, že GNU Assembler zvolil použití relativních skoků v rozsahu -126..128 bajtů. Taktéž si povšimněte toho, že všechny instrukce s osmibitovými registry a konstantami (MOV CL, 0x08, AND AL, 0x0F, CMP AL, 0x0A či ADD AL, 0x30) mají taktéž velikost pouhé dva bajty. Pokud by všude byly použity 32bitové registry, byla by subrutina o několik bajtů delší:
0000000d <hex2string>:
d: b1 08 mov cl,0x8
0000000f <print_one_digit>:
f: c1 c2 04 rol edx,0x4
12: 88 d0 mov al,dl
14: 24 0f and al,0xf
16: 3c 0a cmp al,0xa
18: 7d 04 jge 1e <alpha_digit>
0000001a <numeric_digit>:
1a: 04 30 add al,0x30
1c: eb 02 jmp 20 <store_digit>
0000001e <alpha_digit>:
1e: 04 37 add al,0x37
00000020 <store_digit>:
20: 88 03 mov BYTE PTR [ebx],al
22: 43 inc ebx
23: fe c9 dec cl
25: 75 e8 jne f <print_one_digit>
27: c3 ret
7. Druhý demonstrační příklad: optimalizace spočívající v eliminaci nepodmíněného skoku
První verze subrutiny hex2string není napsána optimálně; zejména se to týká zbytečně velkého množství programových skoků. V celém algoritmu nalezneme jednu podmínku (cifra menší než 10 či naopak větší nebo rovna deseti) a jednu smyčku (opakování výpočtu pro osm cifer). My jsme v první verzi použili následující implementaci podmínky:
cmp al, 10 # je cifra vetsi nebo rovna 10?
jge alpha_digit # ano je, prevest na znak 'A'..'F'
numeric_digit: add al, '0' # neni, prevest na znak '0'..'9' (postacuje pricist ASCII hodnotu '0')
jmp store_digit
alpha_digit: add al, 'A'-10 # prevod hodnoty 10..15 na znaky 'A'..'F'
Tutéž podmínku lze ovšem přepsat takovým způsobem, aby bylo možné vynechat druhý (nepodmíněný) skok. Podmínku otočíme, takže namísto skoku JGE (jump if greater than or equal) se použije skok JL (jump if less). U cifer A až F ovšem dojde k dvěma součtům:
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'
Nová podoba souboru printHexNumber.s vypadá následovně:
# 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
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
.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
8. Výsledky vygenerované druhým demonstračním příkladem
Po úpravě subrutiny hex2string samozřejmě znovu program přeložíme a spustíme, aby se funkčnost subrutiny otestovala. Výsledky jsou (podle očekávání) shodné s originální verzí:
<strong>./a.out</strong>
Hex value: 0x00000000
Hex value: 0x00000001
Hex value: 0x00000002
Hex value: 0x00000009
Hex value: 0x0000000A
Hex value: 0x0000000F
Hex value: 0x00000010
Hex value: 0x0000007F
Hex value: 0x00000080
Hex value: 0x000000FF
Hex value: 0x00000100
Hex value: 0xFFFFFFFF
Hex value: 0xFFFFFFFE
Hex value: 0xFFFFFF00
9. Disassemblovaný strojový kód upravené subrutiny hex2string
Vzhledem k tomu, že jsme ze subrutiny hex2string odstranili jeden nepodmíněný relativní skok, který byl původně přeložen do dvou bajtů, měla by být i podoba subrutiny po překladu do strojového kódu kratší přesně o tyto dva bajty. Můžeme se o tom ostatně velmi snadno přesvědčit po zadání příkazu:
objdump -M intel-mnemonic -f -d -t -h main.o
Ve výsledku nalezneme mj. i strojový kód a zpětný překlad subrutiny hex2string, z něhož skutečně vyplývá délka 25 bajtů v porovnání s 27 bajty předchozí verze:
0000000d <hex2string>:
d: b1 08 mov cl,0x8
0000000f <print_one_digit>:
f: c1 c2 04 rol edx,0x4
12: 88 d0 mov al,dl
14: 24 0f and al,0xf
16: 3c 0a cmp al,0xa
18: 7c 02 jl 1c <store_digit>
0000001a <alpha_digit>:
1a: 04 07 add al,0x7
0000001c <store_digit>:
1c: 04 30 add al,0x30
1e: 88 03 mov BYTE PTR [ebx],al
20: 43 inc ebx
21: fe c9 dec cl
23: 75 ea jne f <print_one_digit>
25: c3 ret
Poznámka: teoreticky jsou možné i další úpravy, například změna adresování při zápisu a použití instrukce STOSB namísto MOV, ovšem na moderních procesorech je použitá subrutina dostatečně rychlá a současně i krátká (což je neméně důležité zejména s ohledem na využití cache v procesoru).
10. Vytištění desítkové hodnoty na standardní výstup
Převod celočíselné 32bitové hodnoty na řetězec představující hexadecimální zápis tohoto čísla již tedy pro nás není problematický. Zkusme si nyní možná poněkud složitější příklad – převod celočíselné 32bitové hodnoty na decimální podobu. V tomto případě už nelze použít postup popsaný výše, tedy postupné získávání jednotlivých hexadecimálních cifer, přičemž každá cifra je uložena ve čtyřech bitech. Budeme muset postupovat jinak – postupně budeme dělit vstupní hodnotu deseti a zbytek po dělení budou jednotlivé cifry, které převedeme na ASCII. Řetězec s textovou reprezentací čísla se tedy bude skládat odzadu (od posledního znaku):
- Do 32bitového registru ECX, který bude fungovat jako počitadlo, uložíme konstantu 10, protože 32bitová hodnota je převedena na deset desítkových cifer (232=4294967296). Můžeme zde vidět rozdíl oproti předchozímu příkladu, kde se pro počitadlo používal osmibitový registr, ale je to nutné, neboť nyní ECX použijeme i při adresování.
- Do jednoho z volných 32bitových registrů se vloží konstanta 10, která bude použita v instrukci DIV. Opět je to nutné, protože instrukce DIV, jako jedna z mála aritmetických instrukcí, vyžaduje použití registru a nikoli konstanty (jinak by bylo možné zapsat DIV 10).
- Vynulujeme registr EDX, protože instrukce DIV provede dělení 64bitové hodnoty uložené v registrovém párů EDX:EAX. My samozřejmě dělíme pouze 32bitové číslo, takže horních 32bitů bude vždy nulových.
- Instrukcí DIV EDI vydělíme hodnotu v (EDX):EAX deseti, přičemž výsledek se uloží zpět do EAX (což se nám hodí pro další iteraci) a zbytek po dělení se ocitne v registru EDX.
- Víme, že po dělení deseti může být zbytek maximálně roven devíti, takže budeme pracovat jen s osmi nejnižšími bity registru EDX, pro něž existuje jmenný alias DL. Přičteme ke zbytku ASCII hodnotu znaku '0' a získáme tak nejnižší cifru výsledku.
- Nyní máme v registru DL uloženou nejnižší cifru výsledku (ASCII kód znaku '0' až '9'), v registru ECX je hodnota počitadla (na začátku nastavena na 10) a v registru EBX adresa začátku řetězce s otazníky (kam se mají postupně vypsat jednotlivé cifry). My nyní potřebujeme zapisovat cifry od konce (od nejnižšího řádu), tedy od adresy EBX+ECX-1. Takový adresovací režim je skutečně na architektuře i386/x86-64 podporován, takže stačí provést zápis: mov byte ptr [ebx+ecx-1], dl .
- Snížíme počitadlo v registru ECX o jedničku a pokud je nenulové, bude se celá smyčka opakovat.
# Subrutina urcena pro prevod 32bitove desitkove hodnoty na retezec
# Vstup: EAX - hodnota, ktera se ma prevest na retezec
# EBX - adresa jiz drive alokovaneho retezce (resp. minimalne deseti bajtu)
decimal2string:
mov ecx, 10 # celkovy pocet zapisovanych cifer/znaku
mov edi, ecx # instrukce DIV vyzaduje deleni registrem, pouzijme tedy EDI
next_digit:
xor edx, edx # delenec je dvojice EDX:EAX, vynulujeme tedy horni registr EDX
div edi # deleni hodnoty ulozene v EDX:EAX deseti (delitelem je EDI)
# vysledek se ulozi do EAX, zbytek do EDX
# pri deleni deseti je jistota, ze zbytek je jen cislo 0..9
add dl, '0' # prevod hodnoty 0..9 na znak '0'-'9'
mov byte ptr [ebx+ecx-1], dl # zapis retezce (od posledniho znaku)
dec ecx # presun na predchozi znak v retezci a soucasne snizeni hodnoty pocitadla
jnz next_digit # uz jsme dosli k poslednimu cislu?
ret # navrat ze subrutiny
Celá subrutina má pouhých devět instrukcí, což není špatné.
11. Třetí demonstrační příklad: použití instrukce DIV a adresování typu registr+registr+offset
Celý třetí demonstrační příklad si zde nebudeme vypisovat, a to z toho prostého důvodu, že soubory exit.s a writeMessage.s jsou naprosto shodné se soubory použitými v prvních dvou příkladech. Ukážeme si tedy obsah souborů main.s a samozřejmě taktéž printDecimalNumber.s.
Soubor main.s
V tomto souboru se přes direktivy .include vkládají další soubory a volají se makra printDecimalNumber, println a exit. Vzhledem k charakteru převodu již netestujeme hodnoty typu 0xf (15) a 255, které jsou zajímavé z hlediska hexadecimálního kódu, ale hodnoty typu 99, 100, 999, 1000 atd. Vyzkoušíme i zadání záporných čísel, i když je nutné dopředu říci, že ta nebudou vypsána korektně:
# asmsyntax=as
# Program pro otestovani makra urceneho pro vytisteni desitkove hodnoty.
# - 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 desitkove 32bitove hodnoty
# spolecne s makrem je nactena i prislusna subrutina
.include "printDecimalNumber.s"
#-----------------------------------------------------------------------------
.section .data
#-----------------------------------------------------------------------------
.section .bss
#-----------------------------------------------------------------------------
.section .text
.global _start # tento symbol ma byt dostupny i linkeru
_start:
printDecimalNumber 0 # otestujme zakladni hodnoty
printDecimalNumber 1
printDecimalNumber 2
println
printDecimalNumber 9 # dulezity je i prechod
printDecimalNumber 10
println
printDecimalNumber 99 # dalsi dulezity prechod pro otestovani
printDecimalNumber 100
println
printDecimalNumber 999 # a dalsi...
printDecimalNumber 1000
println
printDecimalNumber -1 # aritmetika se znamenkem - dvojkovym doplnkem
printDecimalNumber -2
printDecimalNumber -9
printDecimalNumber -10
exit # ukonceni aplikace
Soubor printDecimalNumber.s
Nejdůležitější částí tohoto demonstračního příkladu je již popsané makro printDecimalNumber a k němu příslušející subrutina decimal2string. Ty nalezneme právě v souboru nazvaném printDecimalNumber.s:
# asmsyntax=as
# Makro pro pripravu a tisk desitkove hodnoty na standardni vystup.
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky
.intel_syntax noprefix
# Makro pro vypis 32bitove desitkove hodnoty na standardni vystup
# Jedinym parametrem makra je hodnota (konstanta)
.macro printDecimalNumber value
mov eax, \value # hodnotu pro tisk ulozit do registru EAX
mov ebx, offset decimalValueTemplate # adresu pro retezec ulozit do registru EBX
call decimal2string # zavolani prislusne subrutiny pro prevod na string
writeMessage decimalValueMessage, decimalValueMessageLen # retezec je naplnen, tak ho muzeme vytisknout
.endm
#-----------------------------------------------------------------------------
.section .data
decimalValueMessage:
.string "Decimal value: " # prvni cast zpravy
decimalValueTemplate: # druha cast zpravy ma vlastni navesti
.string "??????????\n" # otazniky budou prepsany (musi jich byt presne deset!)
decimalValueMessageLen = $ - decimalValueMessage # delka zpravy
#-----------------------------------------------------------------------------
.section .text
# Subrutina urcena pro prevod 32bitove desitkove hodnoty na retezec
# Vstup: EDX - hodnota, ktera se ma prevest na retezec
# EBX - adresa jiz drive alokovaneho retezce (resp. minimalne deseti bajtu)
decimal2string:
mov ecx, 10 # celkovy pocet zapisovanych cifer/znaku
mov edi, ecx # instrukce DIV vyzaduje deleni registrem, pouzijme tedy EDI
next_digit:
xor edx, edx # delenec je dvojice EDX:EAX, vynulujeme tedy horni registr EDX
div edi # deleni hodnoty ulozene v EDX:EAX deseti (delitelem je EDI)
# vysledek se ulozi do EAX, zbytek do EDX
# pri deleni deseti je jistota, ze zbytek je jen cislo 0..9
add dl, '0' # prevod hodnoty 0..9 na znak '0'-'9'
mov byte ptr [ebx+ecx-1], dl # zapis retezce (od posledniho znaku)
dec ecx # presun na predchozi znak v retezci a soucasne snizeni hodnoty pocitadla
jnz next_digit # uz jsme dosli k poslednimu cislu?
ret # navrat ze subrutiny
12. Výsledky vygenerované třetím demonstračním příkladem
Po spuštění tohoto demonstračního příkladu by se na standardním výstupu měly objevit následující řádky:
<strong>./a.out</strong>
Decimal value: 0000000000
Decimal value: 0000000001
Decimal value: 0000000002
Decimal value: 0000000009
Decimal value: 0000000010
Decimal value: 0000000099
Decimal value: 0000000100
Decimal value: 0000000999
Decimal value: 0000001000
Decimal value: 4294967295
Decimal value: 4294967294
Decimal value: 4294967287
Decimal value: 4294967286
Povšimněte si především nesprávné funkce subrutiny pro záporná čísla, tedy pro poslední čtyři řádky na výstupu. Pokud by bylo nutné záporná čísla skutečně zpracovávat a vypisovat, celý program by byl poněkud složitější, neboť by se musel testovat nejvyšší bit původní hodnoty (CMP+JP) a namísto instrukce DIV by se použila instrukce IDIV popř. by se před výpočtem číslo převedlo na svou absolutní hodnotu instrukcí NEG.
13. Disassemblovaný strojový kód subrutiny decimal2string
Pro zajímavost se opět podívejme, jak se program přeložil do strojového kódu:
0000000d <decimal2string>:
d: b9 0a 00 00 00 mov ecx,0xa
12: 89 cf mov edi,ecx
00000014 <next_digit>:
14: 31 d2 xor edx,edx
16: f7 f7 div edi
18: 80 c2 30 add dl,0x30
1b: 88 54 0b ff mov BYTE PTR [ebx+ecx*1-0x1],dl
1f: 49 dec ecx
20: 75 f2 jne 14 <next_digit>
22: c3 ret
Velmi neefektivní je především první instrukce, která nastavuje počitadlo. Pokud by nám záleželo na délce subrutiny a nikoli na rychlosti zpracování, lze použít známého triku s vymazáním celého registru a nastavením pouze jeho spodních osmi bitů:
d: 31 c9 xor ecx,ecx
f: b1 0a mov cl,0xa
Naopak je možná překvapivé, jak se přeložila relativně složitá instrukce mov BYTE PTR [ebx+ecx*1-0x1],dl, v níž se, jak vidíme, navíc násobí hodnota počitadla ECX celočíselnou konstantou 1 (zde tuto možnost nevyužijeme, ale při přístupu do polí či záznamů to může být velmi výhodné:
1b: 88 54 0b ff mov BYTE PTR [ebx+ecx*1-0x1],dl
14. Repositář s demonstračními příklady
Všechny tři dnes popisované demonstrač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 s odkazy na zdrojové kódy příkladů i na již zmíněné podpůrné skripty:
První demonstrační příklad – vytištění hexadecimální 32bitové hodnoty
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | main.s | hlavní program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/32_print_hex_number/main.s |
2 | exit.s | program pro GNU Assembler, který se vkládá do prvního souboru | https://github.com/tisnik/presentations/blob/master/assembler/32_print_hex_number/exit.s |
3 | writeMessage.s | program pro GNU Assembler, který se vkládá do prvního souboru | https://github.com/tisnik/presentations/blob/master/assembler/32_print_hex_number/writeMessage.s |
4 | printHexNumber.s | implementace makra a subrutiny pro převod hex2string | https://github.com/tisnik/presentations/blob/master/assembler/32_print_hex_number/printHexNumber.s |
5 | assemble | skript pro překlad na procesorech i386 | https://github.com/tisnik/presentations/blob/master/assembler/32_print_hex_number/assemble |
6 | disassemble | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/32_print_hex_number/disassemble |
Druhý demonstrační příklad – vylepšená verze subrutiny pro vytištění hexadecimální 32bitové hodnoty
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | main.s | hlavní program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/33_print_hex_number_2/main.s |
2 | exit.s | program pro GNU Assembler, který se vkládá do prvního souboru | https://github.com/tisnik/presentations/blob/master/assembler/33_print_hex_number_2/exit.s |
3 | writeMessage.s | program pro GNU Assembler, který se vkládá do prvního souboru | https://github.com/tisnik/presentations/blob/master/assembler/33_print_hex_number_2/writeMessage.s |
4 | printHexNumber2.s | vylepšená implementace makra a subrutiny pro převod hex2string | https://github.com/tisnik/presentations/blob/master/assembler/33_print_hex_number_2/printHexNumber2.s |
5 | assemble | skript pro překlad na procesorech i386 | https://github.com/tisnik/presentations/blob/master/assembler/33_print_hex_number_2/assemble |
6 | disassemble | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/33_print_hex_number_2/disassemble |
Třetí demonstrační příklad – vytištění desítkové 32bitové hodnoty
# | Soubor | Popis | Odkaz do repositáře |
---|---|---|---|
1 | main.s | hlavní program pro GNU Assembler | https://github.com/tisnik/presentations/blob/master/assembler/34_print_decimal_number/main.s |
2 | exit.s | program pro GNU Assembler, který se vkládá do prvního souboru | https://github.com/tisnik/presentations/blob/master/assembler/34_print_decimal_number/exit.s |
3 | writeMessage.s | program pro GNU Assembler, který se vkládá do prvního souboru | https://github.com/tisnik/presentations/blob/master/assembler/34_print_decimal_number/writeMessage.s |
4 | printDecimalNumber.s | implementace makra a subrutiny pro převod decimal2string | https://github.com/tisnik/presentations/blob/master/assembler/34_print_decimal_number/printDecimalNumber.s |
5 | assemble | skript pro překlad na procesorech i386 | https://github.com/tisnik/presentations/blob/master/assembler/34_print_decimal_number/assemble |
6 | disassemble | skript pro disassembling | https://github.com/tisnik/presentations/blob/master/assembler/34_print_decimal_number/disassemble |
15. Odkazy na Internetu
- 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/coder32.html