Assembler (Jazyk symbolických adres)

(4. část)

Volání podprogramů

Využití vkládaného Assembleru je hlavně v implementaci podprogramů. Předem si ale musíme ukázat, jak se podprogramy volají.

Volání podprogramu spočívá v uložení parametrů do zásobníku a změně adresy v registru EIP (čítač instrukcí) na adresu podprogramu s tím, že je uschována adresa odkud provádíme volání (to aby procesor věděl kam se má vrátit). Parametry do zásobníku ukládáme my, zbytek zařídí instrukce CALL.

Ukládání parametrů do zásobníku

V hlavičce procedury (nebo funkce) najdeme téměř vždy definici parametrů volaných: Například void soucet (short a, short b, short *c); je deklarace procedury s názvem soucet s parametry a, b volanými hodnotou a c volaným odkazem. Při volání této procedury z některé části programu psaném v C na místa a, b zapíšeme konkrétní hodnoty (nebo proměnné, ty ale podprogram nezmění) a na místo c zapíšeme adresu (nebo proměnnou typu ukazatel), na které pak najdeme hodnotu po provedení procedury (např. soucet (1, 3, &prom);). Z místa volání předáváme parametry do podprogramů vždy přes zásobník a v opačném pořadí než jsou v deklaraci podprogramu (takže na vrcholu zásobníku je první parametr). Do zásobníku před voláním procedury ukládáme odlišně u parametrů volaných hodnotou a odkazem.

Při volání hodnotou uložíme konkrétní hodnoty (přečtené třeba i z paměti). Vzhledem k organizaci zásobníku jsou parametry volané hodnotou uloženy po dvojslovech následovně:

Při volání odkazem uložíme adresu místa odkud se má hodnota číst nebo kam se má zapsat (což je vlastně obsah ukazatele na paměťové místo).

Samotné volání podprogramu

Při volání se mění registr EIP. Skok do podprogramu zajistí instrukce:

Volání podprogramů je tedy jednoduché. Jednoduše napíšeme instrukci CALL se jménem podprogramu (procedury nebo funkce). Ostatní zařídí překladač, tj. dosadí adresu. Před vstupem do podprogramu jsme do zásobníku uložili parametry podprogramu, proto po ukončení podprogramu nesmíme zapomenout tyto parametry ze zásobníku odstranit (za parametry podprogramu odpovídá volající)!

Příklad:
      #include <stdio.h>

      void factorial_iter(unsigned char a, unsigned long *b)
      {
       if (a <= 1) return;
       *b *= a;
       factorial_iter(a - 1, b);
      }

      unsigned long factorial(unsigned char a)
      {
       unsigned long ret = 1, *pret = &ret;

       _asm {
        push dword ptr pret
        push dword ptr a
        call factorial_iter
        add esp, 8
       }
       return ret;
      }

      int main()
      {
      printf("%u! = %lu\n", 10, factorial(10));
      return 0;
      }
    
Stejnou posloupnost instrukcí jako blok Assembleru v tomto programu provede jeden řádek v C: factorial_iter(a, pret);

Návrat hodnoty z funkce

Funkce je podprogram, který vrací jednu hodnotu typu uvedeného v deklaraci. Vrácenou hodnotu zjistíme po návratu z funkce vždy v registrech:

Př. Upravte uvedený příklad tak, aby se z Assembleru volaly i funkce factorial a printf. Formátovací řetězec si uložte do proměnné.

Tvorba podprogramů

S parametry pracujeme v podprogramech v souladu s tím, jak jsme je přes zásobník předávali. To znamená, že k parametrům volaným hodnotou přistupujeme jako ke klasickým proměnným (z pohledu podprogramu přímou, popř. nepřímou adresací), k parametrům volaným odkazem přistupujeme jako k ukazatelům (nepřímou adresací). Je potřeba si ale uvědomit, že parametry patří volajícímu (podprogramu), ne volanému podprogramu!

