V šesté části seriálu o použití assembleru v Linuxu se seznámíme se základními způsoby manipulace se zásobníkem na mikroprocesorech s architekturami i386 a x86_64. Popíšeme si především instrukce CALL, RET, PUSH a POP i použití relativního adresování při přístupu k parametrům volaných funkcí, což je základ pro vytváření takzvaných zásobníkových rámců – stack frames.

Obsah

1. Použití assembleru v Linuxu: volání podprogramů a použití zásobníku

2. Strojová instrukce CALL

3. Strojová instrukce RET

4. Vytvoření podprogramů (subrutin) v assembleru

5. První demonstrační příklad: otestování chování instrukcí CALL a RET

6. Použití zásobníku pro předávání parametrů volané funkci

7. Meziuložení návratové adresy podprogramu v pracovním registru

8. Druhý demonstrační příklad: ukládání parametrů na zásobník

9. Relativní adresování hodnot uložených na zásobníku

10. Třetí demonstrační příklad: volací sekvence, v níž parametry odstraňuje volající kód

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

12. Odkazy na Internetu

1. Použití assembleru v Linuxu: volání podprogramů, použití zásobníku

V předchozích částech tohoto seriálu jsme se seznámili se základními instrukcemi, které nalezneme u většiny mikroprocesorových architektur. Připomeňme si, že se jednalo především o instrukce určené pro přenosy dat mezi pracovními registry, dále pak o instrukce používané pro načítání a ukládání dat z/do operační paměti, základní aritmetické instrukce (součet, rozdíl), instrukci pro porovnání dvou operandů (v podstatě rozdíl bez uložení výsledku) a v neposlední řadě jsme se zmínili i o podmíněných a nepodmíněných skocích i o jejich použití při implementaci programových smyček a rozhodovacích struktur.

To však samozřejmě není vše, protože nesmíme zapomenout na to, že programy, a to i mnohé programy psané v assembleru, jsou rozděleny do relativně samostatných subrutin (podprogramů) a procesory tedy musí obsahovat nějaké instrukce určené pro volání těchto subrutin, pro návrat ze subrutin a pro případné předání parametrů volaným subrutinám. Při volání subrutiny je nutné si nějakým způsobem zapamatovat adresu, na níž se řízení programu vrátí po provedení celé subrutiny. V závislosti na architektuře procesoru se návratové adresy ukládají buď na zásobník (i386, x86_64) či do takzvaného link registru (ARM a některé další RISCové architektury).

2. Strojová instrukce CALL

Pro implementaci podprogramů, které je možné v případě potřeby volat i rekurzivně, obsahuje většina mikroprocesorových architektur speciální instrukce určené pro skok do podprogramu a taktéž instrukci či instrukce starající se o návrat z podprogramu. Dnes si řekneme, jak jsou tyto instrukce implementovány na mikroprocesorech s architekturou i386 (a odvozeně i s architekturou x86_64). Instrukce pro skok do podprogramu se jmenuje CALL (význam tohoto jména je pravděpodobně zřejmý) a o návrat z podprogramu se stará instrukce RET (odvozeno od slova return).

Skok do podprogramu je realizován prakticky stejným způsobem jako běžný skok, což konkrétně znamená, že adresa zapsaná za operačním kódem instrukce je vložena do registru PC, čímž dojde k přemístění řízení na uvedenou adresu. Ovšem původní hodnota registru PC je ještě před provedením skoku uložena na zásobník, takže si mikroprocesor vlastně zapamatuje, na které adrese se před skokem do podprogramu nacházel (ukazatel na vrchol zásobníku je uschován v registru SP, s nímž se dnes ještě setkáme).

3. Strojová instrukce RET

Instrukce RET vyjme hodnotu uloženou na vrcholu zásobníku a přenese ji do pracovního registru PC (ve skutečnosti se ovšem během operací CALL/RET do tohoto registru přenese hodnota zvýšená o konstantu 5, protože samotná instrukce CALL má v 32bitovém režimu mikroprocesorů Intel délku přesně pěti bajtů). To znamená, že po provedení instrukce RET se řízení programu vrátí na adresu, která leží těsně za původní instrukcí CALL.

Pokud se volaný podprogram postará o automatickou obnovu obsahu všech pracovních registrů, je možné takový podprogram (subrutinu) zavolat prakticky kdekoli, a to bez strachu z toho, že se nějakým způsobem „pokazí“ běh hlavního programu (ostatně podobně by se měly v ideálním případě chovat i funkce či metody deklarované ve vyšších programovacích jazycích). Samozřejmě zbývá vyřešit problematiku předávání parametrů volným podprogramům a taktéž předávání návratové hodnoty (hodnot) zpět do volajícího kódu. Uvidíme, že se tento problém dá řešit hned několika různými způsoby.

