Wie genau ist der callstack Arbeit?
Ich versuche, ein tieferes Verständnis darüber, wie die low-level-Operationen von Programmiersprachen zu arbeiten und vor allem, wie Sie interagieren mit der OS/CPU. Ich habe wohl gelesen, jede Antwort in jedem stack - /heap-bezogenen thread hier auf Stack Überlauf, und Sie sind alle genial. Aber es gibt noch eine Sache, die ich nicht ganz verstehen aber.
Betrachten Sie diese Funktion in pseudo-code, die dazu neigt, um gültig zu sein Rost-code; -)
fn foo() {
let a = 1;
let b = 2;
let c = 3;
let d = 4;
//line X
doSomething(a, b);
doAnotherThing(c, d);
}
Dies ist, wie ich annehme, den Stapel zu schauen, wie in Zeile X:
Stack
a +-------------+
| 1 |
b +-------------+
| 2 |
c +-------------+
| 3 |
d +-------------+
| 4 |
+-------------+
Nun, alles, was ich gelesen habe darüber, wie der stack funktioniert, ist, dass es strikt befolgt LIFO-Regeln (last in, first out). Genau wie ein stack-Datentyp in .NET, Java oder jede andere Programmiersprache.
Aber wenn das der Fall ist, dann was passiert nach der Linie X? Denn natürlich, das nächste, was wir brauchen, ist die Arbeit mit a
und b
, aber das würde bedeuten, dass die OS/CPU (?) hat, um pop-out d
und c
erste, um wieder zu a
und b
. Aber dann wäre es zu Schießen selbst in den Fuß, da braucht es c
und d
in die nächste Zeile.
So, ich Frage mich, was genau passiert hinter den kulissen?
Andere Frage. Betrachten wir übergeben Sie einen Verweis auf eine der anderen Funktionen wie diese:
fn foo() {
let a = 1;
let b = 2;
let c = 3;
let d = 4;
//line X
doSomething(&a, &b);
doAnotherThing(c, d);
}
So, wie ich die Dinge verstehen, würde dies bedeuten, dass die Parameter in doSomething
sind im wesentlichen auf das gleiche Speicheradresse wie a
und b
im foo
. Aber dann wieder das bedeutet, dass es keine pop in den stack, bis wir a
und b
passiert.
Diese beiden Fälle machen mich denken, dass ich noch nicht vollständig Begriffen, wie genau der stack funktioniert und wie folgt strikt der LIFO Regeln.
In anderen Worten,
LIFO
bedeutet, dass Sie können hinzufügen oder entfernen von Elementen nur am Ende der Stapel, und man kann immer Lesen/ändern element.Warum gehst du nicht zerlegen einer einfachen Funktion nach dem kompilieren mit -O0 und Blick auf die generierten Anweisungen? Es ist ziemlich, naja, aufschlussreich ;-). Finden Sie, dass der code macht guten Gebrauch von der R-Teil des RAM; es greift auf die Adressen direkt auf wird. Sie können denken, der den Namen einer Variablen als offset zur Adresse register (stack-pointer). Wie die anderen gesagt haben, ist der Stapel nur LIFO-mit Bezug auf das stapeln (gut für die Rekursion etc.). Es ist nicht LIFO-mit Bezug auf auf es zu. Der Zugang ist völlig zufällig.
Sie können Ihre eigenen stack-Datenstruktur mit einem array, und nur die Speicherung der index des obersten Elements, erhöht, wenn Sie drücken, Dekrementieren, wenn Sie pop. Wenn Sie dies getan haben, würden Sie immer noch Zugriff auf jedes einzelne element im array zu jeder Zeit, ohne drücken oder knallen, wie Sie können immer mit arrays. Ungefähr das gleiche ist hier passiert.
Im Grunde ist die Benennung stack/heap ist bedauerlich. Sie tragen wenig ähnlichkeit mit der stack-und heap-Datenstrukturen' Terminologie, so nannte Sie die gleiche ist, ist sehr verwirrend.
InformationsquelleAutor Christoph | 2014-06-01
Du musst angemeldet sein, um einen Kommentar abzugeben.
Den call-stack könnte auch frame genannt-stack.
Die Dinge, die gestapelt nach dem LIFO-Prinzip sind nicht die lokalen Variablen, sondern die gesamte stack-frames ("calls") der Funktionen aufgerufen wird,. Die lokalen Variablen geschoben und tauchte zusammen mit den Bildern in die so genannte Funktion Einleitung und Epilog, beziehungsweise.
Innerhalb des Rahmens der Reihenfolge der Variablen ist völlig unspezifisch; Compiler "neu ordnen" die Positionen von lokalen Variablen, die innerhalb eines Rahmens entsprechend zu optimieren, Ihre Ausrichtung so kann der Prozessor Holen Sie Sie so schnell wie möglich. Die entscheidende Tatsache ist, dass der offset der Variablen relativ zu einer festen Adresse ist konstant während der gesamten Lebensdauer des Rahmens - so genügt es, um eine Anker-Adresse, sagen wir, die Adresse des Rahmens selbst, und arbeiten mit offsets, die Adresse der Variablen. So ein Anker-Adresse ist tatsächlich enthalten, in der sogenannten Basis oder frame-pointer das gespeichert ist, in das EBP-register. Die offsets, die auf der anderen Seite, klar, der zur Kompilierzeit bekannt und daher sind hardcoded in den Maschinen-code.
Diese Grafik aus Wikipedia zeigt, was die typischen call-stack aufgebaut wie1:
Fügen Sie den offset einer variable, die wir wollen, um Zugriff auf die darin enthaltene Adresse in den frame-pointer, und wir erhalten die Adresse unserer variable. Also kurz gesagt, der code direkt zugreift Sie direkt über Konstante compile-Zeit-offsets von der Basis-Zeiger; Es ist eine einfache Zeiger-Arithmetik.
Beispiel
gcc.godbolt.org gibt uns
.. für
main
. Ich unterteilt den code in drei Unterabschnitte.Die Funktion der Einleitung besteht aus den ersten drei Operationen:
Dann
cin
verschoben wird in die EDI-register2 undget
aufgerufen; Der Rückgabewert in EAX.So weit So gut. Nun ist die interessante Sache passiert:
Low-order byte von EAX, gekennzeichnet durch die 8-bit-register AL, genommen wird, und gespeichert in einem byte gleich nach der base pointer: Das ist
-1(%rbp)
den offset der base-pointer ist-1
. Dieses byte ist unsere variablec
. Die Einpresstiefe ist negativ, weil der stack wächst nach unten auf x86. Die nächste operation speichertc
im EAX: EAX verschoben ESIcout
bewegt wird, um EDI und dann die Einfügemarke-operator aufgerufen wird, mitcout
undc
als Argumente.Schließlich
main
gespeichert in EAX: 0. Das ist wegen der implizitereturn
- Anweisung.Sie können auch sehen
xorl rax rax
stattmovl
.leave
ist Abkürzung dieser Epilog und implizitNach dieser operation und
ret
durchgeführt wurden, hat der Rahmen effektiv aufgetaucht, obwohl der Anrufer noch zu bereinigen, die Argumente, da wir die cdecl-Aufrufkonvention. Andere Konventionen, z.B. stdcall, bedürfen der angerufene in Ordnung zu bringen, z.B. indem die Anzahl von bytesret
.Frame Pointer Omission
Ist es auch möglich, nicht zu verwenden offsets von der Basis/frame-pointer, aber von der stack-pointer (ESB) statt. Dies macht das EBP-register, die sonst enthalten die frame-pointer-Wert für beliebige verwenden - aber es kann debugging unmöglich, auf einigen Maschinen, und wird implizit ausgeschaltet für einige Funktionen. Es ist besonders nützlich beim kompilieren für Prozessoren mit wenigen Registern, einschließlich der x86-Architektur.
Diese Optimierung ist bekannt als FPO (frame pointer omission) und setzen durch
-fomit-frame-pointer
im GCC und der-Oy
im Klang; beachten Sie, dass es implizit ausgelöst durch jede Optimierung > 0, wenn und nur wenn die debugging immer noch möglich ist, da es nicht irgendwelche Kosten davon abgesehen.Weitere Informationen finden Sie unter hier und hier.
1 Wie schon in den Kommentaren, den frame-pointer ist vermutlich gemeint, um zu zeigen Sie die Adresse nach der Rückkehr-Adresse.
2 Beachten Sie, dass die Register, die beginnen mit R sind die 64-bit-Pendants von denen, die beginnen mit E. EAX bezeichnet die vier niederwertigen bytes der RAX. Ich verwendete den Namen des 32-bit-Register für Klarheit.
Ich denke, es ist ein kleiner Fehler in der Zeichnung. Der frame-pointer auf der anderen Seite der return-Adresse. Verlassen einer Funktion erfolgt in der Regel wie folgt vor: bewegen stack-pointer auf den frame-pointer, dem Anrufer-pop-frame-pointer vom stack zurückgeben (D. H. dem Anrufer-pop-Programm-Zähler / instruction pointer vom stack.)
kasperd, ist absolut richtig. Sie sind entweder nicht verwenden die frame-Zeiger zu allen (gültig Optimierung und insbesondere für die register-starved-Architekturen wie x86 extrem nützlich) oder Sie es verwenden, und speichern Sie die Vorherige auf den Stapel - in der Regel gleich nach der Rückkehr-Adresse. Wie der Rahmen gesetzt ist und entfernt werden, hängt zu einem großen Teil von der Architektur und ABI. Es gibt durchaus ein paar Architekturen (Hallo Itanium), wo die ganze Sache ist.. interessanter (und da gibt es Dinge wie variable-sized-argument-Listen!)
Ich denke, man nähert sich diesem aus einer konzeptionellen Sicht. Hier ist ein Kommentar, wird hoffentlich klar, das Das RTS-oder RunTime-Stack, ist ein bisschen anders als die anderen stacks, ist, dass es eine "schmutzige stack" - dort ist eigentlich alles, was verhindert, dass Sie sich von uns auf einen Wert, der nicht auf der Oberseite. Beachten Sie, dass in dem Diagramm der "Return-Adresse" für die grüne Methode - welche die blau-Methode! ist nach dem Parameter. Wie funktioniert die blau-Methode bekommen, die Rückkehr Wert, nachdem das Vorherige Bild war aufgetaucht? Gut, es ist eine schmutzige stack, so kann es nur erreichen und es packen.
Frame pointer ist eigentlich nicht notwendig, da kann man immer verwenden offsets aus dem stack-pointer statt. GCC-targeting-x64-Architekturen standardmäßig verwendet, stack-pointer und schafft
rbp
um andere Arbeit zu erledigen.InformationsquelleAutor Columbo
Kurz:
Gibt es keine Notwendigkeit, um pop-die Argumente. Die übergebenen Argumente vom Anrufer
foo
FunktiondoSomething
und die lokalen Variablen indoSomething
können alle verwiesen werden, die als ein offset von der base pointer.So,
Im detail:
Die Regel ist, dass jeder Funktionsaufruf Ergebnisse in die Erstellung des stack-Frames (mit dem minimum der Adresse zurückzukehren). Also, wenn
funcA
AnrufefuncB
undfuncB
AnrufefuncC
drei stack-frames eingerichtet sind, eine auf der Oberseite des anderen. , Wenn eine Funktion gibt, sein Rahmen wird ungültig. Eine gut konzipierte Funktion wirkt nur auf seinen eigenen stack-frame und nicht Hausfriedensbruch auf der anderen. In anderen Worten, die POPing durchgeführt wird, bis der stack-frame auf der Oberseite (bei der Rückkehr aus der Funktion).Dem Stapel in Ihrer Frage ist setup, indem Sie den Anrufer
foo
. WenndoSomething
unddoAnotherThing
genannt werden, dann Sie setup Ihren eigenen stack. Die Abbildung kann Ihnen helfen, dies zu verstehen:Beachten Sie, dass, Zugriff auf die Argumente, die der Funktion Körper, um die traverse nach unten (höhere Adressen) von dem Ort, wo die Rückkehr-Adresse wird gespeichert und für den Zugriff auf die lokalen Variablen der Funktion Körper zu durchqueren, bis der stack (zu kleineren Adressen), die relativ zu dem Ort, wo die Rücksprungadresse gespeichert ist. In der Tat, typische compiler generiert code für die Funktion wird genau dies tun. Der compiler widmet ein register genannt EBP (Base Pointer). Ein anderer name für die gleiche frame-pointer. Der compiler in der Regel, wie die erste, was für den Rumpf der Funktion, wird der aktuelle EBP-Wert auf den stack und setzt den EP auf den aktuellen ESP. Dies bedeutet, dass, sobald dies geschehen ist, in jedem Teil der Funktion code, argument 1 ist EBP+8 Weg (4 bytes für jedes Anrufers EBP und die Rücksprungadresse), argument 2 ist EBP+12(dezimal) entfernt, lokale Variablen EBP-4n entfernt.
Werfen Sie einen Blick auf das folgende C-code für die Bildung der stack-frame der Funktion:
Wenn der Anrufer nennen es
dem folgenden code wird generiert
- und den Assembler-code für die Funktion (set-up vom aufgerufenen vor der Rückkehr)
Referenzen:
Was Meinst Du mit "wird der aktuelle EBP-Wert auf den stack" und auch nicht, stack-pointer wird gespeichert im register oder zu nimmt eine position im stack ... ich bin etwas verwirrt
Und Sollte das nicht sein *[ebp + 8] [ebp + 8] .?
Jain; weißt du, was ist
EBP
undESP
?esp ist der stack pointer und ebp base pointer. Wenn ich einige vermissen wissen , bitte korrigieren.
InformationsquelleAutor haccks
Wie andere erwähnt, gibt es keine Notwendigkeit, pop-Parameter, bis Sie gehen Sie out-of-scope.
Werde ich fügen Sie einige Beispiel aus "Pointer und Speicher" von Nick Parlante.
Ich denke, die situation ist ein bisschen einfacher, als Sie sich das vorgestellt haben.
Hier ist der code:
Den Punkte in der Zeit
T1, T2, etc
. sind markiertden code und den Zustand der Speicher zu dieser Zeit ist in der Zeichnung dargestellt:
InformationsquelleAutor
Unterschiedliche Prozessoren und Sprachen ein paar verschiedene stack-designs. Zwei traditionelle Muster sowohl auf der 8x86 und 68000 genannt werden die Pascal-Aufrufkonvention und die C-Aufrufkonvention; jedes übereinkommen ist auf die gleiche Weise behandelt in den beiden Prozessoren, außer für die Namen der Register. Jede verwendet zwei Register zur Verwaltung des Stacks und der damit verbundenen Variablen, bezeichnet der stack pointer (SP oder A7) und der frame-pointer (BP oder A6).
Beim Aufruf Unterroutine, die Sie benutzt entweder die Konvention, alle Parameter werden auf den stack geschoben, vor dem Aufruf der routine. Die routine code, dann wird der aktuelle Wert des frame-pointer auf dem stack kopiert den aktuellen Wert der stack-pointer auf den frame-pointer, und subtrahiert von der stack-pointer die Anzahl der bytes verwendet, die von lokalen Variablen [wenn überhaupt]. Sobald das getan ist, auch wenn zusätzliche Daten auf den Stapel verschoben werden, alle lokalen Variablen gespeichert werden, die auf Variablen mit einer Konstante negative Verschiebung aus der stack-pointer, und alle Parameter auf den stack geschoben, indem der Anrufer kann zugegriffen werden, auf eine Konstante positive Verschiebung von der frame-pointer.
Den Unterschied zwischen den beiden Konventionen liegt in der Art, wie Sie behandeln einen Ausstieg aus der subroutine. In der C-Konvention, die Rückkehr-Funktion kopiert den frame-pointer, stack-pointer [wiederherstellen, um den Wert, den es hatte nur nach den alten frame-Zeiger geschoben wurde], erscheint der alte frame-pointer-Wert und führt einen zurück. Alle Parameter, die der Anrufer hatte auf den stack geschoben, bevor der Anruf bleibt bestehen. In der Pascal-Konvention, nach der Einnahme der alte frame-pointer, der Prozessor erscheint die Funktion, die Rücksprungadresse fügt der stack-pointer die Anzahl der bytes der Parameter schob sich durch die Anrufer, und geht dann zu der knallte die Adresse zurück. Auf der original 68000 war es notwendig, eine 3-instruction-Folge zu löschen die Parameter des Aufrufers; die 8x86 und alle 680x0-Prozessoren, die nach dem original ein "ret N" [oder 680x0-äquivalent] Instruktion, die würden hinzufügen N, um den stack-Zeiger beim ausführen einer Rückkehr.
Die Pascal-Konvention hat den Vorteil der ersparnis ein wenig code auf der Anrufer-Seite, da der Anrufer nicht zur Aktualisierung der stack-pointer nach einem Aufruf der Funktion. Es erfordert allerdings, dass die aufgerufene Funktion die genau wissen, wie viele bytes der Wert von Parameter, die der Anrufer wird auf den Stapel gelegt. Andernfalls drücken Sie die richtige Anzahl der Parameter auf dem Stapel vor dem aufrufen einer Funktion, die verwendet die Pascal-Konvention ist fast garantiert, um einen Absturz verursachen. Dies wird ausgeglichen, jedoch durch die Tatsache, dass ein wenig extra-code in jede aufgerufene Methode speichern wird der code an den stellen, wo die Methode aufgerufen wird. Aus diesem Grund, die meisten der ursprünglichen Macintosh-toolbox-Routinen benutzt die Pascal-Aufrufkonvention.
Die C-Aufrufkonvention hat den Vorteil, dass die Routinen, die zu akzeptieren eine variable Anzahl von Parametern, und als robuster, auch wenn eine routine nicht alle Parameter übergeben werden (der Anrufer wissen, wie viele bytes im Wert von Parametern, die es geschoben, und wird somit in der Lage sein, Sie zu bereinigen). Weiter, es ist nicht notwendig, stack cleanup nach jedem Aufruf der Funktion. Wenn eine routine nennt vier Funktionen in der Reihenfolge, von denen jeder verwendet vier bytes im Wert von Parametern, es kann-statt mit einer
ADD SP,4
nach jedem Anruf verwenden Sie eineADD SP,16
nach dem letzten Aufruf von cleanup Parameter aus allen vier Anrufe.Heutzutage die beschriebenen Aufruf-Konventionen werden als etwas antiquiert. Da Compiler bekommen haben, effizienter bei der Nutzung registrieren, ist es üblich, Methoden akzeptieren einige Parameter in Registern ist nicht erforderlich, dass alle Parameter werden auf den stack geschoben; wenn Sie eine Methode verwenden können, registriert zu halten, werden alle Parameter und lokale Variablen, es gibt keine Notwendigkeit zur Verwendung eines frame-pointer, und damit keine Notwendigkeit zum speichern und wiederherstellen der alten. Dennoch ist es manchmal notwendig, um die ältere Aufrufkonventionen beim aufrufen von Bibliotheken, die verbunden war, um Sie zu benutzen.
Wo kommt der frame und stack-pointer auf dem stack gespeichert selbst oder anderswo ?
Normalerweise wird jede gespeicherte Kopie der frame-pointer werden gespeichert eine Feste Verschiebung relativ zur neuen frame-pointer-Wert.
Sir , ich habe diese Zweifel für eine lange Zeit. Wenn in meinem Amt ich Schreibe, wenn
(g==4)
dannint d = 3
undg
ich nehmen Sie die Eingabe mithilfescanf
nach, dass ich die Definition weiterer Variablenint h = 5
. Nun , Woher weiß der compiler jetzt gebend = 3
Platz im stack. Wie funktioniert die offset getan, denn wenng
ist nicht4
wäre , dann gäbe es keine Speicher für d in den stack und einfach Versatz gegeben würdeh
und wenng == 4
dann offset wird zuerst g und dann fürh
. Wie funktioniert compiler zu tun, dass zur compile-Zeit , die es nicht wissen, dass unser Eingang fürg
Frühe Versionen von C erforderlich ist, dass alle automatischen Variablen innerhalb einer Funktion muss vor allen ausführbaren Anweisungen. Entspannung, die komplizierte Zusammenstellung etwas, aber ein Ansatz ist es, code zu erzeugen, der zu Beginn einer Funktion, die subtrahiert von der SP der Wert eines forward-deklariert label. Innerhalb der Funktion kann der compiler an jeder Stelle im code verfolgen, wie viele bytes der Wert der einheimischen sind noch im Rahmen, und auch verfolgen Sie die maximale Anzahl von Byte im Wert von einheimischen, sind immer im Rahmen. Am Ende der Funktion, kann es den Wert angeben, der für den früheren...
InformationsquelleAutor supercat
Gibt es schon einige wirklich gute Antworten hier. Allerdings, wenn Sie immer noch besorgt über die LIFO-Verhalten des Stacks, betrachten Sie es als einen Stapel von Einzelbildern, sondern als ein Stapel von Variablen. Was ich damit vorschlagen, ist, dass, obwohl eine Funktion kann auf Variablen zuzugreifen, sind nicht auf die oben auf dem Stapel, es ist nur noch Betrieb auf dem Element an der Spitze des stack: ein single-stack-frame.
Natürlich gibt es Ausnahmen von dieser. Die lokalen Variablen der gesamten Aufrufkette sind noch Mittel verfügbar. Aber Sie wird nicht direkt zugegriffen werden. Sie werden stattdessen per Referenz übergeben (oder Zeiger, die ist wirklich nur verschiedene, semantisch). In diesem Fall wird eine lokale variable des stack-Frames liegt viel weiter unten zugegriffen werden kann. Aber auch in diesem Fall, wird die aktuell ausgeführte Funktion ist nur noch Betrieb auf seinen eigenen lokalen Daten. Es ist der Zugriff auf eine Referenz gespeichert, die in seinen eigenen stack-frame, das kann ein Verweis auf etwas, das auf dem heap, im statischen Speicher, oder weiter unten auf dem Stapel.
Dies ist der Teil des Stapels Abstraktion, sind die Funktionen aufrufbar, die in beliebiger Reihenfolge und ermöglicht Rekursion. Die top-stack-frame ist das einzige Objekt, das direkt der code zugegriffen hat. Alles andere ist nur indirekt (über einen Zeiger, der lebt in den obersten stack-Frames).
Es ist aufschlussreich, zu betrachten, die Montage Ihrer kleine Programm, vor allem, wenn Sie kompilieren, ohne Optimierung. Ich denke, Sie werden sehen, dass alle memory-access in Ihrer Funktion geschieht durch einen offset vom stack frame pointer, was ist das, wie der code für die Funktion geschrieben werden, die vom compiler. Im Falle einer übergabe per Referenz, würden Sie sehen, indirekte Speicher Anweisungen für den Zugriff über einen Zeiger, der gespeichert ist in irgendeinem Abstand von der stack-frame-pointer.
InformationsquelleAutor Jeremy West
Den call-stack ist nicht wirklich eine stack-Datenstruktur. Hinter den kulissen, die Computer, die wir verwenden, sind Implementierungen von random-access-Maschine-Architektur. Also, a und b direkt aufgerufen werden können.
Hinter den kulissen, die Maschine tut:
http://en.wikipedia.org/wiki/Random-access_machine
InformationsquelleAutor hdante