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

15. Odkazy na Internetu

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):

  1. 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.
  2. 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).
  3. 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.
  4. Horní čtyři bity registru AL maskujeme operací AND AL, 0x0f, takže registr AL bude obsahovat číslo od 0 do 15.
  5. 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.
  6. 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‘.
  7. Přepíšeme první otazník hodnotou uloženou v registru AL (jedná se o nejvyšší cifru výsledku).
  8. Zvýšíme adresu v registru EBX, aby se v dalším kroku přepsal druhý otazník atd.
  9. 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:

./a.out

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í:

./a.out

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):

  1. 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í.
  2. 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).
  3. 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.
  4. 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.
  5. 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.
  6. 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 .
  7. 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:

./a.out

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

  1. X86 Assembly/Arithmetic
    https://en.wikibooks.org/wiki/X86_Assembly/Arithmetic
  2. Art of Assembly – Arithmetic Instructions
    http://oopweb.com/Assembly/Documents/ArtOfAssembly/Volume/Chapter_6/CH06-2.html
  3. The GNU Assembler Tutorial
    http://tigcc.ticalc.org/doc/gnuasm.html
  4. The GNU Assembler – macros
    http://tigcc.ticalc.org/doc/gnuasm.html#SEC109
  5. ARM subroutines & program stack
    http://www.toves.org/books/armsub/
  6. Generating Mixed Source and Assembly List using GCC
    http://www.systutorials.com/240/generate-a-mixed-source-and-assembly-listing-using-gcc/
  7. Calling subroutines
    http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.kui0100a/armasm_cihcfigg.htm
  8. ARM Assembly Language Programming
    http://peter-cockerell.net/aalp/html/frames.html
  9. ASM Flags
    http://www.cavestory.org/guides/csasm/guide/asm_flags.html
  10. Status Register
    https://en.wikipedia.org/wiki/Status_register
  11. Intel x86 JUMP quick reference
    http://unixwiz.net/techtips/x86-jumps.html
  12. Linux assemblers: A comparison of GAS and NASM
    http://www.ibm.com/developerworks/library/l-gas-nasm/index.html
  13. Programovani v assembleru na OS Linux
    http://www.cs.vsb.cz/grygarek/asm/asmlinux.html
  14. Is it worthwhile to learn x86 assembly language today?
    https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1
  15. Why Learn Assembly Language?
    http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language
  16. Is Assembly still relevant?
    http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant
  17. Why Learning Assembly Language Is Still a Good Idea
    http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html
  18. Assembly language today
    http://beust.com/weblog/2004/06/23/assembly-language-today/
  19. Assembler: Význam assembleru dnes
    http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz
  20. Assembler pod Linuxem
    http://phoenix.inf.upol.cz/linux/prog/asm.html
  21. AT&T Syntax versus Intel Syntax
    https://www.sourceware.org/binutils/docs-2.12/as.info/i386-Syntax.html
  22. Linux Assembly website
    http://asm.sourceforge.net/
  23. Using Assembly Language in Linux
    http://asm.sourceforge.net/articles/linasm.html
  24. vasm
    http://sun.hasenbraten.de/vasm/
  25. vasm – dokumentace
    http://sun.hasenbraten.de/vasm/release/vasm.html
  26. The Yasm Modular Assembler Project
    http://yasm.tortall.net/
  27. 680×0:AsmOne
    http://www.amigacoding.com/index.php/680×0:AsmOne
  28. ASM-One Macro Assembler
    http://en.wikipedia.org/wiki/ASM-One_Macro_Assembler
  29. ASM-One pages
    http://www.theflamearrows.info/documents/asmone.html
  30. Základní informace o ASM-One
    http://www.theflamearrows.info/documents/asminfo.html
  31. Linux Syscall Reference
    http://syscalls.kernelgrok.com/
  32. Programming from the Ground Up Book – Summary
    http://savannah.nongnu.org/projects/pgubook/
  33. IBM System 360/370 Compiler and Historical Documentation
    http://www.edelweb.fr/Simula/
  34. IBM 700/7000 series
    http://en.wikipedia.org/wiki/IBM_700/7000_series
  35. IBM System/360
    http://en.wikipedia.org/wiki/IBM_System/360
  36. IBM System/370
    http://en.wikipedia.org/wiki/IBM_System/370
  37. Mainframe family tree and chronology
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_FT1.html
  38. 704 Data Processing System
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP704.html
  39. 705 Data Processing System
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP705.html
  40. The IBM 704
    http://www.columbia.edu/acis/history/704.html
  41. IBM Mainframe album
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_album.html
  42. Osmibitové muzeum
    http://osmi.tarbik.com/
  43. Tesla PMI-80
    http://osmi.tarbik.com/cssr/pmi80.html
  44. PMI-80
    http://en.wikipedia.org/wiki/PMI-80
  45. PMI-80
    http://www.old-computers.com/museum/computer.asp?st=1&c=1016
  46. The 6502 overflow flag explained mathematically
    http://www.righto.com/2012/12/the-6502-overflow-flag-explained.html
  47. X86 Opcode and Instruction Reference
    http://ref.x86asm.net/coder32.html