Poznámka: instrukci RET je možné v případě potřeby použít ve funkci „aritmetického GOTO“. Jedná se o příkaz známý například z některých interpretů programovacího jazyka Basic, ve kterých bylo možné provádět skok na řádek, jehož číslo bylo vypočtené pomocí aritmetického výrazu (podobně se však používá i přímý skok realizovaný instrukcí JMP s pracovním registrem použitým namísto konstantní adresy). V dobách masivního používání ručně optimalizovaného assembleru se tato technika využívala poměrně často, například při implementaci rozhodovacích tabulek (decision tables), popřípadě jako náhrada za strukturovaný příkaz typu switch-case.

Tento způsob využití (či spíše přesněji řečeno většinou zneužití) zásobníku je v některých případech použit například i při pokusu o napadení nativních aplikací, kdy se útočník snaží změnit vstupní data takovým způsobem, aby se poškodila návratová adresa uložená na zásobníku a následně se provedl skok na nějakou pro útočníka „zajímavou“ funkci (login atd.).

4. Vytvoření podprogramů (subrutin) v assembleru

Strojové instrukce CALL a RET jsou použity i v dnešním prvním demonstračním příkladu. Samotné tělo tohoto demonstračního příkladu je tvořeno pouhými třemi instrukcemi, které postupně zavolají podprogramy určené pro vytištění první zprávy na standardní výstup, následně pro vytištění druhé zprávy (opět na standardní výstup) a nakonec se zavolá podprogram, který celý proces ukončí (samozřejmě s využitím příslušného syscallu). Ve skutečnosti je zajištěno, že se z tohoto posledního podprogramu již řízení zpět nikdy nevrátí, takže by zde mohla být namísto instrukce CALL použita instrukce JMP pro provedení nepodmíněného skoku.

Z úryvku zdrojového kódu je zřejmé, jak se instrukce CALL zapisuje společně s návěštím podprogramu (konkrétní adresa, na níž je podprogram uložen, se vypočte automaticky assemblerem a uvidíme ji při pohledu do vygenerovaného objektového kódu):

call  writeFirstMessage      # zavolani podprogramu pro vytisteni prvni zpravy
call  writeSecondMessage     # zavolani podprogramu pro vytisteni druhe zpravy
call  exit                   # zavolani podprogramu pro ukonceni procesu

Jednotlivé podprogramy vždy musí končit instrukcí RET, která zajistí přečtení návratové adresy ze zásobníku a skok na tuto adresu. Příkladem je podprogram určený pro vytištění první zprávy, který naplní pracovní registry ECX a EDX a nakonec zavolá další podprogram:

writeFirstMessage:
        mov   ecx, offset message1   # adresa retezce, ktery se ma vytisknout
        mov   edx, message1len       # pocet znaku, ktere se maji vytisknout
        call  writeMessage           # zavolani podprogramu pro vytisteni zpravy
        ret                          # navrat z podprogramu

Podprogram začínající na návěští writeMessage očekává, že v pracovních registrech ECX a EDX bude předána adresa zprávy a její délka. Následně se naplní obsahy dalších dvou pracovních registrů EAX a EBX a provede se syscall (zavolání služby jádra operačního systému):

writeMessage:
        mov   eax, sys_write         # cislo syscallu pro funkci "write"
        mov   ebx, std_output        # standardni vystup
        int   0x80                   # volani Linuxoveho kernelu
        ret                          # navrat z podprogramu

5. První demonstrační příklad: otestování chování instrukcí CALL a RET

Výše uvedené fragmenty podprogramů jsou použity v dnešním prvním demonstračním příkladu, jehož úplný zdrojový kód je vypsán pod tímto odstavcem:

# asmsyntax=as

# Program pro otestovani chovani instrukci CALL a RET
# - pouzita je "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky

.intel_syntax noprefix


# Linux kernel system call table
sys_exit   = 1
sys_write  = 4

# Dalsi konstanty pouzite v programu - standardni streamy
std_input  = 0
std_output = 1



#-----------------------------------------------------------------------------
.section .data
message1:                          # adresa prvni zpravy
        .string "Hello World\n"
message1len = $ - message1 - 1     # delka prvni zpravy

