Rekurzió

Az alábbiakban egy konkrét példán mutatjuk be, hogy egy önmagát rekurzívan hívó függvény hogyan használja a vermet.

Példaként tekintsük azt a programot, amely a rekurzív definíciója szerint számítja ki a Fibonacci sorozat n-dik elemét.

				F(0) := 0
				F(1) := 1
				F(n) := F(n-1) + F(n-2), ha n>1
			

Vagyis:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...

fibon.asm:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
global _Fibon
section .text

_Fibon:
    push  ebp
    mov   ebp, esp
    push  ecx
    mov   eax, [ ebp + 8 ]   ;N-t kivesszuk a verembol
	cmp   eax, 1
	jbe   nincs_rekurzio
	dec   eax
	push  eax
	dec   eax
	push  eax
	call  _Fibon
	add   esp,4
	mov   ecx, eax
	call  _Fibon
	add   esp,4
	add   eax, ecx

nincs_rekurzio:
	pop   ecx
    pop   ebp
    ret
fibon.c:
1
2
3
4
5
6
7
8
9
#include "stdio.h"

int main( void ) {
    unsigned int n;
    printf( "Meddig szamoljuk ki a fibonacci szamot? " );
    scanf( "%d", &n );
    printf( "Az eredmeny: %u", Fibon( n ) );
}

Vizsgáljuk meg, hogy ha a C főprogramból meghívjuk a Fibon függvényt n-3-re, akkor hogyan alakul a verem tartalma!

A C főprogram a verembe helyezi a függvény paraméterét, majd meghívja a szubrutint, végül a visszatérés után kitakarítja a verembe tett paramétert. Ezt a következő utasítássorozat végzi:

push ...
call _Fibon
add esp,4

Mire az assembly szubrutinhoz kerül a vezérlés, a veremtartalom így fog kinézni:

esp => VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Mire a vezérlés eljut a 15. sorban lévő call utasításig, a verem így alakul:
esp => 1 3-1-1
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp => ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Ezek után a call a verembe teszi a 16. sorba való visszatéréshez szükséges VTC-t, majd elugrik a _Fibon cimkéhez. Ott megint a verembe kerül az ebp majd az ecx regiszter tartalma, és az eax-be kiolvasásra kerül a call előtt a verembe push-olt érték az ebp+8 címről, vagyis a 1. Ekkor a verem tartalma így néz ki:
esp => ecx ez még mindig az az ecx érték, ami még a C főprogramban volt
ebp => ebp
VTC (a 16. sorba visszatéréshez)
1 3-1-1
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Mivel eax=1, ezért a jbe feltétel már teljesül, vagyis a vezérlés a nincs_rekurzio cimkéhez kerül. Itt a pop ecx kiveszi a verem tetején lévő adatot, és azt az ecx-be másolja, majd a pop ebp a következő adatot, és azt az ebp regiszterbe másolja. Ezután a ret a verem következő adatát visszatérési címként értelmezi, és oda ugrik, ahova ez a cím mutat. Jegyezzük meg, hogy a ret végrehajtásakor eax=1, vagyis a szubrutin által visszaadott érték 1 (a legutoljára átadott n-re, vagyis n=1-ra _Fibon(1)=1).

Ha jól írtuk meg a programot, akkor az ecx-be az az érték kerül vissza, amely korábban éppen az ecx-ből került a verembe, és az ebp-be is az, amely az ebp-ből került a verembe, továbbá olyan címet vesz ki a ret és értelmez visszatérési címkként, amely egy call utasítás által került a verembe. Ha nem így lenne, akkor valószínűleg valami hibát vétettünk a program tervezésekor, és a program nem fog működni.

Most nem hibás a programunk, ezért a két pop utáni ret a 16. sorba vezeti a programot. Eddigre a verem tartalma a következőképpen néz ki. (Ne felejtsük el, hogy a veremből kivétel (pop és ret) csak kimásolja az adatokat a megfelelő regiszterekbe, és az esp értékét aktualizálja, de a memóriából az adatok ténylegesen nem törlődnek, ezért a veremmutató által jelzett "verem teteje" feletti adatok bizonytalan ideig még elérhetőek. Ezt halványszürke betűkkel jeleztük)

