Wie funktionieren Zeiger in C unter der Haube?

8

Nehmen Sie ein einfaches Programm wie folgt:

%Vor%

Wie wird &p ermittelt? Errechnet der Compiler alle solchen Referenzen vor oder zur Laufzeit? Wenn zur Laufzeit gibt es eine Tabelle von Variablen oder etwas, wo es diese Dinge sieht? Verfolgt das Betriebssystem sie und es fragt nur das Betriebssystem?

Meine Frage mag im Zusammenhang mit der richtigen Erklärung nicht einmal einen Sinn ergeben, also können Sie mich ruhig stellen.

    
Chris Middleton 14.03.2014, 17:34
quelle

6 Antworten

5

Der Compiler kann die vollständige Adresse von p zur Kompilierzeit nicht kennen, da eine Funktion von verschiedenen Aufrufern mehrmals aufgerufen werden kann und p unterschiedliche Werte haben kann.

Natürlich muss der Compiler wissen, wie er die Adresse von p zur Laufzeit berechnen muss, nicht nur für den address-of-Operator, sondern einfach, um Code zu erzeugen, der funktioniert mit der p Variable. Bei einer regulären Architektur werden lokale Variablen wie p auf dem Stapel zugewiesen, d. H. An einer Position mit festem Versatz relativ zur Adresse des aktuellen Stapelrahmens.

Somit speichert die Zeile q = &p einfach in q (eine andere lokale Variable, die auf dem Stapel zugeordnet ist) die Adresse p im aktuellen Stapelrahmen.

Beachten Sie, dass der Compiler im Allgemeinen von der Implementierung abhängig ist oder nicht. Zum Beispiel könnte ein optimierender Compiler Ihre gesamte main sehr gut optimieren, nachdem Sie analysiert haben, dass seine Aktionen keinen beobachtbaren Effekt haben. Das Obige wird unter der Annahme einer Mainstream-Architektur und eines Compilers und einer nicht-statischen Funktion (außer main ) geschrieben, die von mehreren Aufrufern aufgerufen werden kann.

    
user4815162342 14.03.2014, 17:41
quelle
6
  

Wie wird &p ermittelt? Berechnet der Compiler alle solchen Referenzen vorher oder zur Laufzeit?

Dies ist ein Implementierungsdetail des Compilers. Verschiedene Compiler können abhängig von der Art des Betriebssystems, für das sie Code generieren, und den Launen des Compiler-Schreibers unterschiedliche Techniken wählen.

Lassen Sie mich für Sie beschreiben, wie dies typischerweise auf einem modernen Betriebssystem wie Windows durchgeführt wird.

Wenn der Prozess gestartet wird, gibt das Betriebssystem dem Prozess einen virtuellen Adressraum von beispielsweise 2 GB. Von diesen 2 GB wird ein Abschnitt von 1 MB als "der Stapel" für den Hauptthread reserviert. Der Stapel ist ein Bereich des Speichers, in dem alles "unter" dem aktuellen Stapelzeiger "in Gebrauch" ist, und alles in diesem 1MB Abschnitt "darüber" ist es "frei". Wie das Betriebssystem wählt, welcher 1 MB großer virtueller Adressraum der Stack ist, ist ein Implementierungsdetail von Windows.

(Abgesehen davon, ob der freie Speicherplatz an der "Spitze" oder "unten" des Stapels ist, ob der "gültige" Raum "nach oben" oder "nach unten" wächst, ist auch ein Implementierungsdetail. Verschiedene Betriebssysteme auf verschiedenen Chips Anders ausgedrückt: Nehmen wir an, der Stack wächst von hohen Adressen zu niedrigen Adressen.)

Das Betriebssystem stellt sicher, dass, wenn main aufgerufen wird, das Register ESP die Adresse der Trennlinie zwischen dem gültigen und dem freien Teil des Stapels enthält.