message2:                          # adresa druhe zpravy
        .string "Assembler je fajn\n"
message2len = $ - message2 - 1     # delka druhe zpravy



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



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

_start:
        call  writeFirstMessage      # zavolani podprogramu pro vytisteni prvni zpravy
        call  writeSecondMessage     # zavolani podprogramu pro vytisteni druhe zpravy
        call  exit                   # zavolani podprogramu pro ukonceni procesu



# Podprogram pro vytisteni prvni zpravy
writeFirstMessage:
        mov   ecx, offset message1   # adresa retezce, ktery se ma vytisknout
        mov   edx, message1len       # pocet znaku, ktere se maji vytisknout
        call  writeMessage           # zavolani podprogramu pro vytisteni zpravy
        ret                          # navrat z podprogramu



# Podprogram pro vytisteni druhe zpravy
writeSecondMessage:
        mov   ecx, offset message2   # adresa retezce, ktery se ma vytisknout
        mov   edx, message2len       # pocet znaku, ktere se maji vytisknout
        call  writeMessage           # zavolani podprogramu pro vytisteni zpravy
        ret                          # navrat z podprogramu



# Podprogram pro vytisteni zpravy na standardni vystup
# Ocekava se, ze v ecx bude adresa zpravy a v edx jeji delka
writeMessage:
        mov   eax, sys_write         # cislo syscallu pro funkci "write"
        mov   ebx, std_output        # standardni vystup
        int   0x80                   # volani Linuxoveho kernelu
        ret                          # navrat z podprogramu



# Podprogram pro ukonceni procesu zavolanim syscallu
exit:
        mov   eax, sys_exit          # cislo sycallu pro funkci "exit"
        mov   ebx, 0                 # exit code = 0
        int   0x80                   # volani Linuxoveho kernelu

# finito

Při překladu a linkování si musíme dát pozor na to, aby se generoval 32bitový kód. Je tomu tak z toho důvodu, že v 64bitovém režimu není možné na zásobník ukládat 32bitové konstanty. Příklad by samozřejmě bylo možné pro plně 64bitový systém upravit, ovšem této relativně obsáhlé problematice se budeme věnovat později (navíc bude 32bitový kód na 64bitovém systému fungovat). Povšimněte si, že v prvním kroku (spuštění assembleru) používáme přepínač –32 a ve druhém kroku (spuštění linkeru) naopak přepínač -m elf_i386:

as -g --32 subroutines-1.s -o subroutines-1.o
ld -m elf_i386 -s subroutines-1.o

Zajímavé bude se podívat na obsah vygenerovaného objektového kódu, zejména na tu část, v níž jsou použity instrukce CALL a RET:

objdump -M intel-mnemonic -f -d -t -h subroutines-1.o

subroutines-1.o:     file format elf32-i386
architecture: i386, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000048  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000020  00000000  00000000  0000007c  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  0000009c  2**0
                  ALLOC
SYMBOL TABLE:
00000000 l    d  .text	00000000 .text
00000000 l    d  .data	00000000 .data
00000000 l    d  .bss	00000000 .bss
00000001 l       *ABS*	00000000 sys_exit
00000004 l       *ABS*	00000000 sys_write
00000000 l       *ABS*	00000000 std_input
00000001 l       *ABS*	00000000 std_output
00000000 l       .data	00000000 message1
0000000c l       *ABS*	00000000 message1len
0000000d l       .data	00000000 message2
00000012 l       *ABS*	00000000 message2len
0000000f l       .text	00000000 writeFirstMessage
0000001f l       .text	00000000 writeSecondMessage
0000003c l       .text	00000000 exit
0000002f l       .text	00000000 writeMessage
00000000 g       .text	00000000 _start



Disassembly of section .text:

00000000 <_start>:
   0:	e8 0a 00 00 00       	call   f <writeFirstMessage>
   5:	e8 15 00 00 00       	call   1f <writeSecondMessage>
   a:	e8 2d 00 00 00       	call   3c <exit>

0000000f <writeFirstMessage>:
   f:	b9 00 00 00 00       	mov    ecx,0x0
  14:	ba 0c 00 00 00       	mov    edx,0xc
  19:	e8 11 00 00 00       	call   2f <writeMessage>
  1e:	c3                   	ret    

0000001f <writeSecondMessage>:
  1f:	b9 0d 00 00 00       	mov    ecx,0xd
  24:	ba 12 00 00 00       	mov    edx,0x12
  29:	e8 01 00 00 00       	call   2f <writeMessage>
  2e:	c3                   	ret    

