Výuka assembleru

6. Zásobník

<< Předchozí díl
Další díl >>
Schéma zásobníku

Každý program při svém spuštění dostane k dispozici speciální paměťovou oblast, tzv. zásobník (angl. stack). Adresa zásobníku je uložena v registru ESP, jehož jméno v češtině znamená "ukazatel na zásobník" (Stack Pointer na 16bitových procesorech, Extended Stack Pointer) na 32bitových. Zásobník se skládá z 32bitových čísel a slouží k:

Velikost zásobníku je nastavena při sestavení programu. Pro programy vytvořené ve Visual C++ je to implicitně 1 MB, což většinou dostačuje. Schéma zásobníku ukazuje obrázek vpravo.

Z obrázku je patrné, že zásobník je jakási "studna", do níž "házíme" 32bitové hodnoty. V registru ESP je uložena adresa na hodnotu, která byla naposled vložena do zásobníku. Část zásobníku "nad" touto adresou (tj. paměť s nižšími adresami) je prázdná; je připravena na vložení dalších hodnot. Část zásobníku "pod" ukazatelem ESP (tedy na vyšších adresách) obsahuje hodnoty uložené dříve. Čím "hlouběji" daná hodnota leží, tím dříve tam byla vložena.

Ukládání do zásobníku probíhá způsobem "ulož hodnotu" (push value); vybírání hodnot ze zásobníku se provádí způsobem "vytáhni hodnotu" (pop value). Pokud uložíme hodnotu do zásobníku, bude tato hodnota uložena vždy na vrchol zásobníku a při vybírání bude také pouze v vrcholu zásobníku vybrána.

Instrukce pro práci se zásobníkem

Pro ukládání do zásobníku a vybírání ze zásobníku jsou v assembleru dvě instrukce: push pro vložení, pop pro vybrání. Obě instrukce mají jediný operand, který musí být 32bitový - registr nebo adresa v paměti. Instrukce push může jako operand použít i konstantu (u instrukce pop by to samozřejmě nemělo smysl). Příklady intstrukcí ukazuje následující kód:

        push    1
        pop     eax                 ; EAX bude obsahovat hodnotu 1
        push    VelikostSouboru
        pop     ebx                 ; EBX bude obsahovat velikost souboru
        push    edx
        pop     ecx                 ; ECX bude obsahovat hodnoru registru EDX

        push    eax
        push    ebx
        pop     ecx                 ; Do ECX bude uložena hodnota registru EBX
        pop     edx                 ; Do ECX bude uložena hodnota registru EAX

        pushfd                      ; Uložíme registr EFLAGS
        popfd                       ; Obnovíme registr EFLAGS (jediný způsob,
                                      jak jej přímo modifikovat)

Instrukci push je možné napsat i jinak, a to pomocí instrukcí pro práci s pamětí. Jestliže např. instrukce

        push    eax

sníží ukazatel zásobníku (registr ESP) o 4 byty a uloží na vrchol zásobníku obsah registru EAX, dá se tedy přepsat takto:

        sub     esp, 4
        mov     [esp], eax

Podobně můžeme přepsat i instrukci pop eax:

        mov     eax, [esp]
        add     esp, 4

Rozdíl mezi oběma zápisy je pouze ve velikosti kódu a v délce provádění, instrukce push a pop jsou rychlejší a kratší.

Ukládáme všechny registry

Pro uložení všech osmi registrů (EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP) můžeme buďto použít posloupnost instrukcí push pro každý registr, můžeme ale použít jednu instrukci, která uloží všechny registry najednou:

        pushad                          ; Uložíme registry v pořadí EAX, ECX, EDX, EBX,
                                          ESP, EBP, ESI, EDI)

        popad                           ; Obnoví obsahy všech registrů

A hlavně to nesplést

Při prácí se zásobníkem je nutné pamatovat na důležité pravidlo, že pokud do zásobníku uložíme jednu nebo více hodnot, musíme tyto hodnoty buďto zase vybrat, nebo přičíst příslušný počet bytů k registru ESP (každá vložená hodnota znamená 4 byty). Toto obnovení původního obsahu zásobníku musíme provést nejpozději před návratem z funkce (instrukce ret).

        push    esi                     ; Uložíme obsahy registrů do zásobníku
        push    edi

        ; Něco provádíme

        pop     edi                     ; Obnovíme obsahy registrů
        pop     esi                     ; (a zároveň i stav zásobníku)
        ret
nebo takhle:
        push    esi                     ; Uložíme obsahy registrů do zásobníku
        push    edi

        ; Něco provádíme

        add     esp, 8                  ; Obnovíme stav zásobníku, ale ne obsahy registrů
        ret

Důvodem je instrukce ret, která provádí návrat z funkce na adresu, odkud byla funkce volána. Funkcí se shoduje s pomyslnou instrukcí pop eip (která neexistuje). Při provedení jednak změní hodnotu instrukčního pointeru EIP (která bude vytažena ze zásobníku), a jednak zvýší hodnotu registru ESP o 4. Podívejme se, co provede tento kód:

        push    eax
        ret

Do zásobníku vložíme hodnotu registru EAX a provedeme instrukci ret. Uvedený kód je vlastně skok na adresu, která je uložena v registru EAX. Pokud tento skok skutečně chceme provést, je to v pořádku; pokud je to nedopatření, povede k pádu programu (pokud ovšem v registru EAX nebyla čistě náhodou uložena hodnota návratové adresy).

<< Předchozí díl
Další díl >>