(Wiederum: ob ESP die Adresse des ersten gültigen Punktes oder der erste freie Punkt ist, ist ein Implementierungsdetail.)

Der Compiler erzeugt Code für main , der den Stack-Pointer um etwa fünf Bytes schiebt, indem er subtrahiert, wenn der Stack "down" wird. Es verringert sich um fünf, weil es ein Byte für p und vier für q benötigt. Also ändert sich der Stapelzeiger; Es gibt jetzt fünf weitere "gültige" Bytes und fünf weniger "freie" Bytes.

Nehmen wir an, q ist der Speicher, der jetzt in ESP through ESP+3 und p ist der Speicher jetzt in ESP+4 . Um die Adresse von p auf q zuzuweisen, generiert der Compiler Code, der den vier Byte Wert ESP+4 in die Speicherorte kopiert ESP bis ESP+3 .

(Abgesehen davon: Es ist sehr wahrscheinlich, dass der Compiler den Stack so auslegt, dass alles, was seine Adresse hat, auf einem ESP+offset -Wert liegt, der durch vier teilbar ist. Einige Chips haben Anforderungen, dass Adressen durch Zeiger teilbar sind Größe. Wieder ist dies ein Implementierungsdetail.)

Wenn Sie den Unterschied zwischen einer Adresse, die als Wert verwendet wird, und einer Adresse, die als Speicherort verwendet wird, nicht verstehen, sollten Sie dies herausfinden . Ohne diesen Schlüsselunterschied zu verstehen, werden Sie in C nicht erfolgreich sein.

Das ist ein Weg, wie es funktionieren könnte, aber wie ich schon sagte, verschiedene Compiler können sich entscheiden, es nach Belieben anders zu machen.

    
Eric Lippert 14.03.2014 17:53
quelle
2

Dies ist eigentlich eine außerordentlich schwierige Frage, die in voller Allgemeinheit zu beantworten ist, weil sie durch virtuellen Speicher , Adressraum-Layout-Randomisierung und Umzug .

Die kurze Antwort ist, dass der Compiler grundsätzlich Offsets von einer "Basis" behandelt, die vom Runtime Loader beim Ausführen Ihres Programms entschieden wird. Ihre Variablen, p und q , erscheinen sehr nahe am "Ende" des Stapels (obwohl der Stack-Basis ist in der Regel sehr hoch in VM und es wächst "down").

    
Emmet 14.03.2014 17:58
quelle
1

Die Adresse einer lokalen Variablen kann zur Kompilierzeit nicht vollständig berechnet werden. Lokale Variablen werden normalerweise im Stapel zugewiesen. Wenn sie aufgerufen wird, weist jede Funktion einen Stack-Frame zu - einen einzelnen fortlaufenden Speicherblock, in dem alle lokalen Variablen gespeichert werden. Der physische Speicherort des Stapelrahmens im Speicher kann zur Kompilierungszeit nicht vorhergesagt werden. Es wird erst zur Laufzeit bekannt. Der Anfang jedes Stack-Frames wird normalerweise zur Laufzeit in einem dedizierten Prozessorregister gespeichert, wie zB ebp auf der Intel-Plattform.

Inzwischen wird das interne Speicherlayout eines Stapelrahmens durch den Compiler zur Kompilierungszeit vorbestimmt, d. h. der Compiler entscheidet, wie lokale Variablen innerhalb des Stapelrahmens angeordnet werden. Dies bedeutet, dass der Compiler den lokalen Offset jeder lokalen Variablen innerhalb des Stapelrahmens kennt.

Setzen Sie das alles zusammen und wir erhalten, dass die genaue absolute Adresse einer lokalen Variablen ist die Summe der Adresse des Stack-Frame selbst (die Laufzeitkomponente) und der Offset dieser Variablen innerhalb dieses Rahmens (die Kompilierzeit-Komponente).

Das ist im Grunde genau der kompilierte Code für

%Vor%