0000002f <writeMessage>:
  2f:	b8 04 00 00 00       	mov    eax,0x4
  34:	bb 01 00 00 00       	mov    ebx,0x1
  39:	cd 80                	int    0x80
  3b:	c3                   	ret    

0000003c <exit>:
  3c:	b8 01 00 00 00       	mov    eax,0x1
  41:	bb 00 00 00 00       	mov    ebx,0x0
  46:	cd 80                	int    0x80

Povšimněte si rozdílného kódování instrukcí CALL pro volání subrutiny nazvané writeMessage. První volání nalezneme na adrese 0x00000019, druhé volání pak na adrese 0x00000029:

  19:	e8 11 00 00 00       	call   2f <writeMessage>

  29:	e8 01 00 00 00       	call   2f <writeMessage>

0000002f <writeMessage>:

Samotná subrutina writeMessage je uložena na adrese 0x0000002f, ovšem v prvním instrukčním slovu vidíme adresu 0x00000011 (zakódovanou v sekvenci bajtů 0x11 0x00 0x00 0x00, první bajt s hodnotou 0xe8 je kód instrukce CALL) a ve druhém instrukčním slovu vidíme adresu 0x00000001. Je tomu tak z toho důvodu, že se ve skutečnosti jedná o relativní adresu: v prvním případě 0x00000019+0x11=0x0000002a, což se od skutečné absolutní adresy 0x0000002f liší přesně o hodnotu 5, tedy o délku instrukce CALL. Podobně je tomu samozřejmě i ve druhém případě. Mimochodem – díky tomu, že se pro cíle skoků nepoužívají absolutní adresy, ale adresy relativní, je možné takové programy nebo jejich části přesouvat v operační paměti. Takovému kódu se říká pozičně nezávislý kód (position-independent code – PIC), i když je samozřejmě nutné dodržet i některé další požadavky.

Instrukce RET je naproti tomu až absurdně jednoduchá – je zakódována v jediném bajtu.

Poznámka: ve skutečnosti existuje hned několik adresovacích režimů podporovaných instrukcí CALL, viz též jejich popis na stránce https://pdos.csail.mit.edu/6.828/2007/readings/i386/CALL.htm.

6. Použití zásobníku pro předávání parametrů volané funkci

Problematika předávání parametrů podprogramům je většinou řešena buď předáváním parametrů na zásobníku či alternativně předáváním parametrů v pracovních registrech. Druhý způsob – použití pracovních registrů – jsme si již vlastně ukázali v předchozím demonstračním příkladu, takže se nyní podívejme, jak lze pro tento účel využít zásobník. Ve skutečnosti to není nic těžkého, protože instrukční soubor mikroprocesorů s architekturou i386 i x86_64 pro tento účel obsahuje dvojici instrukcí PUSH a POP. Instrukce PUSH slouží pro uložení konstanty či obsahu pracovního registru na vrchol zásobníku (ukazatel uložený v pracovním registru SP se automaticky změní).

Opakem instrukce PUSH je instrukce nazvaná POP, která naopak přenese data ze zásobníku do zvoleného pracovního registru. Předpokládejme například, že podprogram writeMessage očekává, že při jeho zavolání budou na zásobníku uloženy tři hodnoty: návratová adresa (vložená instrukcí CALL), počet znaků, které se mají vytisknout (tj. délka zprávy) a adresa zprávy. Připomeňme si, že zásobník je struktura typu LIFO (Last In, First Out), tj. naposledy vložená hodnota je ze zásobníku načtena jako první.

Volání podprogramu nazvaného writeMessage je tedy ve skutečnosti velmi jednoduché, pouze si musíme dát pozor na pořadí hodnot ukládaných na zásobník:

# Podprogram pro vytisteni prvni zpravy
writeFirstMessage:
        push  offset message1        # adresa retezce, ktery se ma vytisknout
        push  message1len            # pocet znaku, ktere se maji vytisknout
        call  writeMessage           # zavolani podprogramu pro vytisteni zpravy
        ret                          # navrat z podprogramu

7. Meziuložení návratové adresy podprogramu v pracovním registru

Poněkud složitější je situace uvnitř podprogramu, neboť zde potřebujeme ze zásobníku získat délku zprávy a její adresu. Při použití instrukce POP nám na vrcholu zásobníku „přebývá“ návratová adresa, kterou nesmíme ztratit, protože bude na konci podprogramu použita instrukcí RET. Jedno z možných řešení je následující – návratovou adresu si na chvíli uložíme do nějakého nepoužívaného pracovního registru (například EBP) a těsně před provedením instrukce RET obsah tohoto registru opět vložíme na zásobník. Výsledek sice není ideální, ale jeho předností je, že ho lze použít i na procesorech s omezenými adresovacími schopnostmi:

writeMessage:
        pop   ebp                    # ulozime na chvili navratovou adresu do EBP
        pop   edx                    # ziskame v poradi druhy ulozeny parametr
        pop   ecx                    # ziskame v poradi prvni ulozeny parametr
        mov   eax, sys_write         # cislo syscallu pro funkci "write"
        mov   ebx, std_output        # standardni vystup
        int   0x80                   # volani Linuxoveho kernelu
        push  ebp                    # obnovime navratovou adresu
        ret                          # navrat z podprogramu

Povšimněte si, že si při operaci se zásobníkem stále vystačíme pouze s instrukcemi PUSH a POP (nepřímo též CALL a RET). Zásobník je „vyvážený“ v tom smyslu, že hodnoty uložené do něj volajícím kódem jsou v podprogramu nazvaném writeMessage automaticky odstraněny, takže po návratu z tohoto podprogramu má zásobník opět takovou kapacitu, jako před uložením parametrů tohoto podprogramu.

8. Druhý demonstrační příklad: ukládání parametrů na zásobník

Podívejme se nyní na úplný zdrojový kód dnešního druhého demonstračního příkladu, v němž se používá výše popsané předávání parametrů volaným subrutinám přes zásobník:

# asmsyntax=as

# Program pro otestovani chovani instrukci CALL a RET spolecne
# s ukladanim parametru na zasobnik
# - pouzita je "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky

.intel_syntax noprefix


# Linux kernel system call table
sys_exit   = 1
sys_write  = 4

# Dalsi konstanty pouzite v programu - standardni streamy
std_input  = 0
std_output = 1



#-----------------------------------------------------------------------------
.section .data
message1:                          # adresa prvni zpravy
        .string "Hello World\n"
message1len = $ - message1 - 1     # delka prvni zpravy

message2:                          # adresa druhe zpravy
        .string "Assembler je fajn\n"
message2len = $ - message2 - 1     # delka druhe zpravy



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



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

_start:
        call  writeFirstMessage      # zavolani podprogramu pro vytisteni prvni zpravy
        call  writeSecondMessage     # zavolani podprogramu pro vytisteni druhe zpravy
        call  exit                   # zavolani podprogramu pro ukonceni procesu



# Podprogram pro vytisteni prvni zpravy
writeFirstMessage:
        push  offset message1        # adresa retezce, ktery se ma vytisknout
        push  message1len            # pocet znaku, ktere se maji vytisknout
        call  writeMessage           # zavolani podprogramu pro vytisteni zpravy
        ret                          # navrat z podprogramu



# Podprogram pro vytisteni druhe zpravy
writeSecondMessage:
        push  offset message2        # adresa retezce, ktery se ma vytisknout
        push  message2len            # pocet znaku, ktere se maji vytisknout
        call  writeMessage           # zavolani podprogramu pro vytisteni zpravy
        ret                          # navrat z podprogramu



# Podprogram pro vytisteni zpravy na standardni vystup
# Ocekava se, ze v ecx bude adresa zpravy a v edx jeji delka
writeMessage:
        pop   ebp                    # ulozime na chvili navratovou adresu do EBP
        pop   edx                    # ziskame v poradi druhy ulozeny parametr
        pop   ecx                    # ziskame v poradi prvni ulozeny parametr
        mov   eax, sys_write         # cislo syscallu pro funkci "write"
        mov   ebx, std_output        # standardni vystup
        int   0x80                   # volani Linuxoveho kernelu
        push  ebp                    # obnovime navratovou adresu
        ret                          # navrat z podprogramu



# Podprogram pro ukonceni procesu zavolanim syscallu
exit:
        mov   eax, sys_exit          # cislo sycallu pro funkci "exit"
        mov   ebx, 0                 # exit code = 0
        int   0x80                   # volani Linuxoveho kernelu

# finito

Překlad se opět provede v režimu 32bitových procesorů Intel:

as -g --32 subroutines-2.s -o subroutines-2.o
ld -m elf_i386 -s subroutines-2.o

Na disassemblovaném výpisu se můžeme podívat na způsob kódování instrukčních slov CALL, RET, PUSH, POP atd.:

objdump -M intel-mnemonic -f -d -t -h subroutines-2.o