Lokální proměnné

V okamžiku vstupu do podprogramu se na vrcholu zásobníku automaticky vytvoří místo pro lokální proměnné definované kdekoliv v podprogramu. Velikost alokovaného místa (počet bytů) pro každou proměnnou je vždy rovna nejmenšímu většímu násobku 4 skutečné velikosti proměnné (alokační jednotka zásobníku je dvojslovo). Tzn. např. pro lokální proměnné typu char, short int i long int je vytvořeno místo stejné velikosti dvojslova (4B), pro pole 10-ti prvků char je zabráno místo velikosti 3 dvojslova (12B), apod. Proměnné (místa pro ně) jsou na zásobníku alokovány v pořadí, v jakém jsou deklarovány, a samozřejmě jsou také případně inicializovány na hodnoty uvedené v deklaraci.

Vstup do podprogramu

Všechny podprogramy musí mít stejné podmínky pro vykonávání svého kódu a pro přístup k proměnným lokálním v rámci podprogramu, tzn. musí "mít svůj zásobník". Zásobník programu je ale jen jeden, proto je podprogramu vyhrazena jeho část odpovídající lokálním proměnným podprogramu. Umístění zásobníku v paměti je určeno registry EBP a ESP, registr EBP je tedy nasměrován na vrchol zásobníku v okamžiku vstupu do podprogramu (nové dno zásobníku) a registr ESP je nastaven na adresu hned za lokální proměnné (nový vrchol zásobníku), tj. těsně po vstupu (CALL) do podprogramu se automaticky provádí:
      PUSH EBP
      MOV EBP, ESP
      SUB ESP, velikost_lokalnich_promennych
    

Tuto sekvenci instrukcí provádí také jedna instrukce ENTER velikost_lokalnich_promennych, 0. Druhý parametr se používá při vnořených podprogramech (např. v Pascalu, v C ne).

Nyní s pomocí registru EBP můžeme přistupovat k:

Otázka: Proč se ve výše uvedených adresách přičítá, resp. odečítá, právě 8, resp. 4? Vysvětlete. Jaké jsou adresy dalších parametrů nebo proměnných v pořadí?

Vzhledem k tomu, že se o tyto přepočty adres může postarat překladač, je jednodušší používat pro přístupy k lokálním proměnným a parametrům jen jejich jména uvedená v deklaraci podprogramu.

Pozor! K lokálním proměnným (a parametrům) nelze přistupovat pomocí přičítání k registru ESP (ani na začátku podprogramu), protože překladače si do zásobníku po lokálních proměnných ukládají ještě něco dalšího. Např. překladač M$ Visual C++ si tam ukládá ještě dalších 64 bytů (ví někdo proč?) a registry EBX, ESI a EDI.

Ukončení podprogramu

Před ukončením podprogramu se musí "obnovit" původní zásobník volajícího. Registry EBP a ESP se tedy automaticky obnoví na hodnoty těsně po vstupu do podprogramu (před úpravu zásobníku, původní hodnota registru EBP se ukládala, pamatujete?):
      MOV ESP, EBP
      POP EBP
    

nebo pomocí instrukce LEAVE.

Samotné ukončení podprogramu (návrat do volající části kódu) zajišťuje automaticky instrukce:

Pozor! O akce, které provádějí instrukce ENTER, LEAVE a RET se v inline Assembleru (v C) ale nemusíme vůbec starat, potřebné instrukce si překladač automaticky do každé funkce doplní sám. Proto tyto akce ani provádět nesmíme! Na druhou stranu je ale přece dobré vědět, co všechno za nás překladač udělá!

Př. Imlementujte funkci factorial_iter v uvedeném příkladu v Assembleru. V obou funkcích factorial a factorial_iter přistupujte k parametrům a lokálním proměnným přes jejich adresy vypočítané pomocí EBP.



Jan Outrata
outrataj@phoenix.inf.upol.cz