ecx ez még mindig az az ecx érték, ami még a C főprogramban volt
ebp
VTC (a 16. sorba visszatéréshez)
esp=> 1 3-1-1
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Most az add esp,4 kitakarítja a verembe utoljára berakott paramétert, vagyis felszabadítja az általa elfoglalt területet (vagyis az 1-et), majd a jelenleg eax-ben lévő 1-et (a Fibon(1) értékét) ecx-be másolja átmeneti megőrzés céljából. Ekkor a verem így néz ki:
ecx ez még mindig az az ecx érték, ami még a C főprogramban volt
ebp
VTC (a 16. sorba visszatéréshez)
1 3-1-1
esp=> 2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Most a 18. sorban lévő call hajtódik végre, vagyis a verembe belekerül az a cím, ahova majd vissza kell térni (ez a 19. sor címe lesz), majd a vezérlés újra a _Fibon cimkéhez kerül. Itt megint a verembe másolódik az ebp és az ecx értéke. Az ecx ekkor az 1-et tartalmazza!
ecx ez még mindig az az ecx érték, ami még a C főprogramban volt
esp=> 1 ecx-ből, a Fibon(1)=1 részeredmény
ebp=> ebp
VTC (a 19. sorba visszatéréshez)
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Most az eax-be beolvasódik a legutolsó VTC előtt a verembe tett érték, vagyis a 2. Most tehát a _Fibon(2) értékét számoljuk. Erre az eax értékre még mindig nem teljesül a jbe feltétel, ezért a dec-ek irányába fut tovább a program. A két push a következő verem tartalmaz alakítja ki:
esp=> 0 2-1-1
1 2-1
1 ecx-ből, a Fibon(1)=1 részeredmény
ebp=> ebp
VTC (a 19. sorba visszatéréshez)
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Most a call a _Fibon cimkéhez ugrunk, ahol megint a verembe kerül az ebp majd az ecx értéke:
esp=> ecx
ebp=> ebp
VTC (a 16. sorba visszatéréshez)
0 2-1-1
1 2-1
1 ecx-ből, a Fibon(1)=1 részeredmény
ebp
VTC (a 19. sorba visszatéréshez)
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...

Az eax-be kiolvasódik az ebp+8 címről a legutóbbi VTC előtt verembe tett érték, vagyis eax=0 lesz. Erre a jbe feltétele teljesül, vagyis a nincs_rekurzio cimkéhez ugrunk.

Most a verem tetején lévő érték ecx-be másolódik, és a verem mutató egy sorral lejjebbi értéket fog mutatni (=néggyel nő). A pop ebp hatására a verem aktuális teteje az ebp-be másolódik, és az esp újabb 4-gyel nő. Végül a ret a verem jelenlegi tetején lévő adatot címként értelmezi, és erre a címre ugrik.

Végeredményként a ret után ez a helyzet áll elő (és ne felejtsük el, hogy közben eax=0, vagyis ezt a 0 értéket adjuk vissza _Fibon(0)-ra):