subroutines-2.o:     file format elf32-i386
architecture: i386, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000046  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000020  00000000  00000000  0000007a  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  0000009a  2**0
                  ALLOC
SYMBOL TABLE:
00000000 l    d  .text	00000000 .text
00000000 l    d  .data	00000000 .data
00000000 l    d  .bss	00000000 .bss
00000001 l       *ABS*	00000000 sys_exit
00000004 l       *ABS*	00000000 sys_write
00000000 l       *ABS*	00000000 std_input
00000001 l       *ABS*	00000000 std_output
00000000 l       .data	00000000 message1
0000000c l       *ABS*	00000000 message1len
0000000d l       .data	00000000 message2
00000012 l       *ABS*	00000000 message2len
0000000f l       .text	00000000 writeFirstMessage
0000001c l       .text	00000000 writeSecondMessage
0000003a l       .text	00000000 exit
00000029 l       .text	00000000 writeMessage
00000000 g       .text	00000000 _start



Disassembly of section .text:

00000000 <_start>:
   0:	e8 0a 00 00 00       	call   f <writeFirstMessage>
   5:	e8 12 00 00 00       	call   1c <writeSecondMessage>
   a:	e8 2b 00 00 00       	call   3a <exit>

0000000f <writeFirstMessage>:
   f:	68 00 00 00 00       	push   0x0
  14:	6a 0c                	push   0xc
  16:	e8 0e 00 00 00       	call   29 <writeMessage>
  1b:	c3                   	ret    

0000001c <writeSecondMessage>:
  1c:	68 0d 00 00 00       	push   0xd
  21:	6a 12                	push   0x12
  23:	e8 01 00 00 00       	call   29 <writeMessage>
  28:	c3                   	ret    

00000029 <writeMessage>:
  29:	5d                   	pop    ebp
  2a:	5a                   	pop    edx
  2b:	59                   	pop    ecx
  2c:	b8 04 00 00 00       	mov    eax,0x4
  31:	bb 01 00 00 00       	mov    ebx,0x1
  36:	cd 80                	int    0x80
  38:	55                   	push   ebp
  39:	c3                   	ret    

0000003a <exit>:
  3a:	b8 01 00 00 00       	mov    eax,0x1
  3f:	bb 00 00 00 00       	mov    ebx,0x0
  44:	cd 80                	int    0x80

Za zmínku stojí fakt, že instrukce PUSH a POP pracující s registrem mají délku pouhý jeden bajt. Ještě zajímavější je kódování instrukce PUSH s konstantou:

Instrukce Význam Kódování
push offset message1 uložení 32bitové adresy/konstanty 0x6a + jeden bajt
push message1len uložení 8bitové konstanty 0x68 + čtyři bajty

9. Relativní adresování hodnot uložených na zásobníku

Způsobů, jak předávat parametry volaným subrutinám, existuje větší množství. Jeden ze způsobů, který je (po dalších úpravách) používán, spočívá v tom, že se o obnovu původního obsahu zásobníku stará volající kód. Ten tedy nejprve uloží na zásobník všechny parametry volané subrutiny, následně subrutinu skutečně zavolá (uloží na ni tedy ještě návratovou adresu) a po návratu zásobník obnoví. V našem případě to znamená uvolnění celkem osmi bajtů, což zajistí buď dvě operace POP nebo jednodušeji operace snižující ukazatel na vrchol zásobníku o hodnotu osm (ADD ESP, 8):

writeFirstMessage:
        push  offset message1        # adresa retezce, ktery se ma vytisknout
        push  message1len            # pocet znaku, ktere se maji vytisknout
        call  writeMessage           # zavolani podprogramu pro vytisteni zpravy
        add   esp, 8                 # obnoveni puvodni adresy vrcholu zasobniku
        ret                          # navrat z podprogramu

V subrutině taktéž dojde ke změně – parametry se ze zásobníku nemusí (a nesmí) odstraňovat, takže je můžeme adresovat relativně přes registr ESP. Na adrese [ESP] je uložena návratová adresa subrutiny, na adrese [ESP+4] poslední (zde druhý) parametr, na adrese [ESP+8] předposlední (zde první) parametr atd.:

# Podprogram pro vytisteni zpravy na standardni vystup
# Ocekava se, ze v ecx bude adresa zpravy a v edx jeji delka
writeMessage:
        mov   edx, [esp+4]           # precteni v poradi druheho parametru ze zasobniku
        mov   ecx, [esp+8]           # precteni v poradi prvniho parametru ze zasobniku
        mov   eax, sys_write         # cislo syscallu pro funkci "write"
        mov   ebx, std_output        # standardni vystup
        int   0x80                   # volani Linuxoveho kernelu
        ret                          # navrat z podprogramu

