V demonstračních příkladech, které jsme si ukazovali v předchozích částech tohoto seriálu, jsme si prozatím vystačili s voláním několika funkcí jádra operačního systému (jedná se o takzvané syscalls). V praxi se však dříve či později dostaneme do situace, kdy je zapotřebí používat i další knihovny, například standardní knihovnu jazyka C. Dnes si proto ukážeme, jaké konvence je zapotřebí dodržet při volání funkcí z této knihovny na 64bitové platformě x86-64.

Obsah

1. Použití assembleru v Linuxu: volání funkcí ze standardní knihovny jazyka C

2. Zjednodušení práce s assemblerem a linkerem – přímé použití gcc

3. Vstupní bod do aplikace, předání návratové hodnoty shellu (exit code)

4. První příklad – kostra aplikace s funkcí main naprogramovanou v assembleru

5. Volání knihovní funkce puts

6. Zarovnání vrcholu zásobníku na adresu dělitelnou šestnácti

7. Druhý příklad – výpis řetězce na obrazovku pomocí funkce puts

8. Kontrola zarovnání v debuggeru (GDB)

9. Volání knihovní funkce printf a význam obsahu registru AL

10. Třetí příklad – výpis řetězce na obrazovku pomocí funkce printf

11. Repositář s demonstračními příklady

12. Odkazy na Internetu

1. Použití assembleru v Linuxu: volání funkcí ze standardní knihovny jazyka C

Prakticky jakákoli rozsáhlejší aplikace využívá funkce, které jsou dostupné v (nativních) knihovnách, ať již statických či dynamických. To se samozřejmě týká i jazyka symbolických instrukcí (assembleru), protože by bylo velmi pracné neustále znovu implementovat i tak základní a často používané funkce, jako je načtení celého čísla ze standardního vstupu, výpis zformátovaného čísla s desetinnou čárkou, otevření souboru, zápis či čtení ze souboru, uzavření souboru apod. Mnohé z těchto funkcí jsou implementovány ve standardní knihovně programovacího jazyka C, která je nainstalována na každém Linuxu. Dnes již sice poněkud starší, ale stále užitečný popis funkcí ze standardní céčkové knihovny lze najít na adrese https://www-s.acm.illinois.edu/webmonkeys/book/c_guide/index.html (v tomto popisu nenalezneme některé změny implementované v rámci standardizace C99). Ve skutečnosti je možné standardní céčkovou knihovnu používat i v assembleru, a to poměrně jednoduchým způsobem, což je téma, kterému se budeme věnovat v navazujících kapitolách.

2. Zjednodušení práce s assemblerem a linkerem – přímé použití gcc

Při nutnosti použití vybraných nativních knihoven volaných z programu napsaného v assembleru je zapotřebí zaručit, aby se objektový kód vytvořený assemblerem korektně slinkoval se zvolenou statickou knihovnou či knihovnami. To je možné zajistit několika způsoby, typicky specifikací všech knihoven a objektových kódů, které mají společně vytvořit výslednou aplikaci. Tento seznam se následně předá linkeru, který je na Linuxu představován nástrojem ld (i když je nutné upozornit na to, že způsob zadávání parametrů linkeru je poněkud těžkopádný). To však není vše, protože kromě pouhého slinkování objektového souboru s knihovními funkcemi je taktéž nutné zajistit, aby výsledný spustitelný nativní soubor obsahoval správný vstupní bod (entry point) atd.

Zkušení assemblerovští mágové si vystačí s nástroji as a ld, ovšem existuje i mnohem jednodušší varianta překladu, kterou si ukážeme dneska. Tato varianta spočívá v tom, že se namísto explicitního volání assembleru (as) a linkeru (ld) jednoduše zavolá gcc, kterému se jako jediný parametr předá jméno zdrojového souboru se zdrojovým kódem napsaným v assembleru. Pokud bude jméno tohoto souboru obsahovat koncovku „.s“ (tedy například „test.s“), rozezná gcc, že se jedná o assembler a sám automaticky nejenom spustí as a ld, ale navíc i zajistí korektní slinkování se standardní céčkovou knihovnou apod. Jedinou vážnější nevýhodou tohoto postupu je větší velikost výsledného spustitelného souboru, která dosahuje přibližně šesti kilobajtů (na platformě x86-64).

3. Vstupní bod do aplikace, předání návratové hodnoty shellu (exit code)

Pokud se pro překlad assemblerovského programu použije nástroj gcc, bude kostra aplikace vypadat poněkud odlišně v porovnání s programy, s nimiž jsme se až doposud setkávali. Je tomu tak z toho důvodu, že gcc implicitně vytvoří vstupní bod (entry point) do programu, který je označen návěštím _start a obsahuje nativní kód získaný z objektového souboru crt0.o. Tento kód volá funkci main, což v praxi znamená, že kostra našeho assemblerovského programu se nepatrně změní a z pohledu programátora zjednoduší. Připomeňme si, že původně vypadala kostra následovně (tučně je zvýrazněno návěští představující vstupní bod do aplikace):