ecx
ebp
VTC (a 16. sorba visszatéréshez)
esp=> 0 2-1-1
1 2-1
1 ecx-ből, a Fibon(1)=1 részeredmény
ebp
VTC (a 19. sorba visszatéréshez)
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Most az add esp,4 kitakarítja a verembe utoljára berakott paramétert, vagyis a 0-t, majd a jelenleg eax-ben lévő 0-t ecx-be másolja átmeneti megőrzés céljából. Ez egy fontos pillanat, hiszen az ecx egészen eddig azt az értéket tartalmazta, amit a főprogram benne "hagyott". Az egész push ecx-pop ecx tortúra a 7. és 23. sorban azért kell, hogy ezt az ecx-módosulást a meghívó főprogram elől elrejtsük. Jelenleg a verem így néz ki:
ecx
ebp
VTC (a 16. sorba visszatéréshez)
0 2-1-1
esp=> 1 2-1
1 ecx-ből, a Fibon(1)=1 részeredmény
ebp
VTC (a 19. sorba visszatéréshez)
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Most végrehajtódik a 18. sorban lévő call, amely a 19. sorban lévő utasítás címét helyezi VTC-ként a verembe. Ezután a verem a következőképpen alakul:
ecx
ebp
VTC (a 16. sorba visszatéréshez)
esp=> VTC (a 19. sorba visszatéréshez)
1 2-1
1 ecx-ből, a Fibon(1)=1 részeredmény
ebp
VTC (a 19. sorba visszatéréshez)
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Most a verem tetejére kerül először az ebp aktuális értéke, majd az ecx-ben lévő 0.
ecx
esp=> 0 a Fibon(0) részeredmény tárolása ecx-ből
ebp=> ebp
VTC (a 19. sorba visszatéréshez)
1 2-1
1 ecx-ből, a Fibon(1)=1 részeredmény
ebp
VTC (a 19. sorba visszatéréshez)
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Ekkor az eax-be kiolvasódik az ebp+8 címről a kapott paraméter, az 1. A jbe feltétele erre is teljesül, vagyis a nincs_rekurzio cimkétől folytatódik a végrehajtás. Ezzel a verem tetejéről kiolvasunk egy sort és azt tároljuk az ecx rgiszterben (ez éppen az a 0, amit legutóbb a verembe mentettünk megőrzés céljából!), a következőt pedig az ebp-ben, és a ret hatására visszatérünk a 19. sorhoz. Ekkor az eax értéke 1, vagyis kiszámoltuk, hogy Fibon(1)=1. A verem ezután így néz ki:
ecx
esp=> 0 a Fibon(0) részeredmény tárolása ecx-ből
ebp=> ebp
VTC (a 19. sorba visszatéréshez)
esp=> 1 2-1
1 ecx-ből, a Fibon(1)=1 részeredmény
ebp
VTC (a 19. sorba visszatéréshez)
2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Most a 19. sorban lévő add esp, 4 felszabadítja az utoljára verembe tett paraméter által elfoglalt helyet. Ezután a 20. sorban összeadjuk a most eax-be előállított Fibon(1) értékét az ecx-ben lévő, korábban kiszámolt Fibon(0) értékkel. Most eax-ben előállt a Fibon(2) értéke (=1), már csak a veremből kivesszük az ecx, majd az ebp értékét, és visszatérünk a 19. sorba. Ekkor a verem így fest:
ecx
0 a Fibon(0) részeredmény tárolása ecx-ből
ebp
VTC (a 19. sorba visszatéréshez)
1 2-1
1 ecx-ből, a Fibon(1)=1 részeredmény
ebp
VTC (a 19. sorba visszatéréshez)
esp=> 2 3-1
ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
A 19. sorban járva tehát az eax-ben a Fibon(2) értéke (=1) található, és az ecx-be az imént rekonstruáltuk a Fibon(1)=1 eredményt. Most a veremből kitakarítjuk a kapott paramétert (a 2-t). Ezután a 20. sorban képezzük a Fibon(2) és Fibon(1) összegét, és az eredményt eax-ben tároljuk, vagyis a Fibon(3)=Fibon(2)+Fibon(1) képlet alapján járunk el. Az eax-ben tehát előállt a Fibon(3) eredménye (=2).
ecx
0 a Fibon(0) részeredmény tárolása ecx-ből
ebp
VTC (a 19. sorba visszatéréshez)
1 2-1
1 ecx-ből, a Fibon(1)=1 részeredmény
ebp
VTC (a 19. sorba visszatéréshez)
2 3-1
esp=> ecx az az ecx érték, ami még a C főprogramban volt
ebp az az ebp érték, ami még a C főprogramban volt
VTC (a főprogramba visszatéréshez)
3 Fibon(3) paramétere
...
Most a veremből kiszedjük a két felső értéket az ecx és az ebp regiszterbe, majd visszatérünk. Ezúttal azonban a vissaztérés a főprogramba történik, és az ecx és ebp regiszterek pedig azokat at értékeket nyerik vissza, amelyekkel annak idején a vézérlés átkerült a C nyelvú főprogramból a mi assemblyben megírt függvényünkhöz.

A C főprogram tehát a regiszterekből csak annyit lát, hogy az eax-ben előállt a Fibon(3) értéke, de minden más használt regiszter (az ebp és az ecx is) ugyanazt az értéket tartalmazza, mint kezdetben. Az assembly függvényünk tehát maradéktalanul teljesíti feladatát: csak azt a regisztert módosítja, amelyben az eredményt elő kell állítania, minden más értéket változatlanul hagy.

Tanulság

Amint az látható, még viszonylag kis rekurziós mélységnél is elég bonyolult az egyes függvényhívások paraméterlistáját követni a veremben. Sokkal célszerűbb, ha az ember nem gondol bele ebbe a mélységbe, hanem a következőket tartja szem ellőtt: Ezeket szem előtt tartva sokkal könnyebben megérthető a fenti assembly program: Ha a rekurzív függvényre ezzel a szemlélettel tekintünk, akkor sokkal nagyobb eséllyel sikerülhet működőképes rekurziót megvalósítani.
Megjegyzés: Ez a leírás 2007.12.02. 1:32-kor készült. Igyekeztem hibátlanra írni, de ha mégis van benne hiba, akkor kérem, hogy azt emailben jelezzétek nekem. Köszönöm!