Was ist genau der Basiszeiger und der Stapelzeiger? Worauf weisen sie hin?


225

Verwenden Sie dieses Beispiel aus Wikipedia, in dem DrawSquare () DrawLine () aufruft.

Alt-Text

(Beachten Sie, dass dieses Diagramm unten hohe Adressen und oben niedrige Adressen enthält.)

Könnte mir jemand erklären was ebpund espin diesem Zusammenhang sind?

Nach allem, was ich sehe, würde der Stapelzeiger immer auf den oberen Rand des Stapels und der Basiszeiger auf den Anfang der aktuellen Funktion zeigen. Oder was?


edit: Ich meine das im Kontext von Windows-Programmen

edit2: Und wie funktioniert das eipauch?

edit3: Ich habe den folgenden Code von MSVC ++:

var_C= dword ptr -0Ch
var_8= dword ptr -8
var_4= dword ptr -4
hInstance= dword ptr  8
hPrevInstance= dword ptr  0Ch
lpCmdLine= dword ptr  10h
nShowCmd= dword ptr  14h

Alle scheinen Dwords zu sein und benötigen jeweils 4 Bytes. Ich kann also sehen, dass es eine Lücke von hInstance zu var_4 von 4 Bytes gibt. Was sind Sie? Ich nehme an, es ist die Absenderadresse, wie auf dem Bild von Wikipedia zu sehen ist.


(Anmerkung des Herausgebers: Ein langes Zitat aus Michaels Antwort wurde entfernt, das nicht in die Frage gehört, aber eine Folgefrage wurde in bearbeitet.):

Dies liegt daran, dass der Ablauf des Funktionsaufrufs wie folgt ist:

* Push parameters (hInstance, etc.)
* Call function, which pushes return address
* Push ebp
* Allocate space for locals

Meine Frage (zuletzt hoffe ich!) Ist nun, was genau passiert, sobald ich die Argumente der Funktion, die ich aufrufen möchte, bis zum Ende des Prologs platziere. Ich möchte wissen, wie sich das Ebbe, insbesondere in diesen Momenten entwickelt (ich habe bereits verstanden, wie der Prolog funktioniert, ich möchte nur wissen, was passiert, nachdem ich die Argumente auf den Stapel geschoben habe und vor dem Prolog).


23
Eine wichtige Sache zu beachten ist, dass der Stapel im Speicher "nach unten" wächst. Dies bedeutet, dass Sie den Wert verringern, um den Stapelzeiger nach oben zu bewegen.
BS

4
Ein Hinweis zur Unterscheidung der Aktivitäten von EBP / ESP und EIP: EBP & ESP befassen sich mit Daten, während sich EIP mit Code befasst.
mmmmmmmm

2
In Ihrem Diagramm ist ebp (normalerweise) der "Frame-Zeiger", insbesondere der "Stapelzeiger". Dies ermöglicht den konsistenten Zugriff auf Einheimische über [ebp-x] und Stapelparameter über [ebp + x], unabhängig vom Stapelzeiger (der sich häufig innerhalb einer Funktion ändert). Die Adressierung könnte über ESP erfolgen, wodurch EBP für andere Vorgänge freigegeben wird. Auf diese Weise können Debugger den Aufrufstapel oder die Werte von Einheimischen nicht erkennen.
Peterchen

4
@ Ben. Nicht nesacerily. Einige Compiler legen Stapelrahmen in den Heap. Das Konzept des Abwachsens von Stapeln ist genau das, ein Konzept, das es leicht verständlich macht. Die Implementierung des Stapels kann beliebig sein (die Verwendung zufälliger Teile des Heaps erschwert Hacks, die Teile des Stapels überschreiben, erheblich, da sie nicht so deterministisch sind).
Martin York

1
In zwei Worten: Mit dem Stapelzeiger können Push / Pop-Operationen ausgeführt werden (damit Push und Pop wissen, wo Daten abgelegt / abgerufen werden müssen). Mit dem Basiszeiger kann der Code unabhängig auf Daten verweisen, die zuvor auf den Stapel verschoben wurden.
Tigrou

Antworten:


228

esp ist, wie Sie sagen, die Spitze des Stapels.

