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.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ě:
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);
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é.
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.
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.
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.