Warum wachsen wir den Stapel immer noch rückwärts?

46

Wenn Sie C-Code kompilieren und die Assembly betrachten, wächst der Stack rückwärts wie folgt:

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)- Bedeutet dies, dass der Basiszeiger oder der Stapelzeiger die Speicheradressen tatsächlich nach unten verschieben, anstatt nach oben zu gehen? Warum ist das so?

Ich wechselte $5, -4(%rbp)zu $5, +4(%rbp), kompiliert und lief den Code , und es gab keine Fehler. Warum müssen wir also auf dem Speicherstapel immer noch rückwärts gehen?

Alex
quelle
2
Beachten Sie, dass der Basiszeiger -4(%rbp)überhaupt nicht bewegt wird und +4(%rbp)möglicherweise nicht funktioniert hat.
Margaret Bloom
14
" warum müssen wir noch rückwärts gehen " - was denkst du wäre der vorteil vorwärts zu gehen? Letztendlich spielt es keine Rolle, Sie müssen nur eine auswählen.
Bergi
31
"Warum bauen wir den Stapel rückwärts an?" - weil wenn wir nicht jemand anderes fragen würde, warum mallocder Haufen rückwärts wächst
slebetman
2
@MargaretBloom: Anscheinend ist es dem CRT-Startcode auf der Plattform des OP egal, ob mainer seinen RBP blockiert. Das ist sicher möglich. (Und ja, das Schreiben 4(%rbp)würde auf den gespeicherten RBP-Wert springen). Tatsächlich tut dies das Hauptmenü nie mov %rsp, %rbp, daher ist der Speicherzugriff relativ zum RBP des Anrufers , wenn dies das ist, was das OP tatsächlich getestet hat !!! Wenn dies tatsächlich von der Compiler-Ausgabe kopiert wurde, wurden einige Anweisungen ausgelassen!
Peter Cordes
1
Mir scheint, dass "rückwärts" oder "vorwärts" (oder "runter" und "hoch") von Ihrer Sichtweise abhängt. Wenn Sie den Speicher als Spalte mit niedrigen Adressen im oberen Bereich grafisch darstellen, ist das Vergrößern des Stapels durch Dekrementieren eines Stapelzeigers analog zu einem physischen Stapel.
Jamesdlin

Antworten:

86

Bedeutet dies, dass der Basiszeiger oder der Stapelzeiger die Speicheradressen tatsächlich nach unten verschieben, anstatt nach oben zu gehen? Warum ist das so?

Ja, die pushAnweisungen dekrementieren den Stapelzeiger und schreiben in den Stapel, während popumgekehrt vom Stapel gelesen und der Stapelzeiger inkrementiert wird.

Dies ist insofern etwas historisch, als bei Maschinen mit begrenztem Speicher der Stapel hoch und nach unten gewachsen war, während der Haufen niedrig und nach oben gewachsen war. Es gibt nur eine Lücke des "freien Speichers" - zwischen dem Heap & Stack, und diese Lücke wird gemeinsam genutzt, entweder kann man nach Bedarf in die Lücke hineinwachsen. Das Programm hat nur dann nicht genügend Arbeitsspeicher, wenn Stapel und Heap zusammenstoßen und kein freier Arbeitsspeicher mehr vorhanden ist. 

Wenn der Stapel und der Haufen beide in die gleiche Richtung wachsen, gibt es zwei Lücken, und der Stapel kann nicht wirklich in die Lücke des Haufens hineinwachsen (das Gegenteil ist ebenfalls problematisch).

Ursprünglich hatten Prozessoren keine speziellen Anweisungen zur Stapelverarbeitung. Als jedoch die Hardware um Stapelunterstützung erweitert wurde, entwickelte sich dieses Muster nach unten, und Prozessoren folgen diesem Muster auch heute noch.

Man könnte argumentieren, dass auf einem 64-Bit-Computer genügend Adressraum vorhanden ist, um mehrere Lücken zuzulassen - und als Beweis sind mehrere Lücken zwangsläufig der Fall, wenn ein Prozess mehrere Threads hat. Dies ist zwar keine ausreichende Motivation, um Dinge zu ändern, da bei Systemen mit mehreren Lücken die Wachstumsrichtung wohl willkürlich ist, sodass Tradition / Kompatibilität den Ausschlag geben.


Sie müßten die CPU - Stack Handhabungsanweisungen ändern , um die Richtung des Stapels zu ändern, oder auch auf der Verwendung der gewidmet gibt nach oben drücken und knallend Anweisungen (zB push, pop, call, ret, andere).

Beachten Sie, dass die MIPS-Befehlssatzarchitektur kein dediziertes push& pophat. Daher ist es praktisch, den Stapel in beide Richtungen zu vergrößern. Möglicherweise möchten Sie dennoch ein Speicherlayout mit einer Lücke für einen einzelnen Thread-Prozess, aber möglicherweise vergrößern Sie den Stapel nach oben und den Heap abwärts. In diesem Fall ist für einige C- Varg- Codes möglicherweise eine Anpassung der Übergabe der Quell- oder Unter-der-Haube-Parameter erforderlich.

(Da es in MIPS kein dediziertes Stack - Handling gibt, können wir Pre - oder Post - Increment oder Pre - oder Post - Decrement verwenden, um auf den Stack zu drücken, sofern wir die genaue Umkehrung zum Abspringen des Stacks verwenden und auch annehmen, dass Das Betriebssystem respektiert das gewählte Stack-Nutzungsmodell. In der Tat ist der MIPS-Stack in einigen eingebetteten Systemen und einigen Bildungssystemen aufwärts gewachsen.)

