5. cvičení
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:- hodnotou - podprogram jejich hodnoty pouze využívá
- odkazem - podprogram je může číst a může do nich i zapsat
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ě:
- parametry o délce jedné slabiky (char, unsigned char) - obsadí celé dvojslovo, nevyužité bity budou nulové
- parametry o délce jednoho slova (short, unsigned short) - obsadí celé dvojslovo, nevyužité bity budou nulové
- parametry o délce dvojslova (long, unsigned long, float, ukazatel) - obsadí právě dvojslovo
- parametry delší (řetězce, pole, struktury) - ukládá se jejich adresa (ukazatel)
Samotné volání podprogramu
Při volání se mění registr EIP. Skok do podprogramu zajistí instrukce:- CALL adresa - na vrchol zásobníku ulož obsah EIP a naplň tento registr adresou uvedenou v parametru (v jazyce C se na adresu vyhodnocuje přímo název podprogramu)
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:- AL - funkční hodnota o velikosti slabiky
- AX - funkční hodnota o velikosti slova
- EAX - funkční hodnota o velikosti dvojslova, tedy i adresa (např. řetězce)
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:- parametrům - přičítáním k hodnotě v EBP, protože jsou "před zásobníkem" (např. [EBP + 8] je adresa prvního parametru)
- lokálním proměnným - odečítáním od hodnoty v EBP, protože jsou "v zásobníku" (např. [EBP - 4] je adresa první lokální proměnné typu long int)
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.
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:- RET - z vrcholu zásobníku vezmi adresu a dosaď ji do EIP, velmi důležitá instrukce, při jejím neuvedení na konci podprogramu (překladačem) by procesor pokračoval další instrukcí za instrukcemi podprogramu (což téměř nikdy nejsou instrukce volající procedury za místem volání) a prováděl by se tak libovolný kód (nebo dokonce data!) programu!!
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á!
Používání registrů
Za normálních okolností všechny funkce sdílí stejnou sadu registrů. To nemusí být vždy žádoucí, např. pokud chceme zachovat hodnotu v registru i po zavolání funkce, která tento registr používá. Proto se na procesorech x86 používá následující konvence:
- Za registry EAX, ECX, EDX je zodpovědná volající funkce. Tzn. pokud chceme zachovat hodnotu v některém z těchto registrů, musíme ji někam uložit před zavoláním funkce. Po návratu z funkce může být v těchto registrech libovolná hodnota.
- Za registry EBX, ESI, EDI je zodpovědná volaná funkce. Tzn. pokud chceme použít některý z těchto registrů, musíme jeho hodnotu na začátku funkce uložit na zásobník a při návratu musíme hodnotu nastavit zpět. Po zavolání funkce by v těchto registrech měla být stejná hodnota jako před zavoláním.
Nastavení Visual Studia
Visual Studio se ve svém implicitním nastavení nemusí chovat intuitivně. Proto doporučuji nastavení projektu (Solution Explorer -- Properties) provést následující změny:
- C/C++ / Code Generation / Run Time Library -> Multi-threaded Debug
- C/C++ / Code Generation / Basic Runtime Checks -> default
- C/C++ / Code Generation / Buffer Security Check -> No
- C/C++ / Linker / General / Enable Incremental Linking -> No
Úkoly
- Přepište výše uvedenou funkci
factorial_iter
do assembleru. - Napište funkci
char *my_strdup(char *s)
, která vytvoří kopii řetezces
. Použijte volání funkcímalloc
astrlen
. - Napište funkci
char *abcs(unsigned char n)
, která vytvoří řetězec s s prvními n písmeny abecedy. Pokud by výčet měl obsahovat víc než písmeno Z, vratí řetězec "Oops." Použijte volání funkcímalloc
astrcpy
. - Napište funkci
unsigned int fib(unsigned short n)
, která vypočítá rekurzivně hodnotu n-tého fibonacciho čísla. - Napište funkci
void print_fact(unsigned char n)
, která vypíše hodnotu ve tvaru "fact(n) = X". Pro výpočet a výpis hodnoty zavolejte funkcefactorial
aprintf
. - Napište funkci
void print_facts(unsigned char n)
, která vypíše prvních n hodnot faktoriálu s pomocí voláníprint_fact
. - Napište funkci
void read_and_print_fib()
, která načte ze vstupu hodnotun
, vypočte a výpíše hodnotu "fib(n) = X". Použijte funkciscanf
. V případě, že je na vstupu neplatná hodnota, program vypíše chybovou hlášku. - Napište funkci
unsigned int ack(unsigned short n, unsigned short m)
, která vypočítá hodnotu ackermannovy funkce.
Pro získání bodového hodnocení je potřeba splnit úkoly 3, 5 a 6.