# asmsyntax=as

.intel_syntax noprefix


# Linux kernel system call table
sys_exit=1



#-----------------------------------------------------------------------------
.section .data



#-----------------------------------------------------------------------------
.section .bss



#-----------------------------------------------------------------------------
.section .text
        .global _start               # tento symbol ma byt dostupny i linkeru

_start:
        mov   eax, sys_exit          # cislo sycallu pro funkci "exit"
        mov   ebx, 0                 # exit code = 0
        int   0x80                   # volani Linuxoveho kernelu

Nyní (tedy při překladu s využitím gcc) dojde ke změně názvu programátorem vytvářené funkce, protože namísto návěští _start:

.section .text
        .global _start               # tento symbol ma byt dostupny i linkeru

_start:

Se použije návěští main:

.section .text
        .global main               # tento symbol ma byt dostupny i linkeru

main:

Navíc jsme dříve ukončovali program explicitním zavoláním syscallu a předáním návratového kódu v registru EBX, tedy následujícím způsobem:

mov   eax, sys_exit          # cislo sycallu pro funkci "exit"
mov   ebx, 0                 # exit code = 0
int   0x80                   # volani Linuxoveho kernelu

Nová podoba kódu je pro programátora jednodušší, protože funkci main může opustit instrukcí RET (návrat ze subrutiny), přičemž návratový kód (exit code) je předán v registru EAX:

xor  eax, eax              # navratova hodnota (exit status)
ret                        # ukonceni aplikace

Poznámka: použití souboru crt0.o je sice možné obejít a spolehnout se na vlastní kód, ve skutečnosti je však poměrně příjemné, že za nás kód naprogramovaný v crt0.o provede jak základní inicializaci, tak i zpracování vstupních parametrů atd.

4. První příklad – kostra aplikace s funkcí main naprogramovanou v assembleru

První demonstrační příklad je velmi primitivní, protože po svém spuštění pouze nastaví hodnotu registru EAX na nulu (využití instrukce xor je často používaným trikem, v assembleru však již spíše idiomem) a vyskočí ze subrutiny instrukcí ret:

# asmsyntax=as

# Program pro otestovani volani funkci ze standardni knihovny jazyka C
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky

.intel_syntax noprefix



#-----------------------------------------------------------------------------
.section .data



#-----------------------------------------------------------------------------
.section .text
        .global main               # tento symbol ma byt dostupny i linkeru

main:

        xor  eax, eax              # navratova hodnota (exit status)
        ret                        # ukonceni aplikace

Překlad a slinkování se provede tímto příkazem:

gcc main.s

Výsledkem by měl být soubor a.out o velikosti přibližně 8,5 kB, který je však možné zmenšit příkazem strip na přibližně 6,2 kB.

Disassemblerem se můžeme podívat na interní strukturu, která je již poněkud složitá, ale funkci main i sekci _start zde nalezneme:

objdump -M intel-mnemonic -f -d -t -h a.out
Disassembly of section .init:

00000000004003a8 <_init>:
  4003a8:       48 83 ec 08             sub    rsp,0x8
  4003ac:       48 8b 05 45 0c 20 00    mov    rax,QWORD PTR [rip+0x200c45]        # 600ff8 <_DYNAMIC+0x1d0>
  4003b3:       48 85 c0                test   rax,rax
  4003b6:       74 05                   je     4003bd <_init+0x15>
  4003b8:       e8 33 00 00 00          call   4003f0 <[email protected]>
  4003bd:       48 83 c4 08             add    rsp,0x8
  4003c1:       c3                      ret    

Disassembly of section .plt:

00000000004003d0 <[email protected]>:
  4003d0:       ff 35 32 0c 20 00       push   QWORD PTR [rip+0x200c32]        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4003d6:       ff 25 34 0c 20 00       jmp    QWORD PTR [rip+0x200c34]        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4003dc:       0f 1f 40 00             nop    DWORD PTR [rax+0x0]

00000000004003e0 <[email protected]>:
  4003e0:       ff 25 32 0c 20 00       jmp    QWORD PTR [rip+0x200c32]        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  4003e6:       68 00 00 00 00          push   0x0
  4003eb:       e9 e0 ff ff ff          jmp    4003d0 <_init+0x28>

00000000004003f0 <[email protected]>:
  4003f0:       ff 25 2a 0c 20 00       jmp    QWORD PTR [rip+0x200c2a]        # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
  4003f6:       68 01 00 00 00          push   0x1
  4003fb:       e9 d0 ff ff ff          jmp    4003d0 <_init+0x28>