ebpwird normalerweise zu espBeginn der Funktion eingestellt. Auf Funktionsparameter und lokale Variablen wird zugegriffen, indem ein konstanter Versatz von addiert bzw. subtrahiert wird ebp. Alle x86-Aufrufkonventionen gelten ebpals über Funktionsaufrufe hinweg erhalten. ebpselbst zeigt tatsächlich auf den Basiszeiger des vorherigen Frames, wodurch das Stapeln in einem Debugger und das Anzeigen der lokalen Variablen anderer Frames funktioniert.

Die meisten Funktionsprologe sehen ungefähr so ​​aus:

push ebp      ; Preserve current frame pointer
mov ebp, esp  ; Create new frame pointer pointing to current stack top
sub esp, 20   ; allocate 20 bytes worth of locals on stack.

Dann später in der Funktion können Sie Code wie haben (vorausgesetzt, beide lokalen Variablen sind 4 Bytes)

mov [ebp-4], eax    ; Store eax in first local
mov ebx, [ebp - 8]  ; Load ebx from second local

Die FPO- oder Frame-Pointer-Auslassungsoptimierung, die Sie aktivieren können, beseitigt dies tatsächlich und verwendet sie ebpals ein anderes Register und greift direkt auf Lokale zu esp. Dies erschwert jedoch das Debuggen etwas, da der Debugger nicht mehr direkt auf die Stack-Frames früherer Funktionsaufrufe zugreifen kann.

BEARBEITEN:

Für Ihre aktualisierte Frage fehlen zwei Einträge im Stapel:

var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
*savedFramePointer = dword ptr 0*
*return address = dword ptr 4*
hInstance = dword ptr  8h
PrevInstance = dword ptr  0C
hlpCmdLine = dword ptr  10h
nShowCmd = dword ptr  14h

Dies liegt daran, dass der Ablauf des Funktionsaufrufs wie folgt ist:

  • Push-Parameter ( hInstanceusw.)
  • Aufruffunktion, die die Absenderadresse drückt
  • drücken ebp
  • Weisen Sie den Einheimischen Platz zu

1
Danke für die Erklärung! Aber ich bin jetzt ein bisschen verwirrt. Nehmen wir an, ich rufe eine Funktion auf und befinde mich in der ersten Zeile ihres Prologs, ohne eine einzige Zeile daraus ausgeführt zu haben. Welchen Wert hat ebp zu diesem Zeitpunkt? Hat der Stack zu diesem Zeitpunkt noch etwas anderes als die Push-Argumente? Vielen Dank!
verschlungenes Elysium

3
EBP wird nicht auf magische Weise geändert. Bis Sie also ein neues EBP für Ihre Funktion eingerichtet haben, haben Sie immer noch den Anruferwert. Und neben Argumenten wird der Stapel auch die alte EIP (Rücksprungadresse) enthalten
MSalters

3
Gute Antwort. Es kann jedoch nicht vollständig sein, ohne zu erwähnen, was im Epilog steht: Anweisungen "Verlassen" und "Zurück".
Calmarius

2
Ich denke, dieses Bild wird helfen, einige Dinge über den Fluss zu klären. Denken Sie auch daran, dass der Stapel nach unten wächst. ocw.cs.pub.ro/courses/_media/so/laboratoare/call_stack.png
Andrei-Niculae Petre

Bin ich es oder fehlen alle Minuszeichen im obigen Code-Snippet?
BarbaraKwarc

96

ESP ist der aktuelle Stapelzeiger, der sich jedes Mal ändert, wenn ein Wort oder eine Adresse auf den Stapel geschoben oder von diesem entfernt wird. EBP ist eine bequemere Möglichkeit für den Compiler, die Parameter und lokalen Variablen einer Funktion zu verfolgen, als das ESP direkt zu verwenden.

Im Allgemeinen (und dies kann von Compiler zu Compiler variieren) werden alle Argumente für eine aufgerufene Funktion von der aufrufenden Funktion auf den Stapel verschoben (normalerweise in umgekehrter Reihenfolge, in der sie im Funktionsprototyp deklariert sind, dies variiert jedoch). . Dann wird die Funktion aufgerufen, die die Rücksprungadresse (EIP) auf den Stapel schiebt.

