Interessant wird das Konzept nun im Kontext von Funktionsaufrufen. Beim Aufruf einer Funktion wird für sie festgelegt, ab welche Speicheradresse sie ihre Daten ablegen kann; diese Speicheradresse wird ebenfalls in einem Register gespeichert, welches mit ebp
(kurz für extended base pointer) bezeichnet wird. Wird nun einer aufgerufenen Funktion mitgeteilt, dass sie ihre Daten zum Beispiel ab der Adresse 123462
ablegen kann, ergibt sich das folgende Bild im Speicher (der Pfeil <--
deutet dabei an, dass der entsprechende Bezeichner auf die angegebene Speicherzeile verweist):
Adresse | Inhalt | Inhalt | Inhalt | Inhalt | |
---|---|---|---|---|---|
123450 | |||||
123454 | |||||
123458 | |||||
123462 | <– ebp <– esp |
||||
123466 | |||||
123470 | |||||
… | … | … | … | … | |
123490 | <– bottom |
Wird innerhalb einer Funktion nun eine Variable deklariert, wird ihre Adresse einfach relativ zur aktuellen Adresse von ebp
definiert und der esp
entsprechend angepasst; die letzte lokale Variable der Funktion liegt dabei immer in der Zeile (nicht Zelle!) vor der ebp
-Adresse, die vorletzte lokale Variable in der Zeile davor und so weiter (natürlich nur bei Variablen, die nur eine einzelne Speicherzeile zum Ablegen ihrer Daten brauchen). Man sieht: die Variablen einer Funktion werden im Speicher in umgekehrter Reihenfolge angelegt. Zu beachten ist auch: der esp
wird nicht erst verschoben, wenn die entsprechende Variable deklariert wird, sondern direkt beim Funktionsaufruf passend gesetzt; der Speicher für alle in einer Funktion benötigten Variablen wird also direkt beim Aufruf der Funktion reserviert.
Die Adresse der letzten Variable lautet also ebp - 4
, die der vorletzten Variable entsprechend ebp - 8
und so weiter. Nehmen wir zum Beispiel folgenden Codeausschnitt:
void f() { int x; int y; ... }
Damit würde sich folgendes Bild im Speicher ergeben:
Adresse | Inhalt | Inhalt | Inhalt | Inhalt | |
---|---|---|---|---|---|
123450 | <– esp | ||||
123454 | <– x [= ebp – 8] | ||||
123458 | <– y [= ebp – 4] | ||||
123462 | <– ebp | ||||
123466 | |||||
123470 | |||||
… | … | … | … | … | |
123490 | <– bottom |
Damit ist auch gleich die Frage geklärt, wie eine Variable immer auf die korrekte Speicheradresse verweisen kann; sie wird immer relativ zum ebp
der umschließenden Funktion definiert und kann so auch während der Programmausführung dynamisch bestimmt werden.
Funktionsparameter werden in der gleichen Manier definiert, allerdings in der anderen Richtung vom ebp
aus gesehen. Der Wert des ersten Parameters wird 2(!) Zeilen hinter dem ebp
gespeichert, der zweite Parameter in der folgenden Zeile und so weiter; nehmen wir folgenden Codeausschnitt:
void f( int m, int n ) { int x; int y; ... }
Dann ergibt sich hier das folgende Speicherbild bei einem konkreten Funktionsaufruf:
Adresse | Inhalt | Inhalt | Inhalt | Inhalt | |
---|---|---|---|---|---|
123450 | <– esp | ||||
123454 | <– x [= ebp – 8] | ||||
123458 | <– y [= ebp – 4] | ||||
123462 | <– ebp | ||||
123466 | |||||
123470 | <– m | ||||
123474 | <– n | ||||
… | … | … | … | … | |
123490 | <– bottom |
Der Speicherbereich übrigens, der alle Parameter und Variablen eines Funktionsaufrufs umfasst (im Beispiel also von 123450
bis einschließlich 123474
), wird Stack Frame genannt.
Jetzt stellen sich aber 2 neue Fragen: woher kommt der Wert für den ebp
eigentlich und was wird in den beiden Speicherzellen um den ebp
herum
gespeichert, die ich bisher ignoriert habe – immerhin wird auf letztere ja nicht durch eine Variable verwiesen.
Die Herkunft des ebp
-Wertes ist recht einfach zu klären. Wenn wir uns innerhalb einer Funktion befinden, hat das esp
-Register ja einen bestimmten Wert, nämlich das aktuelle Ende des gültigen Stack-Bereiches. Wird nun innerhalb der Funktion eine weitere aufgerufen, wird der Wert des esp
-Registers genutzt, um den ebp
-Wert der aufgerufenen Funktion zu bestimmen (nachdem die Argumente für den Funktionsaufruf durch die aufrufende Funktion auf den Stack gelegt wurden). Betrachten wir hierzu als Beispiel den folgenden Code:
Kommentare (11)