Disassembly of section .text:

0000000000400400 <_start>:
  400400:       31 ed                   xor    ebp,ebp
  400402:       49 89 d1                mov    r9,rdx
  400405:       5e                      pop    rsi
  400406:       48 89 e2                mov    rdx,rsp
  400409:       48 83 e4 f0             and    rsp,0xfffffffffffffff0
  40040d:       50                      push   rax
  40040e:       54                      push   rsp
  40040f:       49 c7 c0 60 05 40 00    mov    r8,0x400560
  400416:       48 c7 c1 f0 04 40 00    mov    rcx,0x4004f0
  40041d:       48 c7 c7 ed 04 40 00    mov    rdi,0x4004ed
  400424:       e8 b7 ff ff ff          call   4003e0 <[email protected]>
  400429:       f4                      hlt    
  40042a:       66 0f 1f 44 00 00       nop    WORD PTR [rax+rax*1+0x0]

0000000000400430 <deregister_tm_clones>:
  400430:       b8 3f 10 60 00          mov    eax,0x60103f
  400435:       55                      push   rbp
  400436:       48 2d 38 10 60 00       sub    rax,0x601038
  40043c:       48 83 f8 0e             cmp    rax,0xe
  400440:       48 89 e5                mov    rbp,rsp
  400443:       77 02                   ja     400447 <deregister_tm_clones+0x17>
  400445:       5d                      pop    rbp
  400446:       c3                      ret    
  400447:       b8 00 00 00 00          mov    eax,0x0
  40044c:       48 85 c0                test   rax,rax
  40044f:       74 f4                   je     400445 <deregister_tm_clones+0x15>
  400451:       5d                      pop    rbp
  400452:       bf 38 10 60 00          mov    edi,0x601038
  400457:       ff e0                   jmp    rax
  400459:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]

0000000000400460 <register_tm_clones>:
  400460:       b8 38 10 60 00          mov    eax,0x601038
  400465:       55                      push   rbp
  400466:       48 2d 38 10 60 00       sub    rax,0x601038
  40046c:       48 c1 f8 03             sar    rax,0x3
  400470:       48 89 e5                mov    rbp,rsp
  400473:       48 89 c2                mov    rdx,rax
  400476:       48 c1 ea 3f             shr    rdx,0x3f
  40047a:       48 01 d0                add    rax,rdx
  40047d:       48 d1 f8                sar    rax,1
  400480:       75 02                   jne    400484 <register_tm_clones+0x24>
  400482:       5d                      pop    rbp
  400483:       c3                      ret    
  400484:       ba 00 00 00 00          mov    edx,0x0
  400489:       48 85 d2                test   rdx,rdx
  40048c:       74 f4                   je     400482 <register_tm_clones+0x22>
  40048e:       5d                      pop    rbp
  40048f:       48 89 c6                mov    rsi,rax
  400492:       bf 38 10 60 00          mov    edi,0x601038
  400497:       ff e2                   jmp    rdx
  400499:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]

00000000004004a0 <__do_global_dtors_aux>:
  4004a0:       80 3d 91 0b 20 00 00    cmp    BYTE PTR [rip+0x200b91],0x0        # 601038 <__TMC_END__>
  4004a7:       75 11                   jne    4004ba <__do_global_dtors_aux+0x1a>
  4004a9:       55                      push   rbp
  4004aa:       48 89 e5                mov    rbp,rsp
  4004ad:       e8 7e ff ff ff          call   400430 <deregister_tm_clones>
  4004b2:       5d                      pop    rbp
  4004b3:       c6 05 7e 0b 20 00 01    mov    BYTE PTR [rip+0x200b7e],0x1        # 601038 <__TMC_END__>
  4004ba:       f3 c3                   repz ret 
  4004bc:       0f 1f 40 00             nop    DWORD PTR [rax+0x0]

00000000004004c0 <frame_dummy>:
  4004c0:       48 83 3d 58 09 20 00    cmp    QWORD PTR [rip+0x200958],0x0        # 600e20 <__JCR_END__>
  4004c7:       00 
  4004c8:       74 1e                   je     4004e8 <frame_dummy+0x28>
  4004ca:       b8 00 00 00 00          mov    eax,0x0
  4004cf:       48 85 c0                test   rax,rax
  4004d2:       74 14                   je     4004e8 <frame_dummy+0x28>
  4004d4:       55                      push   rbp
  4004d5:       bf 20 0e 60 00          mov    edi,0x600e20
  4004da:       48 89 e5                mov    rbp,rsp
  4004dd:       ff d0                   call   rax
  4004df:       5d                      pop    rbp
  4004e0:       e9 7b ff ff ff          jmp    400460 <register_tm_clones>
  4004e5:       0f 1f 00                nop    DWORD PTR [rax]
  4004e8:       e9 73 ff ff ff          jmp    400460 <register_tm_clones>