Beim Aufrufen der Funktion wird der alte EBP-Wert auf den Stapel verschoben und EBP auf den Wert von ESP gesetzt. Dann wird der ESP dekrementiert (weil der Stapel im Speicher nach unten wächst), um Platz für die lokalen Variablen und Temporäre der Funktion zuzuweisen. Ab diesem Zeitpunkt befinden sich während der Ausführung der Funktion die Argumente für die Funktion bei positiven Offsets von EBP auf dem Stapel (da sie vor dem Funktionsaufruf verschoben wurden), und die lokalen Variablen befinden sich bei negativen Offsets von EBP (weil sie nach dem Funktionseintrag auf dem Stapel zugeordnet wurden). Aus diesem Grund wird das EBP als Frame-Zeiger bezeichnet , da es auf die Mitte des Funktionsaufruf-Frames zeigt .

Beim Beenden muss die Funktion lediglich ESP auf den Wert von EBP setzen (wodurch die lokalen Variablen vom Stapel freigegeben werden und der Eintrag EBP oben auf dem Stapel verfügbar gemacht wird) und dann der alte EBP-Wert vom Stapel entfernt werden. und dann kehrt die Funktion zurück (die Rücksprungadresse wird in EIP eingefügt).

Bei der Rückkehr zur aufrufenden Funktion kann ESP erhöht werden, um die Funktionsargumente zu entfernen, die unmittelbar vor dem Aufrufen der anderen Funktion auf den Stapel übertragen wurden. Zu diesem Zeitpunkt befindet sich der Stapel wieder in dem Zustand, in dem er sich vor dem Aufrufen der aufgerufenen Funktion befand.


15

Du hast es richtig. Der Stapelzeiger zeigt auf das oberste Element auf dem Stapel, und der Basiszeiger zeigt auf die "vorherige" Oberseite des Stapels, bevor die Funktion aufgerufen wurde.

Wenn Sie eine Funktion aufrufen, wird jede lokale Variable auf dem Stapel gespeichert und der Stapelzeiger wird inkrementiert. Wenn Sie von der Funktion zurückkehren, verlassen alle lokalen Variablen auf dem Stapel den Gültigkeitsbereich. Sie tun dies, indem Sie den Stapelzeiger auf den Basiszeiger zurücksetzen (der vor dem Funktionsaufruf die "vorherige" Spitze war).

Die Speicherzuweisung auf diese Weise ist sehr , sehr schnell und effizient.


14
@Robert: Wenn Sie vor dem Aufruf der Funktion "Vorheriges" oben im Stapel sagen, ignorieren Sie beide Parameter, die unmittelbar vor dem Aufruf der Funktion und des Aufrufers EIP auf den Stapel übertragen werden. Dies könnte die Leser verwirren. Angenommen, in einem Standard-Stack-Frame zeigt EBP auf dieselbe Stelle, auf die ESP unmittelbar nach Eingabe der Funktion zeigte.
Perücke

7

BEARBEITEN: Eine bessere Beschreibung finden Sie unter x86-Demontage / -Funktionen und Stapelrahmen in einem WikiBook über x86-Assembly. Ich versuche, einige Informationen hinzuzufügen, die Sie möglicherweise für die Verwendung von Visual Studio interessieren.

Das Speichern des Aufrufer-EBP als erste lokale Variable wird als Standard-Stack-Frame bezeichnet und kann für fast alle Aufrufkonventionen unter Windows verwendet werden. Es gibt Unterschiede, ob der Aufrufer oder der Angerufene die übergebenen Parameter freigeben und welche Parameter in Registern übergeben werden, diese sind jedoch orthogonal zum Standard-Stapelrahmenproblem.

Wenn Sie über Windows-Programme sprechen, verwenden Sie möglicherweise Visual Studio, um Ihren C ++ - Code zu kompilieren. Beachten Sie, dass Microsoft eine Optimierung namens Frame Pointer Omission verwendet, die es nahezu unmöglich macht, den Stapel zu durchlaufen, ohne die dbghlp-Bibliothek und die PDB-Datei für die ausführbare Datei zu verwenden.

