Um ein konkretes Beispiel dafür zu geben, wie ein Compiler den Stack verwaltet und wie auf Werte im Stack zugegriffen wird, sehen wir uns visuelle Darstellungen sowie Code an, der GCC
in einer Linux-Umgebung mit i386 als Zielarchitektur generiert wurde .
1. Rahmen stapeln
Wie Sie wissen, ist der Stapel ein Speicherort im Adressraum eines laufenden Prozesses, der von Funktionen oder Prozeduren in dem Sinne verwendet wird, dass auf dem Stapel Speicherplatz für lokal deklarierte Variablen sowie für an die Funktion übergebene Argumente reserviert ist ( Platz für Variablen, die außerhalb einer Funktion deklariert wurden (dh globale Variablen), wird in einer anderen Region im virtuellen Speicher zugewiesen. Der für alle Funktionsdaten zugewiesene Speicherplatz wird als Stapelrahmen bezeichnet . Hier ist eine visuelle Darstellung mehrerer Stapelrahmen (aus Computersystemen: Perspektive eines Programmierers ):
2. Stack-Frame-Management und variable Position
Damit Werte, die in den Stapel innerhalb eines bestimmten Stapelrahmens geschrieben werden, vom Compiler verwaltet und vom Programm gelesen werden können, muss es eine Methode zum Berechnen der Positionen dieser Werte und zum Abrufen ihrer Speicheradresse geben. Dabei helfen die in der CPU als Stapelzeiger und Basiszeiger bezeichneten Register.
Der Basiszeiger ebp
enthält gemäß der Konvention die Speicheradresse des Bodens oder der Basis des Stapels. Die Positionen aller Werte innerhalb des Stapelrahmens können unter Verwendung der Adresse im Basiszeiger als Referenz berechnet werden. Dies ist in der obigen Abbildung dargestellt: %ebp + 4
Ist die im Basiszeiger gespeicherte Speicheradresse beispielsweise plus 4.
3. Vom Compiler generierter Code
Was ich aber nicht bekomme, ist, wie Variablen auf dem Stapel von einer Anwendung gelesen werden. Wenn ich x als Ganzzahl deklariere und zuordne, z. B. x = 3, wird Speicher auf dem Stapel reserviert und dann der Wert 3 gespeichert dort und dann in der gleichen Funktion deklariere ich y als, sage 4, und folge dann, dass ich x in einem anderen Ausdruck verwende (sage z = 5 + x), wie kann das Programm x lesen, um z wann auszuwerten liegt es unter y auf dem stapel?
Verwenden wir ein einfaches Beispielprogramm in C, um zu sehen, wie dies funktioniert:
int main(void)
{
int x = 3;
int y = 4;
int z = 5 + x;
return 0;
}
Lassen Sie uns den von GCC erstellten Assembler-Text für diesen C-Quelltext untersuchen (der Übersichtlichkeit halber habe ich ihn ein wenig aufgeräumt):
main:
pushl %ebp # save previous frame's base address on stack
movl %esp, %ebp # use current address of stack pointer as new frame base address
subl $16, %esp # allocate 16 bytes of space on stack for function data
movl $3, -12(%ebp) # variable x at address %ebp - 12
movl $4, -8(%ebp) # variable y at address %ebp - 8
movl -12(%ebp), %eax # write x to register %eax
addl $5, %eax # x + 5 = 9
movl %eax, -4(%ebp) # write 9 to address %ebp - 4 - this is z
movl $0, %eax
leave
Was wir beobachten ist , dass Variablen x, y und z an Adressen angeordnet %ebp - 12
, %ebp -8
und %ebp - 4
ist. Mit anderen Worten, die Positionen der Variablen innerhalb des Stapelrahmens für main()
werden unter Verwendung der im CPU-Register gespeicherten Speicheradresse berechnet %ebp
.
4. Daten im Speicher außerhalb des Stapelzeigers liegen außerhalb des Bereichs
Mir fehlt eindeutig etwas. Geht es bei der Position auf dem Stapel nur um die Lebensdauer / den Gültigkeitsbereich der Variablen und darum, dass das Programm jederzeit auf den gesamten Stapel zugreifen kann? Wenn ja, bedeutet dies, dass es einen anderen Index gibt, der nur die Adressen der Variablen auf dem Stapel enthält, damit die Werte abgerufen werden können? Aber dann dachte ich, der springende Punkt des Stapels sei, dass die Werte am selben Ort wie die variable Adresse gespeichert werden.
Der Stack ist eine Region im virtuellen Speicher, deren Verwendung vom Compiler verwaltet wird. Der Compiler generiert Code so, dass auf Werte jenseits des Stapelzeigers (Werte jenseits des obersten Stapels) nie verwiesen wird. Wenn eine Funktion aufgerufen wird, ändert sich die Position des Stapelzeigers, um Platz auf dem Stapel zu schaffen, der sozusagen nicht "außerhalb der Grenzen" liegt.
Wenn Funktionen aufgerufen und zurückgegeben werden, wird der Stapelzeiger dekrementiert und inkrementiert. Auf den Stapel geschriebene Daten verschwinden nicht, wenn sie außerhalb des Gültigkeitsbereichs liegen. Der Compiler generiert jedoch keine Anweisungen, die auf diese Daten verweisen, da der Compiler die Adressen dieser Daten nicht mit %ebp
oder berechnen kann %esp
.
5. Zusammenfassung
Code, der direkt von der CPU ausgeführt werden kann, wird vom Compiler generiert. Der Compiler verwaltet den Stack, Stack-Frames für Funktionen und CPU-Register. Eine Strategie, die von GCC zum Verfolgen der Positionen von Variablen in Stapelrahmen in Code verwendet wird, der auf einer i386-Architektur ausgeführt werden soll, besteht darin, die Speicheradresse im Stapelrahmen-Basiszeiger %ebp
als Referenz zu verwenden und Werte von Variablen in Positionen in den Stapelrahmen zu schreiben bei Offsets an die Adresse in %ebp
.