00000000004004ed <main>:
  4004ed:       31 c0                   xor    eax,eax
  4004ef:       c3                      ret    

00000000004004f0 <__libc_csu_init>:
  4004f0:       41 57                   push   r15
  4004f2:       41 89 ff                mov    r15d,edi
  4004f5:       41 56                   push   r14
  4004f7:       49 89 f6                mov    r14,rsi
  4004fa:       41 55                   push   r13
  4004fc:       49 89 d5                mov    r13,rdx
  4004ff:       41 54                   push   r12
  400501:       4c 8d 25 08 09 20 00    lea    r12,[rip+0x200908]        # 600e10 <__frame_dummy_init_array_entry>
  400508:       55                      push   rbp
  400509:       48 8d 2d 08 09 20 00    lea    rbp,[rip+0x200908]        # 600e18 <__init_array_end>
  400510:       53                      push   rbx
  400511:       4c 29 e5                sub    rbp,r12
  400514:       31 db                   xor    ebx,ebx
  400516:       48 c1 fd 03             sar    rbp,0x3
  40051a:       48 83 ec 08             sub    rsp,0x8
  40051e:       e8 85 fe ff ff          call   4003a8 <_init>
  400523:       48 85 ed                test   rbp,rbp
  400526:       74 1e                   je     400546 <__libc_csu_init+0x56>
  400528:       0f 1f 84 00 00 00 00    nop    DWORD PTR [rax+rax*1+0x0]
  40052f:       00 
  400530:       4c 89 ea                mov    rdx,r13
  400533:       4c 89 f6                mov    rsi,r14
  400536:       44 89 ff                mov    edi,r15d
  400539:       41 ff 14 dc             call   QWORD PTR [r12+rbx*8]
  40053d:       48 83 c3 01             add    rbx,0x1
  400541:       48 39 eb                cmp    rbx,rbp
  400544:       75 ea                   jne    400530 <__libc_csu_init+0x40>
  400546:       48 83 c4 08             add    rsp,0x8
  40054a:       5b                      pop    rbx
  40054b:       5d                      pop    rbp
  40054c:       41 5c                   pop    r12
  40054e:       41 5d                   pop    r13
  400550:       41 5e                   pop    r14
  400552:       41 5f                   pop    r15
  400554:       c3                      ret    
  400555:       66 66 2e 0f 1f 84 00    data32 nop WORD PTR cs:[rax+rax*1+0x0]
  40055c:       00 00 00 00 

0000000000400560 <__libc_csu_fini>:
  400560:       f3 c3                   repz ret 

Disassembly of section .fini:

0000000000400564 <_fini>:
  400564:       48 83 ec 08             sub    rsp,0x8
  400568:       48 83 c4 08             add    rsp,0x8
  40056c:       c3                      ret    

5. Volání knihovní funkce puts

Způsob překladu i slinkování aplikace již známe, dokonce i víme, jak má vypadat kostra assemblerovské aplikace překládané s využitím nástroje gcc. Podívejme se tedy, jakým způsobem lze volat nějakou knihovní funkci ze standardní céčkovské knihovny. Na první pohled to není nic těžkého, protože se funkce zavolá instrukcí CALL, které se předá návěští, jehož jméno odpovídá jménu funkce. To znamená, že například standardní funkci puts() (výpis řetězce na standardní výstup) můžeme zavolat instrukcí CALL puts. Druhý problém, který je nutné vyřešit, spočívá v tom, jak funkcím předávat parametry. Na platformě x86-64 se pro předání prvních šesti celočíselných parametrů nebo adres používají registry, a to v tomto pořadí: RDI, RSI, RDX, RCX, R8 a R9. Pokud je zapotřebí předat hodnotu s plovoucí řádovou čárkou, použijí se registry XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 a XMM7.

Funkci puts() se předává jediný parametr, kterým je počáteční adresa řetězce, který se má vytisknout. Tuto adresu nejprve načteme do registru RDI (první parametr). Povšimněte si, že řetězec je alokován pomocí deklarace .asciiz zajišťující mj. i uložení nuly za konec řetězce. To je céčkovskou funkcí puts() vyžadováno. První verze programu typu „Hello world!“ by mohla vypadat následovně:

#-----------------------------------------------------------------------------
.section .data
hello_world_message:
        .asciz "Hello world!\n"    # zprava, ktera se ma vytisknout na standardni vystup



#-----------------------------------------------------------------------------
.section .text
        .global main               # tento symbol ma byt dostupny i linkeru