Diese Frame Pointer Omission bedeutet, dass der Compiler das alte EBP nicht an einem Standardspeicherort speichert und das EBP-Register für etwas anderes verwendet. Daher fällt es Ihnen schwer, die EIP des Aufrufers zu finden, ohne zu wissen, wie viel Speicherplatz die lokalen Variablen für eine bestimmte Funktion benötigen. Natürlich bietet Microsoft eine API, mit der Sie auch in diesem Fall Stack-Walks durchführen können. Das Nachschlagen der Symboltabellendatenbank in PDB-Dateien dauert jedoch für einige Anwendungsfälle zu lange.

Um FPO in Ihren Kompilierungseinheiten zu vermeiden, müssen Sie die Verwendung von / O2 vermeiden oder / Oy- explizit zu den C ++ - Kompilierungsflags in Ihren Projekten hinzufügen. Sie verknüpfen wahrscheinlich mit der C- oder C ++ - Laufzeit, die FPO in der Release-Konfiguration verwendet, sodass Sie Schwierigkeiten haben, Stack-Walks ohne die Datei dbghlp.dll durchzuführen.


Ich verstehe nicht, wie EIP auf dem Stapel gespeichert ist. Sollte es nicht ein Register sein? Wie kann ein Register auf dem Stapel sein? Vielen Dank!
verschlungenes Elysium

Das Aufrufer-EIP wird durch den CALL-Befehl selbst auf den Stapel geschoben. Der RET-Befehl holt nur die Oberseite des Stapels und legt ihn in der EIP ab. Wenn Sie Pufferüberläufe haben, kann diese Tatsache verwendet werden, um von einem privilegierten Thread in Benutzercode zu springen.
Perücke

@devouredelysium Der Inhalt (oder Wert ) des EIP-Registers wird auf den Stapel gelegt (oder auf diesen kopiert), nicht auf das Register selbst.
BarbaraKwarc

@BarbaraKwarc Danke für die wertvolle Eingabe. Ich konnte nicht sehen, was dem OP in meiner Antwort fehlte. In der Tat bleiben Register dort, wo sie sind, nur ihr Wert wird von der CPU an den RAM gesendet. Im amd64-Modus wird dies etwas komplexer, aber überlassen Sie dies einer anderen Frage.
Perücke

Was ist mit dem amd64? Ich bin neugierig.
BarbaraKwarc

6

Zunächst zeigt der Stapelzeiger auf den unteren Rand des Stapels, da x86-Stapel von hohen Adresswerten zu niedrigeren Adresswerten aufgebaut werden. Der Stapelzeiger ist der Punkt, an dem der nächste Aufruf (oder Aufruf) den nächsten Wert platziert. Die Operation entspricht der C / C ++ - Anweisung:

 // push eax
 --*esp = eax
 // pop eax
 eax = *esp++;

 // a function call, in this case, the caller must clean up the function parameters
 move eax,some value
 push eax
 call some address  // this pushes the next value of the instruction pointer onto the
                    // stack and changes the instruction pointer to "some address"
 add esp,4 // remove eax from the stack

 // a function
 push ebp // save the old stack frame
 move ebp, esp
 ... // do stuff
 pop ebp  // restore the old stack frame
 ret

Der Basiszeiger befindet sich oben im aktuellen Frame. ebp verweist in der Regel auf Ihre Absenderadresse. ebp + 4 zeigt auf den ersten Parameter Ihrer Funktion (oder auf diesen Wert einer Klassenmethode). ebp-4 zeigt auf die erste lokale Variable Ihrer Funktion, normalerweise den alten Wert von ebp, damit Sie den vorherigen Frame-Zeiger wiederherstellen können.


2
Nein, ESP zeigt nicht auf den Boden des Stapels. Das Speicheradressierungsschema hat nichts damit zu tun. Es spielt keine Rolle, ob der Stapel auf niedrigere oder höhere Adressen anwächst. Die "Oberseite" des Stapels ist immer dort, wo der nächste Wert verschoben wird (auf die Oberseite des Stapels gelegt wird) oder auf anderen Architekturen, wo der zuletzt geschobene Wert abgelegt wurde und wo er derzeit liegt. Daher zeigt ESP immer auf die Oberseite des Stapels.
BarbaraKwarc