Po mírných úpravách se tento způsob používá pro vytváření zásobníkových rámců, které si popíšeme příště.

10. Třetí demonstrační příklad: volací sekvence, v níž parametry odstraňuje volající kód

Opět se podívejme na implementaci výše popsaného předávání a čtení parametrů v demonstračním příkladu:

# asmsyntax=as

# Program pro otestovani chovani instrukci CALL a RET spolecne
# s ukladanim parametru na zasobnik
# - pouzita je "Intel" syntaxe.
#
# Autor: Pavel Tisnovsky

.intel_syntax noprefix


# Linux kernel system call table
sys_exit   = 1
sys_write  = 4

# Dalsi konstanty pouzite v programu - standardni streamy
std_input  = 0
std_output = 1



#-----------------------------------------------------------------------------
.section .data
message1:                          # adresa prvni zpravy
        .string "Hello World\n"
message1len = $ - message1 - 1     # delka prvni zpravy

message2:                          # adresa druhe zpravy
        .string "Assembler je fajn\n"
message2len = $ - message2 - 1     # delka druhe zpravy



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



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

_start:
        call  writeFirstMessage      # zavolani podprogramu pro vytisteni prvni zpravy
        call  writeSecondMessage     # zavolani podprogramu pro vytisteni druhe zpravy
        call  exit                   # zavolani podprogramu pro ukonceni procesu



# Podprogram pro vytisteni prvni zpravy
writeFirstMessage:
        push  offset message1        # adresa retezce, ktery se ma vytisknout
        push  message1len            # pocet znaku, ktere se maji vytisknout
        call  writeMessage           # zavolani podprogramu pro vytisteni zpravy
        add   esp, 8                 # obnoveni puvodni adresy vrcholu zasobniku
        ret                          # navrat z podprogramu



# Podprogram pro vytisteni druhe zpravy
writeSecondMessage:
        push  offset message2        # adresa retezce, ktery se ma vytisknout
        push  message2len            # pocet znaku, ktere se maji vytisknout
        call  writeMessage           # zavolani podprogramu pro vytisteni zpravy
        add   esp, 8                 # obnoveni puvodni adresy vrcholu zasobniku
        ret                          # navrat z podprogramu



# Podprogram pro vytisteni zpravy na standardni vystup
# Ocekava se, ze v ecx bude adresa zpravy a v edx jeji delka
writeMessage:
        mov   edx, [esp+4]           # precteni v poradi druheho parametru ze zasobniku
        mov   ecx, [esp+8]           # precteni v poradi prvniho parametru ze zasobniku
        mov   eax, sys_write         # cislo syscallu pro funkci "write"
        mov   ebx, std_output        # standardni vystup
        int   0x80                   # volani Linuxoveho kernelu
        ret                          # navrat z podprogramu



# Podprogram pro ukonceni procesu zavolanim syscallu
exit:
        mov   eax, sys_exit          # cislo sycallu pro funkci "exit"
        mov   ebx, 0                 # exit code = 0
        int   0x80                   # volani Linuxoveho kernelu

# finito

Překlad tohoto demonstračního příkladu se opět provede v režimu 32bitových procesorů Intel:

as -g --32 subroutines-3.s -o subroutines-3.o
ld -m elf_i386 -s subroutines-3.o

Na disassemblovaném výpisu se podíváme na to, jak vlastně vypadá adresování parametrů uložených na zásobníku:

objdump -M intel-mnemonic -f -d -t -h subroutines-3.o

subroutines-3.o:     file format elf32-i386
architecture: i386, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000050  00000000  00000000  00000034  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000020  00000000  00000000  00000084  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  00000000  00000000  000000a4  2**0
                  ALLOC
SYMBOL TABLE:
00000000 l    d  .text	00000000 .text
00000000 l    d  .data	00000000 .data
00000000 l    d  .bss	00000000 .bss
00000001 l       *ABS*	00000000 sys_exit
00000004 l       *ABS*	00000000 sys_write
00000000 l       *ABS*	00000000 std_input
00000001 l       *ABS*	00000000 std_output
00000000 l       .data	00000000 message1
0000000c l       *ABS*	00000000 message1len
0000000d l       .data	00000000 message2
00000012 l       *ABS*	00000000 message2len
0000000f l       .text	00000000 writeFirstMessage
0000001f l       .text	00000000 writeSecondMessage
00000044 l       .text	00000000 exit
0000002f l       .text	00000000 writeMessage
00000000 g       .text	00000000 _start