main:
                                   # jedinym parametrem je adresa zpravy
        mov  rdi, offset hello_world_message
        call puts                  # volani funkce 'puts' ze standardni knihovny

6. Zarovnání vrcholu zásobníku na adresu dělitelnou šestnácti

Volání funkce puts() sice bude fungovat, ale není zcela korektní, což by se mohlo projevit při volání složitějších funkcí s více parametry. Je tomu tak z toho důvodu, že na platformě x86-64 je nutné, aby byl při volání funkcí vrchol zásobníku zarovnán na celočíselný násobek šestnácti. A právě zde leží jádro problému – již při volání subrutiny main je registr RSP ukazující na vrchol zásobníku posunut o osm bajtů, protože na zásobník bylo nutné uložit návratovou adresu volajícího kódu. Proto musíme ještě před voláním funkce puts() na zásobník uložit dalších osm bajtů a po návratu z této funkce obnovit původní hodnotu registru RSP. Řešení je jednoduché – RSP se nejprve zmenší o hodnotu osm (zásobník totiž roste směrem k nižším adresám) a po návratu z této funkce se hodnota naopak o osmičku zvětší. Korektní kód tedy bude vypadat následovně:

#-----------------------------------------------------------------------------
.section .data
hello_world_message:
        .asciz "Hello world!\n"    # zprava, ktera se ma vytisknout na standardni vystup



#-----------------------------------------------------------------------------
.section .text
        .global main               # tento symbol ma byt dostupny i linkeru

main:
        sub  rsp, 8                # zajistit zarovnani RSP na adresu delitelnou 16

                                   # jedinym parametrem je adresa zpravy
        mov  rdi, offset hello_world_message
        call puts                  # volani funkce 'puts' ze standardni knihovny

        add  rsp, 8                # obnoveni puvodni hodnoty RSP

7. Druhý příklad – výpis řetězce na obrazovku pomocí funkce puts

Korektní varianta programu typu „Hello world!“ vypadá následovně. Pro překlad tohoto programu opět použijte nástroj gcc a nikoli as:

# asmsyntax=as

# Program pro otestovani volani funkci ze standardni knihovny jazyka C
# - volani funkce 'puts'
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky

.intel_syntax noprefix



#-----------------------------------------------------------------------------
.section .data
hello_world_message:
        .asciz "Hello world!\n"    # zprava, ktera se ma vytisknout na standardni vystup



#-----------------------------------------------------------------------------
.section .text
        .global main               # tento symbol ma byt dostupny i linkeru

main:
        sub  rsp, 8                # zajistit zarovnani RSP na adresu delitelnou 16

                                   # jedinym parametrem je adresa zpravy
        mov  rdi, offset hello_world_message
        call puts                  # volani funkce 'puts' ze standardni knihovny

        add  rsp, 8                # obnoveni puvodni hodnoty RSP

        xor  eax, eax              # navratova hodnota (exit status)
        ret                        # ukonceni aplikace

8. Kontrola zarovnání v debuggeru (GDB)

Pojďme si nyní zkontrolovat, zda instrukce pro zarovnání vrcholu zásobníku na adresu dělitelnou šestnácti skutečně fungují. Nejdříve si upravený program přeložíme, a to s použitím volby -g, aby se do výsledného kódu vložily i ladicí informace:

gcc -g main_64bit.s

Po úspěšném překladu můžeme spustit interaktivní debugger, což zajišťuje následující příkaz:

gdb a.out
...
...
...
Reading symbols from a.out...done.
(gdb)

Pokud se namísto posledního řádku vypíše toto chybové hlášení:

Reading symbols from a.out...(no debugging symbols found)...done.

znamená to, že při překladu nebyl použit přepínač -g.

Dále již můžeme pokračovat v ladění. Nastavíme breakpoint na první instrukci v subrutině main:

(gdb) break main
Breakpoint 1 at 0x40052d: file main_64bit.s, line 25.

Laděnou aplikaci spustíme, po spuštění se aplikace zastaví na nastaveném breakpointu:

(gdb) run
Starting program: /home/tester/44_stdc_puts/a.out 

Breakpoint 1, main () at main_64bit.s:25
25              sub  rsp, 8                # zajistit zarovnani RSP na adresu delitelnou 16

Vypíšeme si informace o všech registrech. Zajímat nás samozřejmě bude především obsah registru RSP:

(gdb) info registers
rax            0x40052d 4195629
rbx            0x0      0
rcx            0x0      0
rdx            0x7fffffffe118   140737488347416
rsi            0x7fffffffe108   140737488347400
rdi            0x1      1
rbp            0x0      0x0
rsp            0x7fffffffe028   0x7fffffffe028
r8             0x7ffff7dd4e80   140737351863936
r9             0x7ffff7dea530   140737351951664
r10            0x7fffffffdeb0   140737488346800
r11            0x7ffff7a36e50   140737348070992
r12            0x400440 4195392
r13            0x7fffffffe100   140737488347392
r14            0x0      0
r15            0x0      0
rip            0x40052d 0x40052d <main>
eflags         0x246    [ PF ZF IF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0

Vidíme, že registr RSP skutečně neobsahuje hodnotu dělitelnou šestnácti, ovšem po provedení další instrukce příkazem step se situace změní:

(gdb) step
main () at main_64bit.s:28
28              mov  rdi, offset hello_world_message

Opět si vypíšeme obsah všech registrů:

(gdb) info registers
rax            0x40052d 4195629
rbx            0x0      0
rcx            0x0      0
rdx            0x7fffffffe118   140737488347416
rsi            0x7fffffffe108   140737488347400
rdi            0x1      1
rbp            0x0      0x0
rsp            0x7fffffffe020   0x7fffffffe020
r8             0x7ffff7dd4e80   140737351863936
r9             0x7ffff7dea530   140737351951664
r10            0x7fffffffdeb0   140737488346800
r11            0x7ffff7a36e50   140737348070992
r12            0x400440 4195392
r13            0x7fffffffe100   140737488347392
r14            0x0      0
r15            0x0      0
rip            0x400531 0x400531 <main+4>
eflags         0x202    [ IF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0

Nyní již RSP obsahuje adresu dělitelnou šestnácti, což se v hexadecimálním výpisu snadno pozná podle nuly v nejnižší cifře.

9. Volání knihovní funkce printf a význam obsahu registru AL

Způsobem, který jsme si popsali v předchozích kapitolách, je možné volat většinu funkcí ze standardní céčkové knihovny. Stačí si zapamatovat pořadí registrů pro předávání parametrů:

  1. Celočíselné parametry a adresy: RDI, RSI, RDX, RCX, R8 a R9
  2. Hodnoty single(float) a double: XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 a XMM7

Výjimkou jsou funkce s proměnným počtem parametrů. U těch je totiž ještě nutné v registru RAX (ve skutečnosti však jen v AL, tj. spodních osmi bitech) předat počet předávaných hodnot typu float či double. Pokud na nastavení tohoto registru zapomenete, skončí většinou pokus o zavolání funkcí s proměnným počtem parametrů (typickým příkladem je printf()) pádem aplikace!

10. Třetí příklad – výpis řetězce na obrazovku pomocí funkce printf

Se znalostí nutnosti nastavení registru AL (či celého registru EAX) na hodnotu odpovídající počtu parametrů typu float/double předávaných variadické funkci můžeme vytvořit demonstrační příklad, v němž se volá variadická funkce printf(). Této funkci se předá jediný parametr představující řetězec, který se má vytisknout. V řetězci nejsou uvedeny žádné speciální formátovací znaky, takže se funkci printf() nemusí předávat žádné další parametry. Zdrojový kód vypadá následovně:

# asmsyntax=as

# Program pro otestovani volani funkci ze standardni knihovny jazyka C
# - volani funkce 'printf'
# - pro zapis je pouzita "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky

.intel_syntax noprefix



#-----------------------------------------------------------------------------
.section .data
hello_world_message:
        .asciz "Hello world!\n"    # zprava, ktera se ma vytisknout na standardni vystup



#-----------------------------------------------------------------------------
.section .text
        .global main               # tento symbol ma byt dostupny i linkeru

main:
        sub  rsp, 8                # zajistit zarovnani RSP na adresu delitelnou 16

                                   # jedinym parametrem je adresa zpravy
        mov  rdi, offset hello_world_message
        xor  al, al                # pocet parametru predanych ve vektorovych registrech
        call printf                # volani funkce 'printf' ze standardni knihovny

        add  rsp, 8                # obnoveni puvodni hodnoty RSP

        xor  eax, eax              # navratova hodnota (exit status)

        ret                        # ukonceni aplikace

Podívejme se na to, jak se funkce (či subrutina) main přeloží do strojového kódu:

000000000040052d <main>:
  40052d:       48 83 ec 08             sub    rsp,0x8
  400531:       48 c7 c7 40 10 60 00    mov    rdi,0x601040
  400538:       30 c0                   xor    al,al
  40053a:       e8 d1 fe ff ff          call   400410 <[email protected]>
  40053f:       48 83 c4 08             add    rsp,0x8
  400543:       31 c0                   xor    eax,eax
  400545:       c3                      ret    
  400546:       66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  40054d:       00 00 00 

Poznámka: poslední dva řádky jsou ve skutečnosti pouze „smetí“ vzniklé zarovnáním kódu a nikoli reálné instrukce, které by se skutečně spouštěly (ostatně instrukce NOP s prapodivným adresováním je skutečně nesmyslná).

11. 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 obsahující odkazy na zdrojové kódy příkladů i na již zmíněné podpůrné skripty:

První demonstrační příklad: šablona pro překlad nástrojem gcc

# Soubor Popis Odkaz do repositáře
1 main.s hlavní program pro procesory x86-64 https://github.com/tisnik/presentations/blob/master/assembler/43_stdc_stub/main.s
2 assemble skript pro překlad s využitím gcc https://github.com/tisnik/presentations/blob/master/assembler/43_stdc_stub/assemble
3 disassemble skript pro disassembling https://github.com/tisnik/presentations/blob/master/assembler/43_stdc_stub/disassemble

Druhý demonstrační příklad: volání knihovní funkce puts()

# Soubor Popis Odkaz do repositáře
1 main_64bit.s hlavní program pro procesory x86-64 https://github.com/tisnik/presentations/blob/master/assembler/44_stdc_puts/main_64bit.s
2 assemble_64bit skript pro překlad s využitím gcc https://github.com/tisnik/presentations/blob/master/assembler/44_stdc_puts/assemble_64bit
3 disassemble skript pro disassembling https://github.com/tisnik/presentations/blob/master/assembler/44_stdc_puts/disassemble

Druhý demonstrační příklad: volání knihovní funkce printf()

# Soubor Popis Odkaz do repositáře
1 main.s hlavní program pro procesory x86-64 https://github.com/tisnik/presentations/blob/master/assembler/45_stdc_printf/main_64bit.s
2 assemble skript pro překlad s využitím gcc https://github.com/tisnik/presentations/blob/master/assembler/45_stdc_printf/assemble_64bit
3 disassemble skript pro disassembling https://github.com/tisnik/presentations/blob/master/assembler/45_stdc_printf/disassemble

12. Odkazy na Internetu

  1. C Functions Without Arguments
    https://eklitzke.org/c-functions-without-arguments
  2. GNU Assembler Examples
    http://cs.lmu.edu/~ray/notes/gasexamples/
  3. Simply FPU
    http://www.website.masmforum.com/tutorials/fptute/
  4. Art of Assembly language programming: The 80×87 Floating Point Coprocessors
    https://courses.engr.illinois.edu/ece390/books/artofasm/CH14/CH14-3.html
  5. Art of Assembly language programming: The FPU Instruction Set
    https://courses.engr.illinois.edu/ece390/books/artofasm/CH14/CH14-4.html
  6. INTEL 80387 PROGRAMMER’S REFERENCE MANUAL
    http://www.ragestorm.net/downloads/387intel.txt
  7. x86 Instruction Set Reference: FLD
    http://x86.renejeschke.de/html/file_module_x86_id_100.html
  8. x86 Instruction Set Reference: FLD1/FLDL2T/FLDL2E/FLDPI/FLDLG2/FLDLN2/FLDZ
    http://x86.renejeschke.de/html/file_module_x86_id_101.html
  9. x86 Instruction Set Reference: FLD
    http://x86.renejeschke.de/html/file_module_x86_id_100.html
  10. x86 Instruction Set Reference: FST/FSTP
    http://x86.renejeschke.de/html/file_module_x86_id_117.html
  11. x86 Instruction Set Reference: FADD/FADDP/FIADD
    http://x86.renejeschke.de/html/file_module_x86_id_81.html
  12. x86 Instruction Set Reference: FSUB/FSUBP/FISUB
    http://x86.renejeschke.de/html/file_module_x86_id_121.html
  13. x86 Instruction Set Reference: FDIV/FDIVP/FIDIV
    http://x86.renejeschke.de/html/file_module_x86_id_91.html
  14. x86 Instruction Set Reference: BT
    http://x86.renejeschke.de/html/file_module_x86_id_22.html
  15. x86 Instruction Set Reference: BTC
    http://x86.renejeschke.de/html/file_module_x86_id_23.html
  16. x86 Instruction Set Reference: BTR
    http://x86.renejeschke.de/html/file_module_x86_id_24.html
  17. x86 Instruction Set Reference: BTS
    http://x86.renejeschke.de/html/file_module_x86_id_25.html
  18. x86 Instruction Set Reference: BSF
    http://x86.renejeschke.de/html/file_module_x86_id_19.html
  19. x86 Instruction Set Reference: BSR
    http://x86.renejeschke.de/html/file_module_x86_id_20.html
  20. x86 Instruction Set Reference: BSWAP
    http://x86.renejeschke.de/html/file_module_x86_id_21.html
  21. x86 Instruction Set Reference: XCHG
    http://x86.renejeschke.de/html/file_module_x86_id_328.html
  22. x86 Instruction Set Reference: SETcc
    http://x86.renejeschke.de/html/file_module_x86_id_288.html
  23. X86 Assembly/Arithmetic
    https://en.wikibooks.org/wiki/X86_Assembly/Arithmetic
  24. Art of Assembly – Arithmetic Instructions
    http://oopweb.com/Assembly/Documents/ArtOfAssembly/Volume/Chapter_6/CH06-2.html
  25. The GNU Assembler Tutorial
    http://tigcc.ticalc.org/doc/gnuasm.html
  26. The GNU Assembler – macros
    http://tigcc.ticalc.org/doc/gnuasm.html#SEC109
  27. ARM subroutines & program stack
    http://www.toves.org/books/armsub/
  28. Generating Mixed Source and Assembly List using GCC
    http://www.systutorials.com/240/generate-a-mixed-source-and-assembly-listing-using-gcc/
  29. Calling subroutines
    http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.kui0100a/armasm_cihcfigg.htm
  30. ARM Assembly Language Programming
    http://peter-cockerell.net/aalp/html/frames.html
  31. ASM Flags
    http://www.cavestory.org/guides/csasm/guide/asm_flags.html
  32. Status Register
    https://en.wikipedia.org/wiki/Status_register
  33. Intel x86 JUMP quick reference
    http://unixwiz.net/techtips/x86-jumps.html
  34. Linux assemblers: A comparison of GAS and NASM
    http://www.ibm.com/developerworks/library/l-gas-nasm/index.html
  35. Programovani v assembleru na OS Linux
    http://www.cs.vsb.cz/grygarek/asm/asmlinux.html
  36. Is it worthwhile to learn x86 assembly language today?
    https://www.quora.com/Is-it-worthwhile-to-learn-x86-assembly-language-today?share=1
  37. Why Learn Assembly Language?
    http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language
  38. Is Assembly still relevant?
    http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant
  39. Why Learning Assembly Language Is Still a Good Idea
    http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html
  40. Assembly language today
    http://beust.com/weblog/2004/06/23/assembly-language-today/
  41. Assembler: Význam assembleru dnes
    http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz
  42. Assembler pod Linuxem
    http://phoenix.inf.upol.cz/linux/prog/asm.html
  43. AT&T Syntax versus Intel Syntax
    https://www.sourceware.org/binutils/docs-2.12/as.info/i386-Syntax.html
  44. Linux Assembly website
    http://asm.sourceforge.net/
  45. Using Assembly Language in Linux
    http://asm.sourceforge.net/articles/linasm.html
  46. vasm
    http://sun.hasenbraten.de/vasm/
  47. vasm – dokumentace
    http://sun.hasenbraten.de/vasm/release/vasm.html
  48. The Yasm Modular Assembler Project
    http://yasm.tortall.net/
  49. 680×0:AsmOne
    http://www.amigacoding.com/index.php/680×0:AsmOne
  50. ASM-One Macro Assembler
    http://en.wikipedia.org/wiki/ASM-One_Macro_Assembler
  51. ASM-One pages
    http://www.theflamearrows.info/documents/asmone.html
  52. Základní informace o ASM-One
    http://www.theflamearrows.info/documents/asminfo.html
  53. Linux Syscall Reference
    http://syscalls.kernelgrok.com/
  54. Programming from the Ground Up Book – Summary
    http://savannah.nongnu.org/projects/pgubook/
  55. IBM System 360/370 Compiler and Historical Documentation
    http://www.edelweb.fr/Simula/
  56. IBM 700/7000 series
    http://en.wikipedia.org/wiki/IBM_700/7000_series
  57. IBM System/360
    http://en.wikipedia.org/wiki/IBM_System/360
  58. IBM System/370
    http://en.wikipedia.org/wiki/IBM_System/370
  59. Mainframe family tree and chronology
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_FT1.html
  60. 704 Data Processing System
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP704.html
  61. 705 Data Processing System
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_PP705.html
  62. The IBM 704
    http://www.columbia.edu/acis/history/704.html
  63. IBM Mainframe album
    http://www-03.ibm.com/ibm/history/exhibits/mainframe/mainframe_album.html
  64. Osmibitové muzeum
    http://osmi.tarbik.com/
  65. Tesla PMI-80
    http://osmi.tarbik.com/cssr/pmi80.html
  66. PMI-80
    http://en.wikipedia.org/wiki/PMI-80
  67. PMI-80
    http://www.old-computers.com/museum/computer.asp?st=1&c=1016
  68. The 6502 overflow flag explained mathematically
    http://www.righto.com/2012/12/the-6502-overflow-flag-explained.html
  69. X86 Opcode and Instruction Reference
    http://ref.x86asm.net/coder32.html