1
Auf der Unterseite oder Basis des Stapels hingegen wurde der erste (oder älteste ) Wert abgelegt und dann von neueren Werten abgedeckt. Daher kam der Name "Basiszeiger" für EBP: Er sollte auf die Basis (oder den Boden) des aktuellen lokalen Stapels einer Unterroutine zeigen.
BarbaraKwarc

Barbara, im Intel x86 ist der Stack UPSIDE DOWN. Die Oberseite des Stapels enthält das erste Element, das auf den Stapel geschoben wird, und jedes Element danach wird UNTER das oberste Element geschoben. Am unteren Rand des Stapels werden neue Elemente platziert. Programme werden ab 1k gespeichert und bis unendlich groß. Der Stapel beginnt bei unendlich, realistisch maximal mem minus ROMs, und wächst in Richtung 0. ESP zeigt auf eine Adresse, deren Wert kleiner als die zuerst gepusste Adresse ist.
jmucchiello

1

Es ist lange her, dass ich Assembly-Programmierung durchgeführt habe, aber dieser Link könnte nützlich sein ...

Der Prozessor verfügt über eine Sammlung von Registern, in denen Daten gespeichert werden. Einige davon sind direkte Werte, während andere auf einen Bereich im RAM verweisen. Register werden in der Regel für bestimmte Aktionen verwendet, und jeder Operand in der Assembly benötigt eine bestimmte Datenmenge in bestimmten Registern.

Der Stapelzeiger wird meistens verwendet, wenn Sie andere Prozeduren aufrufen. Bei modernen Compilern wird eine Reihe von Daten zuerst auf dem Stapel abgelegt, gefolgt von der Rücksprungadresse, damit das System weiß, wohin es zurückkehren soll, sobald es aufgefordert wird, zurückzukehren. Der Stapelzeiger zeigt auf die nächste Stelle, an der neue Daten auf den Stapel übertragen werden können, wo sie verbleiben, bis sie wieder zurückspringen.

Basisregister oder Segmentregister zeigen nur auf den Adressraum einer großen Datenmenge. In Kombination mit einem zweiten Regiser teilt der Basiszeiger den Speicher in große Blöcke auf, während das zweite Register auf ein Element in diesem Block zeigt. Basiszeiger zeigen daher auf die Basis von Datenblöcken.

Beachten Sie, dass Assembly sehr CPU-spezifisch ist. Die Seite, auf die ich verlinkt habe, enthält Informationen zu verschiedenen CPU-Typen.


Segmentregister sind auf x86 separat - sie sind gs, cs, ss, und wenn Sie keine Speicherverwaltungssoftware schreiben, berühren Sie sie nie.
Michael

ds ist auch ein Segmentregister und in den Tagen von MS-DOS und 16-Bit-Code mussten Sie diese Segmentregister definitiv gelegentlich ändern, da sie niemals auf mehr als 64 KB RAM verweisen konnten. DOS konnte jedoch auf Speicher mit bis zu 1 MB zugreifen, da 20-Bit-Adresszeiger verwendet wurden. Später bekamen wir 32-Bit-Systeme, einige mit 36-Bit-Adressregistern und jetzt 64-Bit-Registern. Heutzutage müssen Sie diese Segmentregister nicht mehr wirklich ändern.
Wim ten Brink

Kein modernes Betriebssystem verwendet 386 Segmente
Ana Betts

@ Paul: FALSCH! FALSCH! FALSCH! Die 16-Bit-Segmente werden durch 32-Bit-Segmente ersetzt. Im geschützten Modus ermöglicht dies die Virtualisierung des Speichers, wodurch der Prozessor im Grunde genommen physische Adressen logischen Adressen zuordnen kann. In Ihrer Anwendung scheinen die Dinge jedoch immer noch flach zu sein, da das Betriebssystem den Speicher für Sie virtualisiert hat. Der Kernel arbeitet im geschützten Modus, sodass Anwendungen in einem Flat-Memory-Modell ausgeführt werden können. Siehe auch en.wikipedia.org/wiki/Protected_mode
Wim ten Brink

