wake-up-neo.com

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

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 aufweist.)

Kann mir jemand erklären, was ebp und esp in diesem Zusammenhang sind?

Aus meiner Sicht würde ich sagen, dass der Stapelzeiger immer auf den Anfang des Stapels und der Basiszeiger auf den Anfang der aktuellen Funktion zeigt. Oder was?


edit: ich meine das im zusammenhang mit windows-programmen

edit2: Und wie funktioniert eip auch?

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 von ihnen scheinen Dwords zu sein und nehmen daher jeweils 4 Bytes ein. So kann ich 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?


(Anmerkung des Herausgebers: ein langes Zitat aus Michaels Antwort entfernt, das nicht zur Frage gehört, in dem jedoch eine Folgefrage bearbeitet wurde):

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 (hoffentlich zuletzt!) Ist nun, was genau passiert, wenn ich die Argumente der Funktion, die ich aufrufen möchte, bis zum Ende des Prologs einblende. Ich möchte wissen, wie sich das ebp in diesen Momenten entwickelt (ich habe bereits verstanden, wie der Prolog funktioniert, ich möchte nur wissen, was passiert, nachdem ich die Argumente auf dem Stapel und vor dem Prolog verschoben habe).

207

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

ebp wird normalerweise zu Beginn der Funktion auf esp gesetzt. Auf Funktionsparameter und lokale Variablen wird durch Addieren bzw. Subtrahieren eines konstanten Offsets von ebp zugegriffen. Alle x86-Aufrufkonventionen definieren ebp so, dass es bei Funktionsaufrufen beibehalten wird. ebp selbst zeigt tatsächlich auf den Basiszeiger des vorherigen Frames, wodurch das Stapeln in einem Debugger und das Anzeigen lokaler Variablen anderer Frames möglich wird.

Die meisten Funktions-Prologe 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 haben Sie möglicherweise Code wie (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

FPO oder Frame Pointer Auslassung Optimierung, die Sie aktivieren können, wird dies tatsächlich beseitigen und ebp als ein anderes Register verwenden und direkt von esp auf Locals zugreifen, aber dies macht das Debuggen zu einem Problem Etwas schwieriger, da der Debugger nicht mehr direkt auf die Stack-Frames früherer Funktionsaufrufe zugreifen kann.

BEARBEITEN:

Für Ihre aktualisierte Frage sind die fehlenden 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 (hInstance usw.)
  • Anruffunktion, die die Absenderadresse drückt
  • ebp drücken
  • Platz für Einheimische reservieren
216
Michael

ESP ist der aktuelle Stapelzeiger, der sich jedes Mal ändert, wenn ein Wort oder eine Adresse auf den Stapel verschoben oder aus ihm entfernt wird. EBP bietet dem Compiler eine bequemere Möglichkeit, die Parameter und lokalen Variablen einer Funktion zu verfolgen, als die direkte Verwendung von ESP.

Im Allgemeinen (und dies kann von Compiler zu Compiler variieren) werden alle Argumente zu einer aufgerufenen Funktion von der aufrufenden Funktion auf den Stack verschoben (in der Regel in der umgekehrten Reihenfolge, in der sie im Funktionsprototyp deklariert wurden, dies ist jedoch unterschiedlich). . 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 Stack geschoben und EBP auf den Wert von ESP gesetzt. Dann wird ESP) dekrementiert (da der Stack im Speicher nach unten wächst), um Platz für die lokalen Variablen und Temporäre der Funktion zu reservieren Die Funktionen befinden sich auf dem Stack bei positiven Offsets von EBP (weil sie vor dem Funktionsaufruf verschoben wurden), und die lokalen Variablen befinden sich bei negativen Offsets von EBP (weil sie nach der Funktionseingabe auf dem Stack zugewiesen wurden) Der EBP wird als Frame-Zeiger bezeichnet, da er auf die Mitte des Funktionsaufruf-Frames zeigt .

Beim Beenden muss die Funktion nur ESP auf den Wert von EBP gesetzt werden (wodurch die lokalen Variablen aus dem Stapel freigegeben werden und der Eintrag EBP oben im Stapel verfügbar gemacht wird) Pop den alten EBP-Wert aus dem Stapel, und dann kehrt die Funktion zurück (Poppen der Rücksprungadresse in EIP).

Wenn Sie zur aufrufenden Funktion zurückkehren, können Sie ESP) inkrementieren, um die Funktionsargumente zu entfernen, die direkt vor dem Aufrufen der anderen Funktion auf den Stapel geschrieben wurden im selben Zustand war es vor dem Aufrufen der aufgerufenen Funktion.

80
David R Tribble

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

Wenn Sie eine Funktion aufrufen, wird eine lokale Variable auf dem Stapel gespeichert und der Stapelzeiger inkrementiert. Wenn Sie von der Funktion zurückkehren, verlassen alle lokalen Variablen auf dem Stapel den Gültigkeitsbereich. Dazu setzen Sie den Stapelzeiger zurück auf den Basiszeiger (der vor dem Funktionsaufruf der "vorherige" Anfang war).

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

15
Robert Cartaino

BEARBEITEN: Eine bessere Beschreibung finden Sie unter x86-Demontage/Funktionen und Stapelrahmen in einem WikiBook über x86-Assembly. Ich versuche, einige Informationen hinzuzufügen, an denen Sie mit Visual Studio interessiert sein könnten.

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

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

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

Um FPO in Ihren Kompilierungseinheiten zu vermeiden, müssen Sie/O2 vermeiden oder/Oy- explizit zu den C++ - Kompilierungsflags in Ihren Projekten hinzufügen. Sie stellen wahrscheinlich eine Verknüpfung zur C- oder C++ - Laufzeit her, die FPO in der Release-Konfiguration verwendet, sodass Sie Schwierigkeiten haben, Stapelvorgänge ohne die dbghlp.dll durchzuführen.

7
wigy

Erstens zeigt der Stapelzeiger auf den unteren Teil des Stapels, da x86-Stapel von hohen Adresswerten zu niedrigeren Adresswerten aufgebaut werden. Der Stapelzeiger ist der Punkt, an dem beim nächsten Aufruf von Push (oder Call) der nächste Wert platziert wird. Die Bedienung 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 Rücksendeadresse. ebp + 4 zeigt auf den ersten Parameter Ihrer Funktion (oder 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.

6
jmucchiello

Es ist lange her, dass ich die 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 Assembly erfordert eine bestimmte Datenmenge in bestimmten Registern.

Der Stapelzeiger wird meistens verwendet, wenn Sie andere Prozeduren aufrufen. Bei modernen Compilern wird zuerst eine Reihe von Daten auf den Stapel kopiert, gefolgt von der Absenderadresse, 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 in den Stapel verschoben werden können. Dort bleiben sie, bis sie wieder zurückgesetzt werden.

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

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

1
Wim ten Brink