Disassembly of section .text:

00000000 <_start>:
   0:	e8 0a 00 00 00       	call   f <writeFirstMessage>
   5:	e8 15 00 00 00       	call   1f <writeSecondMessage>
   a:	e8 35 00 00 00       	call   44 <exit>

0000000f <writeFirstMessage>:
   f:	68 00 00 00 00       	push   0x0
  14:	6a 0c                	push   0xc
  16:	e8 14 00 00 00       	call   2f <writeMessage>
  1b:	83 c4 08             	add    esp,0x8
  1e:	c3                   	ret    

0000001f <writeSecondMessage>:
  1f:	68 0d 00 00 00       	push   0xd
  24:	6a 12                	push   0x12
  26:	e8 04 00 00 00       	call   2f <writeMessage>
  2b:	83 c4 08             	add    esp,0x8
  2e:	c3                   	ret    

0000002f <writeMessage>:
  2f:	8b 54 24 04          	mov    edx,DWORD PTR [esp+0x4]
  33:	8b 4c 24 08          	mov    ecx,DWORD PTR [esp+0x8]
  37:	b8 04 00 00 00       	mov    eax,0x4
  3c:	bb 01 00 00 00       	mov    ebx,0x1
  41:	cd 80                	int    0x80
  43:	c3                   	ret    

00000044 <exit>:
  44:	b8 01 00 00 00       	mov    eax,0x1
  49:	bb 00 00 00 00       	mov    ebx,0x0
  4e:	cd 80                	int    0x80

Instrukce mov edx, [esp+0x04] je zakódována do čtyř bajtů, což je vlastně relativně malá velikost, zvláště když si uvědomíme, že v instrukčním slovu musí být zakódován cílový registr, zdrojový registr a relativní adresa (tedy konstanta):

  2f:	8b 54 24 04          	mov    edx,DWORD PTR [esp+0x4]

Z referenční příručky můžeme zjistit, že kód 0x8B znamená:

MOV   r16/32   r/m16/32

Další bajt 0x54 znamená registr EDX a současně způsob zápisu druhé adresy REG+offset. Třetí bajt 0x24 specifikuje druhý registr, kterým je ESP a čtvrtý bajt 0x04 je kýžený offset (kódování instrukcí u procesorů ARM je značně jednodušší).

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

Všechny tři dnes popisované demonstrační příklady byly 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 dnešní příklady jsou určené pro GNU Assembler a využívají přitom Intel syntaxi. Následuje tabulka s odkazy na zdrojové kódy příkladů i na již zmíněné podpůrné skripty:

První demonstrační příklad: otestování chování instrukcí CALL a RET

# Soubor Popis Odkaz do repositáře
1 subroutines-1.s program pro GNU Assembler https://github.com/tisnik/presentations/blob/master/assembler/15_subroutines-1/subroutines-1.s
2 assemble skript pro překlad na procesorech i386 https://github.com/tisnik/presentations/blob/master/assembler/15_subroutines-1/assemble
3 disassemble skript pro disassembling https://github.com/tisnik/presentations/blob/master/assembler/15_subroutines-1/assemble

Druhý demonstrační příklad: ukládání parametrů na zásobník

# Soubor Popis Odkaz do repositáře
1 subroutines-2.s program pro GNU Assembler https://github.com/tisnik/presentations/blob/master/assembler/16_subroutines-2/subroutines-2.s
2 assemble skript pro překlad na procesorech i386 https://github.com/tisnik/presentations/blob/master/assembler/16_subroutines-2/assemble
3 disassemble skript pro disassembling https://github.com/tisnik/presentations/blob/master/assembler/16_subroutines-2/disassemble

Třetí demonstrační příklad: volací sekvence, v níž parametry odstraňuje volající kód

# Soubor Popis Odkaz do repositáře
1 subroutines-3.s program pro GNU Assembler https://github.com/tisnik/presentations/blob/master/assembler/17_subroutines-3/subroutines-3.s
2 assemble skript pro překlad na procesorech i386 https://github.com/tisnik/presentations/blob/master/assembler/17_subroutines-3/assemble
3 disassemble skript pro disassembling https://github.com/tisnik/presentations/blob/master/assembler/17_subroutines-3/disassemble

12. Odkazy na Internetu

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