@Workshop ALex: Das ist eine technische Angelegenheit. Alle modernen Betriebssysteme setzen alle Segmente auf [0, FFFFFFFF]. Das zählt nicht wirklich. Und wenn Sie die verlinkte Seite lesen würden, würden Sie sehen, dass alle ausgefallenen Dinge mit Seiten gemacht werden, die viel feinkörniger sind als Segmente.
MSalters

-4

Bearbeiten Ja, das ist meistens falsch. Es beschreibt etwas ganz anderes, falls jemand interessiert ist :)

Ja, der Stapelzeiger zeigt auf die Oberseite des Stapels (ob dies die erste leere oder die letzte volle Stapelposition ist, bei der ich mir nicht sicher bin). Der Basiszeiger zeigt auf den Speicherort des Befehls, der ausgeführt wird. Dies ist auf der Ebene der Opcodes - die grundlegendste Anweisung, die Sie auf einem Computer erhalten können. Jeder Opcode und seine Parameter werden an einem Speicherort gespeichert. Eine C- oder C ++ - oder C # -Zeile kann in einen Opcode oder eine Folge von zwei oder mehr übersetzt werden, je nachdem, wie komplex sie ist. Diese werden nacheinander in den Programmspeicher geschrieben und ausgeführt. Unter normalen Umständen wird der Basiszeiger um einen Befehl erhöht. Für die Programmsteuerung (GOTO, IF usw.) kann sie mehrmals erhöht oder einfach durch die nächste Speicheradresse ersetzt werden.

In diesem Zusammenhang werden die Funktionen unter einer bestimmten Adresse im Programmspeicher gespeichert. Wenn die Funktion aufgerufen wird, werden bestimmte Informationen auf den Stapel übertragen, mit denen das Programm feststellen kann, wo sich die Funktion befindet, sowie die Parameter für die Funktion. Anschließend wird die Adresse der Funktion im Programmspeicher in den Speicher verschoben Basiszeiger. Beim nächsten Taktzyklus beginnt der Computer, Anweisungen von dieser Speicheradresse auszuführen. Dann kehrt es irgendwann nach der Anweisung, die die Funktion aufgerufen hat, zum Speicherort zurück und fährt von dort fort.


Ich habe ein bisschen Probleme zu verstehen, was das Ebbe ist. Wenn wir 10 Zeilen MASM-Code haben, bedeutet dies, dass ebp immer größer wird, wenn wir diese Zeilen ausführen?
verschlungenes Elysium

1
@Devoured - Nein. Das ist nicht wahr. eip wird zunehmen.
Michael

Sie meinen, dass das, was ich gesagt habe, richtig ist, aber nicht für EBP, sondern für IEP, oder?
verschlungenes Elysium

2
Ja. EIP ist der Befehlszeiger und wird nach Ausführung jedes Befehls implizit geändert.
Michael

2
Oooh mein schlechtes. Ich denke an einen anderen Zeiger. Ich denke, ich werde mein Gehirn auswaschen.
Stephen Friederichs

-8

esp steht für "Extended Stack Pointer" ..... ebp für "Something Base Pointer" .... und eip für "Something Instruction Pointer" ...... Der Stack Pointer zeigt auf die Offset-Adresse des Stack-Segments . Der Basiszeiger zeigt auf die Versatzadresse des zusätzlichen Segments. Der Anweisungszeiger zeigt auf die Versatzadresse des Codesegments. Nun zu den Segmenten ... es handelt sich um kleine 64-KB-Bereiche des Prozessorspeicherbereichs ..... Dieser Prozess wird als Speichersegmentierung bezeichnet. Ich hoffe dieser Beitrag war hilfreich.


3
Dies ist eine alte Frage, jedoch steht sp für Stapelzeiger, bp für Basiszeiger und ip für Befehlszeiger. Das e am Anfang von allen sagt nur, dass es ein 32-Bit-Zeiger ist.
Hyden

1
Die Segmentierung spielt hier keine Rolle.
BarbaraKwarc
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.