Zásobník volání
Zásobník volání (často zkrátka zásobník) je v informatice datová struktura typu zásobník, na kterou se při běhu procesu ukládají informace týkající se provádění podprogramů. Přestože správa zásobníku je důležitou součástí prakticky veškerého software, většina programátorů s ním explicitně nepracuje, neboť ve vyšších programovacích jazycích se o správnou funkci zásobníku stará automaticky překladač. Naopak v nízkoúrovňových jazycích, například v jazyce symbolických adres, musí programátor pracovat se zásobníkem explicitně.
Zásobník volání je používán pro několik účelů, přičemž tím hlavním je uložení informace o tom, do jakého stavu se má proces vrátit po ukončení provádění aktuálně běžícího podprogramu. Nejdůležitější takovou informací je návratová adresa, tedy adresa, z níž se má načíst první instrukce po návratu z podprogramu. Jako návratová adresa bývá na zásobník volání při provádění instrukce volání podprogramu uložena adresa instrukce následující po volající instrukci.
Popis
Zásobník volání funguje jako každý jiný zásobník, jsou zde dvě základní operace push
pro uložení hodnoty na vrchol zásobníku a pop
pro vyzvednutí hodnoty z vrcholu zásobníku. Operace uložení je používána především instrukcí volání podprogramu a operace vyzvednutí je používána především instrukcí návratu z podprogramu. K oběma operacím může docházet opakovaně a tím je umožněno volání dalšího podprogramu z běžícího podprogramu (a jako speciální případ takových vnořených volání také rekurze).
Pokud je ovšem pro zásobník volání rezervováno příliš málo místa a to je opakovaným ukládáním adres zaplněno (například při nekonečné rekurzi), dochází k přetečení zásobníku, které obvykle způsobí pád programu.
Každý běžící program má alespoň jeden svůj zásobník volání, obvykle jeden pro každé vlákno. Přestože program (respektive vlákno) může pracovat i s jinými datovými strukturami typu zásobník, například při správě signálů nebo při kooperativním multitaskingu, zůstává zásobník volání nejdůležitějším zásobníkem - proto je běžné označovat jej krátce zásobník.
Jednotlivé funkce zásobníku
- Ukládání návratové adresy
- je hlavním smyslem zásobníku. V okamžiku zavolání podprogramu je potřeba nějak uchovat informaci, odkud byl podprogram zavolán, respektive kam se má provádění vrátit po ukončení podprogramu. Pro tento účel je využit zásobník volání, což má několik výhod: Jednou je, že protože má každý proces respektive vlákno svůj zásobník, nebrání uložení informací tomu, aby byl podprogram reentrantní, tedy aby byl zavolán současně z několika míst. Zároveň je bez dalších problémů možná rekurze.
V závislosti na operačním systému, programovacím jazyku a architektuře počítače je zásobník využíván například pro:
- Uložení lokálních proměnných
- Podprogramy běžně využívají lokální proměnné, tedy adresový prostor používaný pouze v rámci podprogramu, jež může být po opuštění podprogramu přepsán. Jednoduchý způsob, jak rezervovat požadovaný prostor předem dané velikosti, je posunout ukazatel na vrchol zásobníku a adresy v rozsahu mezi novým a starým vrcholem používat pro hodnoty lokálních proměnných. Je to výrazně rychlejší než jiné způsoby dynamické přidělování paměti a opět to automaticky podporuje reentrantnost a také rekurzi – při každém novém vstupu do podprogramu je zarezervován zvláštní prostor pro lokální proměnné.
- Předávání parametrů podprogramu
- Z volající části kódu je často volaným podprogramům potřeba předat nějaké parametry. Pokud je parametrů jen málo, je možné (a některé překladače takovou možnost nabízí) předat parametry v registrech procesoru, pokud je jich však více, je potřeba předat je v paměti. Opět se zde nabízí posunutí vrcholu zásobníku a uložení parametrů do vzniklého prostoru.
- Uložení ukazatele na instanci objektu
- V objektově orientovaném programování jsou obvykle podprogramy přiřazeny ke konkrétní instanci třídy. Některé objektově orientované jazyky, například C++, informaci o této příslušnosti udržují tak, že umístěním na zásobník předávají podprogramům jako zvláštní parametr ukazatel na patřičnou instanci.
- Vyhodnocování výrazů
- Operandy a meziprodukty aritmetických a logických operací jsou obvykle udržovány v registrech procesoru, ale může dojít k situaci, že operandů je moc a registrů je málo. V takových případech může překladač odkládat některé operandy na zásobník (výpočet pak připomíná postfixovou notaci).
- Zpřístupnění kontextu u vnořených funkcí
- Některé programovací jazyky (například Pascal nebo Ada) podporují vnořené podprogramy, kdy kód z vnitřního podprogramu má přístup k lokálním proměnným vnějšího podprogramu. To může být implementováno například tak, že kromě samotných lokálních proměnných se na zásobníku při volání předává také ukazatel na místo uložení lokálních proměnných nadřazené funkce.
- Ukládání širšího kontextu
- V některých situacích je potřeba uchovat přes volání podprogramu víc hodnot, než jen adresu příští instrukce, například i jiné registry. V takových případech mohou být i ony uloženy na zásobník. Typickým příkladem je obsluha přerušení, při které je zapotřebí uchovat například obsah registru příznaků.
Příklad struktury zásobníku volání
Záznamy na zásobníku se skládají z jednotlivých rámců (anglicky stack frame) odpovídajících jednotlivým neukončeným voláním. Konkrétní podoba rámců je dána zejména architekturou.
Například na některých architekturách roste zásobník směrem nahoru (další záznamy jsou ukládány na místa s vyšší adresou), na jiných směrem dolů (další záznamy jsou ukládány na místa s nižší adresou)..
Pokud například byla zavolána funkce DrawLine
z funkce DrawSquare
, pak může vrchol zásobníku vypadat takto:
Rámec na vrcholu zásobníku patří k současnosti vykonávanému podprogramu a obvykle obsahuje minimálně následující položky:
- parametry předané funkci (jsou-li)
- návratová adresa
- rezervovaný prostor pro lokální proměnné (jsou-li)
Ukazatele na zásobníkové rámce
Na vrchol zásobníku vždy ukazuje speciální registr, ukazatel zásobníku (anglicky stack pointer, SP). Jeho hodnota se ovšem mění nejen při vstupu do funkce či při návratu z ní, ale může se měnit i v době běhu (například je-li zásobník použit při aritmetických výpočtech, nebo v okamžiku, když už jsou do něj připravovány parametry pro dále volanou funkci). Obvyklým postupem je tedy na začátku podprogramu uložit hodnotu ukazatele zásobníku do jiného zvláštního registru, kde se pak uchovává v nezměněné podobě po celou dobu běhu funkce.
Velikost rámců na zásobníku
Protože na zásobník jsou ukládány parametry podprogramů, vyžadují různé podprogramy různě veliké rámce v závislosti na počtu a velikosti svých parametrů. Překladače vyšších programovacích jazyků dnes navíc často umožňují alokovat různě veliké lokální proměnné v závislosti na parametrech, takže dokonce různá volání téže funkce mohou vyžadovat jiné množství prostoru, a to takové, které je v době kompilace programu neznámé. To je také dalším důvodem, proč je využíván ukazatel na rámec, který ukazuje na spodek rámce - z ukazatele na vrchol rámce nelze bez znalosti velikosti parametrů zjistit adresy jednotlivých parametrů.
Ukazatel na rámec volajícího
Na většině systémů je obvyklé mít jako součást rámce aktuálního podprogramu uložený také ukazatel na rámec volajícího podprogramu. To je především proto, že ukazatel na rámec volajícího podprogramu patří mezi lokální hodnoty toho kterého podprogramu a tedy nemůže být uchován v registrech, ale je zapotřebí ho při zavolání podprogramu někam uložit a při ukončení podprogramu opět obnovit. Zároveň to může usnadnit ladění programu, neboť je kdykoliv možné snadno a rychle zjistit posloupnost volání.
Překryv rámců
Různé překladače postupují při alokování prostoru pro předání parametrů různě. Některé vždy před každým voláním alokují potřebný prostor pro parametry toho kterého podprogramu, jiné na začátku podprogramu rovnou alokují potřebný prostor pro všechny podprogramy, které je možno dále volat. Pak je takový prostor pokládán za překryv rámců.
Použití
Operace na straně volajícího
Na straně volajícího je potřeba v rámci volání obvykle vykonat jen minimum explicitně prováděných činností (což je dobře, protože jedna funkce může být volána mnohokrát z různých míst). Jsou zde pochopitelně vypočítány hodnoty jednotlivých parametrů volání a ty jsou poté umístěny na zásobník (případně do registrů, v závislosti na použité konvenci volání). Pak už je provedena instrukce volání.
Operace při vstupu do podprogramu
Na začátku podprogramu je obvykle proveden takzvaný prolog podprogramu, který zajišťuje nutný servis než začne provádění samotného podprogramu.
Na začátku obvykle dojde k uložení některých hodnot na zásobník, bývá ukládána návratová adresa a také hodnota ukazatele na zásobník a ukazatele na rámec. Na některých architekturách se ovšem o toto vše automaticky postará instrukce volání a není třeba to provádět v rámci prologu.
Dalším krokem je aktualizace ukazatele na rámec (je-li používán) na hodnotu ukazatele na zásobník. Pak je hodnota ukazatele na zásobník změněna, aby tak byl rezervován prostor pro lokální proměnné.
Operace při návratu z podprogramu
Podprogram před návratem řízení do volají funkce obvykle provede soubor příkazů, které jsou opakem kroků při vstupu do podprogramu. Obnoví se uložené hodnoty registrů, změní se hodnota ukazatele (tedy dealokují se lokální proměnné) a pak se zavolá instrukce návratu, která nahraje ze zásobníku návratovou adresu do instrukčního ukazatele.
Někdy se také ze zásobníku uvolní prostor pro parametry podprogramu, jindy se o uvolnění tohoto prostoru stará až volající funkce, když je jí navráceno řízení.
Další souvislosti
Aktuální data ze zásobníku mohou být použita kromě ladění také k profilování.
Zároveň je také zásobník tradičním místem pro bezpečnostní útoky. V jazycích, které nekontrolují zápis mimo meze pole, totiž zápisem za hranici lokálního pole lze přepsat návratovou adresu a tak přinutit program předat řízení útočníkovi. Jedná se o takzvané přetečení na zásobníku.
Reference
V tomto článku byl použit překlad textu z článku Call stack na anglické Wikipedii.