Erik Eidt
quelle
32
Es ist nicht nur pushund popauf den meisten Architekturen, sondern auch das weitaus wichtige Interrupt-Handling, call, ret, und was auch immer hat backene in Interaktion mit dem Stapel.
Deduplizierer
3
ARM kann alle vier Stack-Varianten haben.
Margaret Bloom
14
Für das, was es wert ist, denke ich nicht, dass "die Wachstumsrichtung willkürlich ist" in dem Sinne, dass jede Wahl gleich gut ist. Das Herabsteigen hat die Eigenschaft, dass ein Überlaufen des Endes eines Puffers frühere Stapelrahmen, einschließlich gespeicherter Rücksprungadressen, überlastet. Das Heranwachsen hat die Eigenschaft, dass ein Überlaufen des Endes eines Puffers nur den Speicher in demselben oder einem späteren (wenn der Puffer nicht im neuesten ist, kann es spätere geben) Aufrufrahmen und möglicherweise sogar nur unbenutzten Speicherplatz belastet (alle unter der Annahme eines Schutzes) Seite nach dem Stapel). Aus sicherheitstechnischer Sicht ist das Aufwachsen sehr zu bevorzugen
R ..
6
@R ..: Das Heranwachsen eliminiert keine Buffer Overrun-Exploits, da anfällige Funktionen normalerweise keine Blattfunktionen sind: Sie rufen andere Funktionen auf und platzieren eine Rücksprungadresse über dem Buffer. Blattfunktionen, die einen Zeiger von ihrem Aufrufer erhalten, können anfällig für das Überschreiben ihrer eigenen Rücksprungadresse werden. ZB Wenn eine Funktion einen Puffer auf dem Stapel zuweist und ihn an gets()eine strcpy()nicht eingebundene Funktion übergibt, verwendet die Rückgabe in diesen Bibliotheksfunktionen die überschriebene Rückgabeadresse. Derzeit mit abwärts wachsenden Stapeln ist es, wenn ihr Anrufer zurückkehrt.
Peter Cordes
5
@PeterCordes: In der Tat habe ich in meinem Kommentar festgestellt, dass Stapelrahmen mit derselben Ebene oder neueren Stapeln als der übergelaufene Puffer immer noch potenziell überlastbar sind, aber das ist viel weniger. In dem Fall, in dem die Clobbering-Funktion eine Blattfunktion ist, die direkt von der Funktion aufgerufen wird, deren Puffer sie ist (z. B. strcpy), gibt es auf einem Bogen, in dem die Rücksprungadresse in einem Register gespeichert ist, keinen Zugriff, um die Rücksprungfunktion zu blockieren Adresse.
R ..
8

In Ihrem speziellen System beginnt der Stapel mit einer hohen Speicheradresse und "wächst" nach unten zu einer niedrigen Speicheradresse. (Der symmetrische Fall von niedrig nach hoch existiert auch)

Und da du von -4 auf +4 gewechselt hast und es lief, heißt das nicht, dass es korrekt ist. Das Speicherlayout eines laufenden Programms ist komplexer und hängt von vielen anderen Faktoren ab, die möglicherweise dazu beigetragen haben, dass Sie bei diesem extrem einfachen Programm nicht sofort abgestürzt sind.

Nadir
quelle
1

Der Stapelzeiger zeigt auf die Grenze zwischen zugewiesenem und nicht zugewiesenem Stapelspeicher. Wird es nach unten vergrößert, bedeutet dies, dass es auf den Anfang der ersten Struktur im zugewiesenen Stapelspeicher verweist , wobei andere zugewiesene Elemente an größeren Adressen folgen. Zeiger auf den Anfang der zugewiesenen Strukturen zeigen zu lassen, ist weitaus üblicher als umgekehrt.

Heutzutage gibt es auf vielen Systemen ein separates Register für Stapelrahmen , die etwas zuverlässig abgewickelt werden können, um die Aufrufkette herauszufinden, wobei der lokale variable Speicher dazwischenliegt. Die Art und Weise, wie dieses Stapelrahmenregister auf einigen Architekturen eingerichtet ist, führt dazu, dass es im Gegensatz zum Stapelzeiger davor hinter den lokalen Variablenspeicher zeigt. Die Verwendung dieses Stapelrahmenregisters erfordert dann eine negative Indizierung.

Beachten Sie, dass Stapelrahmen und ihre Indizierung ein Nebenaspekt kompilierter Computersprachen sind, sodass der Codegenerator des Compilers sich eher mit der "Unnatürlichkeit" befassen muss als mit einem schlechten Assembler-Programmierer.

Es gab also gute historische Gründe für die Auswahl von Stapeln, die nach unten wachsen sollten (und einige davon bleiben erhalten, wenn Sie in Assemblersprache programmieren und sich nicht um die Einrichtung eines geeigneten Stapelrahmens kümmern), aber sie sind weniger sichtbar.


quelle
2
"Heutzutage gibt es auf vielen Systemen ein separates Register für Stack-Frames." Sie sind in Verzug. Reichhaltigere Debug-Informationsformate haben heutzutage die Notwendigkeit von Frame-Zeigern weitgehend beseitigt.
Peter Green