Lektion
Die Theorie — Der Aufrufstack: Stackrahmen, Rücksprungadressen und Überläufe
Der Aufrufstack ist ein Speicherbereich, in dem jeder Funktionsaufruf einen Stackrahmen erhält. Hier enthält er das Argument, die Rücksprungadresse, an der der Aufrufer seine Ausführung fortsetzt, den gesicherten Rahmenzeiger des Aufrufers und die lokalen Variablen. Der Stackzeiger markiert die niedrigste belegte Adresse. Ein Aufruf verschiebt ihn um einen Stackrahmen nach unten, die Rückkehr wieder nach oben. Eine Rekursion mit der Aufruftiefe d und Stackrahmen von jeweils f Bytes benötigt d·f Bytes auf dem Stack.
Was die einzelnen Symbole bedeuten
d- die Tiefe: Anzahl der gleichzeitig vorhandenen Stackrahmen
f- die Größe eines Stackrahmens in Bytes: hier 12, dazu 4 je lokaler Variable
- Setzt voraus
- Eine 32-Bit-Maschine mit der klassischen C-Aufrufkonvention: 4-Byte-Speicherstellen, ein Stack, der zu niedrigeren Adressen wächst, Argumentübergabe auf dem Stack und ein Rahmenzeiger in jedem Stackrahmen. Gängige 64-Bit-Aufrufkonventionen übergeben die ersten Argumente in Registern. Compiler können den Rahmenzeiger weglassen, und auf ARM kann eine Funktion, die keine weitere Funktion aufruft, ihre Rücksprungadresse in einem Register halten. Echte Stackrahmen unterscheiden sich daher in Größe und Aufbau von den drei hier gezeigten Speicherstellen.
Aufgabe vollständig gelöst
-
Wie tief kann eine Rekursion gehen? 4 Schritte
Klick auf fact(11): Überlauf. Der Stack dieser Seite fasst 128 Bytes. Der Linker von Microsoft reserviert für ein Programm standardmäßig 1 MB Stack. Rechne dafür mit 1 MiB, also 1.048.576 Bytes, und den Stackrahmen dieser Seite. Wie tief kann fact damit gehen? Wie tief, wenn jeder Aufruf zusätzlich eine lokale Variable von 4 Bytes hält? Und wie viel Stack braucht fib(30) im Verhältnis zur Anzahl seiner Aufrufe?
-
Ein Stackrahmen enthält hier ein Argument, eine Rücksprungadresse und einen gesicherten Rahmenzeiger: 3 × 4 = 12 Bytes. 128 / 12 = 10,67, also passen 10 Stackrahmen hinein. fact(11) braucht 11 × 12 = 132 Bytes. Die Ergebnistabelle zeigt den Überlauf.
-
1.048.576 / 12 = 87.381,33, also passen 87.381 Stackrahmen hinein. fact könnte eine Rekursionstiefe von 87.381 erreichen, bevor der Stack voll ist. Schon viel früher passt das Ergebnis nicht mehr in einen 32-Bit-int: bei 13! = 6.227.020.800.
-
Mit einer lokalen Variable von 4 Bytes wächst jeder Stackrahmen auf 16 Bytes. 1.048.576 / 16 = 65.536 geht genau auf.
-
Die längste Aufrufkette von fib(30) umfasst 30 Stackrahmen und benötigt 30 × 12 = 360 Bytes. Die Anzahl der Aufrufe ist 2·F(31) − 1. Dabei ist F(31) = 1.346.269 die 31. Fibonacci-Zahl. Das ergibt 2.692.537 Aufrufe.
Antwort
87.381 Rekursionsebenen, mit einer lokalen Variable 65.536. fib(30) braucht für 2.692.537 Aufrufe nur 360 Bytes Stack. Die Tiefe bestimmt den Stackbedarf; bei fib bestimmt die Anzahl der Aufrufe die Laufzeit.
-
Lernpfad
Wie ein Computer ein Programm ausführt
Quellen (2)
- The worked problem's stack size: the linker's default reservation: Microsoft Learn, "/STACK (Stack allocations)," C/C++ build reference, updated 23 February 2022.
- The first insight's 1988 example: fingerd's unbounded read into a 512-byte buffer on the stack: Bob Page, "A Report on the Internet Worm." University of Lowell, 7 November 1988 (copy hosted at Toronto Metropolitan University).