wird es tun. Es wird den aktuellen Wert des Stack-Frame-Registers annehmen, eine Konstante für die Kompilierzeit hinzufügen (Offset von p ) und das Ergebnis in q speichern.

    
AnT 14.03.2014 17:49
quelle
1

In jeder Funktion werden die Funktionsargumente und die lokalen Variablen auf dem Stack nach der Position (Programmzähler) der letzten Funktion an dem Punkt zugewiesen, an dem sie die aktuelle Funktion aufruft. Wie diese Variablen auf dem Stack zugewiesen und dann bei der Rückkehr von der Funktion freigegeben werden, kümmert sich der Compiler während der Kompilierzeit.

Für z.B. Für diesen Fall könnte p (1 Byte) zuerst auf dem Stapel zugewiesen werden, gefolgt von q (4 Bytes für 32-Bit-Architektur). Der Code ordnet q die Adresse von p zu. Die Adresse von p wird dann natürlich von dem letzten Wert des Stapelzeigers addiert oder subtrahiert. Nun, so etwas hängt davon ab, wie der Wert des Stackpointers aktualisiert wird und ob der Stack nach oben oder unten wächst.

Wie der Rückgabewert an die aufrufende Funktion zurückgegeben wird, ist etwas, von dem ich nicht sicher bin, aber ich nehme an, dass es durch die Register und nicht durch den Stapel geleitet wird. Wenn also die Rückgabe aufgerufen wird, sollte der zugrunde liegende Assemblercode p und q freigeben, Null in das Register setzen und dann zur letzten Position der Aufruferfunktion zurückkehren. Natürlich ist es in diesem Fall die Hauptfunktion, so dass es komplizierter ist, da es bewirkt, dass das Betriebssystem den Prozess beendet. Aber in anderen Fällen geht es einfach zurück zur aufrufenden Funktion.

In ANSI C sollten alle lokalen Variablen an der Spitze der Funktion platziert werden und einmal in den Stapel beim Eingeben der Funktion zugewiesen und bei der Rückkehr von der Funktion freigegeben werden. In C ++ oder späteren Versionen von C wird dies komplizierter, wenn lokale Variablen auch innerhalb von Blöcken deklariert werden können (wie if-else oder while-Anweisungsblöcke). In diesem Fall wird die lokale Variable beim Eingeben des Blocks dem Stapel zugeordnet und beim Verlassen des Blocks freigegeben.

In allen Fällen ist die Adresse einer lokalen Variablen immer eine feste Zahl, die vom Stapelzeiger addiert oder subtrahiert wird (wie vom Compiler berechnet, relativ zum umgebenden Block) und die Größe der Variablen wird vom Variablentyp bestimmt .

Allerdings sind static lokale Variablen und globale Variablen in C unterschiedlich. Diese werden an festen Positionen im Speicher zugewiesen, und somit gibt es eine feste Adresse für sie (oder einen festen Offset relativ zur Prozessgrenze), die wird vom Linker berechnet.

Eine dritte Variante ist der Speicher, der auf dem Heap mithilfe von malloc / new und free / delete zugewiesen wird. Ich denke, diese Diskussion wäre zu langwierig, wenn wir das ebenfalls berücksichtigen würden.

Das heißt, meine Beschreibung bezieht sich nur auf eine typische Hardware-Architektur und ein Betriebssystem. Alle diese sind auch abhängig von einer Vielzahl von Dingen, wie von Emmet erwähnt.

    
ruben2020 14.03.2014 17:44
quelle
1

p ist eine Variable mit automatischem Speicher. Es lebt nur so lange wie die Funktion in Leben ist. Jedes Mal, wenn seine Funktion Speicher aufgerufen wird, wird sie vom Stack übernommen. Daher kann sich ihre Adresse ändern und ist erst zur Laufzeit bekannt.

    
Fiddling Bits 14.03.2014 17:41
quelle

Tags und Links