Assembler – Inštrukcie CALL a RET
Syntax:
CALL typ_volania operandy
RET
Inštrukcie CALL a RET používame na implementáciu podprogramov. Inštrukcia CALL má jeden, zvyčajne priamy operand (ako u inštrukcii skoku sa pripúšťa operand v registri alebo v pamäti), ktorým je adresa miesta v pamäti, kam sa odovzdá riadenie. Na rozdiel od inštrukcie JMP sa do zásobníka uloží hodnota registra IP (EIP) nasledujúcej inštrukcie po inštrukcii CALL.
Parameter typ volania je podobný ako u inštrukcie JMP, ak nie je uvedený, bude sa predpokladať typ volania near, a preto sa bude ukladať a meniť iba register IP (EIP). Pri typu volania far sa na zásobník okrem registra IP (EIP) uloží aj hodnota segmentového registra CS (adresa volania sa lak bude pozostávať z hodnoty pre register IP (EIP) a hodnoty pre register CS).
Návrat z podprogramu zabezpečuje inštrukcia RET, ktorá zo zásobníka vyberie hodnotu z vrcholu a tu uloží do registra IP (EIP), čím sa začnú vykonávať inštrukcie uložené za volajúcou inštrukciou CALL. Pre návrat z podprogramu, ktorý bol privolaný inštrukciou CALL far musíme obnoviť aj pôvodnú hodnotu registra CS, čo môžeme zabezpečiť použitím inštrukcie RETF, ktorá zo zásobníka najprv vyberie hodnotu a uloží ju do registra IP, a potom vyberie ešte jednu, ktorú uloží do registra CS.
Situáciu môžeme ešte trochu skomplikovať.
Inštrukcie RET alebo RETF môžu mať ako priamy operand počet položiek, ktoré sa majú zo zásobníka vybrať po naplnení registra IP (EIP) (alebo aj SK). Tento špeciálny tvar inštrukcie RET využijeme až neskôr, kde si povieme o možnostiach prepojenia vyšších programovacích jazykov a assembleru.
Ukážme si použitie inštrukcií CALL a RET na príklade:
Pomocou podprogramu sčítame dve čísla uložené v registroch EAX a EBX, výsledok súčtu uložíme v registri ECX (ignoruje pretečenie). Hodnoty uložené v registroch EAX a EBX musia byť zachované aj po návrate z podprogramu.
Vytvoríme podprogram sčítaj. Najskôr musíme vyriešiť odovzdávanie parametrov podprogramu. Vo vyšších programovacích jazykoch sa pre odovzdanie parametra používa zásobník, my si zatiaľ vystačíme s dohodnutými registrami. Akonáhle vykonáme inštrukciu pre sčítanie ADD, bude jedna hodnota operanda prepísaná výsledkom. Využijeme zásobník a hodnotu uloženú v danom registri si uložíme a neskôr opäť obnovíme.
scitaj: push eax; uložíme hodnotu registra EAX na zásobník add eax, EBX; sčítame EAX a EBX, výsledok bude v EAX mov ecx, eax; výsledok z EAX uložíme do ECX pop eax; obnovíme pôvodnú hodnotu v registri EAX ret ; návrat z podprogramu.
Podprogram scitaj vyskúšame s hodnotami 4 a 8.
mov eax, 4; do EAX uložíme 4 mov EBX, 8; EBX = 8 call scitaj ; zavoláme podprogram scitaj ;Výsledok bude uložený v registri ECX a bude JIRRA. ; hodnota ECX = OxOOOOOOOC
Čo by sa stalo keby sme vynechali inštrukciu POP eax? Inštrukcia RET by tak predala riadenie na adresu, ktorá bola pôvodne uložená v registri EAX, čo by viedlo k pádu programu a v horšom prípade pádu systému. Podobne opomenutie inštrukcie RET by viedlo k vykonávanie inštrukcií dávno nepatria nášmu podprogramu, čo tiež ľahko (a celkom určite) vedie k pádu programu (to sa veľmi často využíva crackarmi a hackermi).
Podprogram scitaj možno v našom jednoduchom prípade zjednodušiť tak, že sa zaobídeme bez inštrukcií PUSH a POP:
scitaj: mov ecx, eax ; hodnotu registra EAX (prvý parameter) prekopírujete do registra ECX add ecx, EBX ; a sčítame s druhým parametrom v registri EBX, ; výsledok bude v požadovanom registri ECX ret ; návrat z podprogramu.