Die Kompilierung eines C ++ - Programms umfasst drei Schritte:
Vorverarbeitung: Der Präprozessor nimmt eine C ++ - Quellcodedatei und behandelt die Anweisungen #include
s, #define
s und andere Präprozessoren. Die Ausgabe dieses Schritts ist eine "reine" C ++ - Datei ohne Vorprozessoranweisungen.
Kompilierung: Der Compiler nimmt die Ausgabe des Vorprozessors und erstellt daraus eine Objektdatei.
Verknüpfen: Der Linker nimmt die vom Compiler erstellten Objektdateien und erstellt entweder eine Bibliothek oder eine ausführbare Datei.
Vorverarbeitung
Der Präprozessor verarbeitet die Präprozessoranweisungen wie #include
und #define
. Es ist unabhängig von der Syntax von C ++, weshalb es mit Vorsicht verwendet werden muss.
Es funktioniert auf einer C ++ Quelldatei zu einem Zeitpunkt durch Ersetzen #include
Richtlinien mit dem Inhalt der jeweiligen Dateien (die in der Regel nur Erklärungen sind), von Makros (tun Ersatz #define
), und die Auswahl unterschiedliche Teile des Textes in Abhängigkeit von #if
, #ifdef
und #ifndef
Richtlinien.
Der Präprozessor arbeitet mit einem Strom von Vorverarbeitungstoken. Makrosubstitution ist definiert als Ersetzen von Token durch andere Token (der Operator ##
ermöglicht das Zusammenführen von zwei Token, wenn dies sinnvoll ist).
Nach alledem erzeugt der Präprozessor eine einzelne Ausgabe, die ein Strom von Token ist, die aus den oben beschriebenen Transformationen resultieren. Außerdem werden einige spezielle Markierungen hinzugefügt, die dem Compiler mitteilen, woher die einzelnen Zeilen stammen, damit diese sinnvolle Fehlermeldungen erzeugen können.
In dieser Phase können durch geschickte Verwendung der Direktiven #if
und einige Fehler auftreten #error
.
Zusammenstellung
Der Kompilierungsschritt wird an jedem Ausgang des Präprozessors ausgeführt. Der Compiler analysiert den reinen C ++ - Quellcode (jetzt ohne Präprozessoranweisungen) und konvertiert ihn in Assemblycode. Ruft dann das zugrunde liegende Back-End (Assembler in der Toolchain) auf, das diesen Code zu Maschinencode zusammensetzt und eine tatsächliche Binärdatei in einem bestimmten Format (ELF, COFF, a.out, ...) erzeugt. Diese Objektdatei enthält den kompilierten Code (in binärer Form) der in der Eingabe definierten Symbole. Symbole in Objektdateien werden mit Namen bezeichnet.
Objektdateien können sich auf Symbole beziehen, die nicht definiert sind. Dies ist der Fall, wenn Sie eine Deklaration verwenden und keine Definition dafür angeben. Der Compiler hat nichts dagegen und wird die Objektdatei gerne erstellen, solange der Quellcode wohlgeformt ist.
Mit Compilern können Sie die Kompilierung normalerweise an dieser Stelle beenden. Dies ist sehr nützlich, da Sie damit jede Quellcodedatei separat kompilieren können. Dies bietet den Vorteil, dass Sie nicht alles neu kompilieren müssen, wenn Sie nur eine einzelne Datei ändern.
Die erstellten Objektdateien können in speziellen Archiven, so genannten statischen Bibliotheken, abgelegt werden, um sie später leichter wiederverwenden zu können.
In diesem Stadium werden "normale" Compilerfehler wie Syntaxfehler oder fehlgeschlagene Überlastungsauflösungsfehler gemeldet.
Verknüpfen
Der Linker erzeugt die endgültige Kompilierungsausgabe aus den vom Compiler erstellten Objektdateien. Diese Ausgabe kann entweder eine gemeinsam genutzte (oder dynamische) Bibliothek sein (und obwohl der Name ähnlich ist, haben sie mit den zuvor erwähnten statischen Bibliotheken nicht viel gemeinsam) oder eine ausführbare Datei.
Es verknüpft alle Objektdateien, indem die Verweise auf undefinierte Symbole durch die richtigen Adressen ersetzt werden. Jedes dieser Symbole kann in anderen Objektdateien oder in Bibliotheken definiert werden. Wenn sie in anderen Bibliotheken als der Standardbibliothek definiert sind, müssen Sie dem Linker davon erzählen.
Zu diesem Zeitpunkt sind die häufigsten Fehler fehlende Definitionen oder doppelte Definitionen. Ersteres bedeutet, dass entweder die Definitionen nicht vorhanden sind (dh nicht geschrieben sind) oder dass die Objektdateien oder Bibliotheken, in denen sie sich befinden, nicht an den Linker übergeben wurden. Letzteres ist offensichtlich: Das gleiche Symbol wurde in zwei verschiedenen Objektdateien oder Bibliotheken definiert.
Dieses Thema wird unter CProgramming.com behandelt:
https://www.cprogramming.com/compilingandlinking.html
Folgendes hat der Autor dort geschrieben:
quelle
Auf der Standardfront:
Eine Übersetzungseinheit ist die Kombination von Quelldateien, enthaltenen Headern und Quelldateien abzüglich aller Quellzeilen, die von der Präprozessor-Direktive für bedingte Einbeziehung übersprungen werden.
Der Standard definiert 9 Phasen in der Übersetzung. Die ersten vier entsprechen der Vorverarbeitung, die nächsten drei sind die Kompilierung, die nächste ist die Instanziierung von Vorlagen (die Instanziierungseinheiten erzeugen ) und die letzte ist die Verknüpfung.
In der Praxis wird die achte Phase (die Instanziierung von Vorlagen) häufig während des Kompilierungsprozesses durchgeführt, aber einige Compiler verzögern sie auf die Verknüpfungsphase und einige verteilen sie auf beide.
quelle
Das Dünne ist, dass eine CPU Daten von Speicheradressen lädt, Daten in Speicheradressen speichert und Befehle nacheinander aus Speicheradressen heraus ausführt, wobei einige bedingte Sprünge in der Reihenfolge der verarbeiteten Befehle erfolgen. Jede dieser drei Kategorien von Befehlen beinhaltet das Berechnen einer Adresse an eine Speicherzelle, die in dem Maschinenbefehl verwendet werden soll. Da Maschinenanweisungen abhängig von der jeweiligen Anweisung eine variable Länge haben und wir beim Erstellen unseres Maschinencodes eine variable Länge aneinanderreihen, erfolgt die Berechnung und Erstellung von Adressen in zwei Schritten.
Zuerst legen wir die Speicherzuordnung so gut wie möglich fest, bevor wir wissen, was genau in jeder Zelle vor sich geht. Wir finden die Bytes oder Wörter oder was auch immer heraus, die die Anweisungen und Literale und alle Daten bilden. Wir beginnen einfach damit, Speicher zuzuweisen und die Werte zu erstellen, mit denen das Programm erstellt wird, und notieren uns jeden Ort, an dem wir zurückkehren und eine Adresse festlegen müssen. An dieser Stelle setzen wir einen Dummy, um nur die Position aufzufüllen, damit wir weiterhin die Speichergröße berechnen können. Zum Beispiel könnte unser erster Maschinencode eine Zelle enthalten. Der nächste Maschinencode kann 3 Zellen enthalten, darunter eine Maschinencodezelle und zwei Adresszellen. Jetzt ist unser Adresszeiger 4. Wir wissen, was in der Maschinenzelle, dem Operationscode, steht, aber wir müssen warten, um zu berechnen, was in den Adresszellen steht, bis wir wissen, wo sich diese Daten befinden werden, d. H.
Wenn es nur eine Quelldatei gäbe, könnte ein Compiler theoretisch vollständig ausführbaren Maschinencode ohne Linker erzeugen. In einem Prozess mit zwei Durchläufen könnten alle tatsächlichen Adressen für alle Datenzellen berechnet werden, auf die durch Anweisungen zum Laden oder Speichern von Maschinen verwiesen wird. Und es könnte alle absoluten Adressen berechnen, auf die durch Anweisungen für absolute Sprünge verwiesen wird. So funktionieren einfachere Compiler wie der in Forth ohne Linker.
Mit einem Linker können Codeblöcke separat kompiliert werden. Dies kann den gesamten Prozess der Codeerstellung beschleunigen und eine gewisse Flexibilität bei der späteren Verwendung der Blöcke ermöglichen. Mit anderen Worten, sie können im Speicher verschoben werden, z. B. indem jeder Adresse 1000 hinzugefügt werden, um den Block um 1000 Adresszellen zu erweitern.
Der Compiler gibt also groben Maschinencode aus, der noch nicht vollständig erstellt wurde, aber so angelegt ist, dass wir die Größe von allem kennen, mit anderen Worten, damit wir berechnen können, wo sich alle absoluten Adressen befinden. Der Compiler gibt auch eine Liste von Symbolen aus, bei denen es sich um Name / Adresse-Paare handelt. Die Symbole beziehen sich auf einen Speicheroffset im Maschinencode im Modul mit einem Namen. Der Versatz ist der absolute Abstand zum Speicherort des Symbols im Modul.
Dort kommen wir zum Linker. Der Linker schlägt zuerst alle diese Maschinencodeblöcke Ende an Ende zusammen und notiert, wo jeder beginnt. Anschließend werden die zu fixierenden Adressen berechnet, indem der relative Versatz innerhalb eines Moduls und die absolute Position des Moduls im größeren Layout addiert werden.
Offensichtlich habe ich dies zu stark vereinfacht, damit Sie versuchen können, es zu erfassen, und ich habe absichtlich nicht den Jargon von Objektdateien, Symboltabellen usw. verwendet, der für mich Teil der Verwirrung ist.
quelle
GCC kompiliert ein C / C ++ - Programm in 4 Schritten in eine ausführbare Datei.
Zum Beispiel
gcc -o hello hello.c
wird wie folgt ausgeführt:1. Vorverarbeitung
Vorverarbeitung über den GNU C-Präprozessor (
cpp.exe
), der die Header (#include
) enthält und die Makros (#define
) erweitert.Die resultierende Zwischendatei "hello.i" enthält den erweiterten Quellcode.
2. Zusammenstellung
Der Compiler kompiliert den vorverarbeiteten Quellcode in Assembler-Code für einen bestimmten Prozessor.
Die Option -S gibt an, dass Assembler-Code anstelle von Objektcode erstellt werden soll. Die resultierende Assembly-Datei lautet "hello.s".
3. Montage
Der Assembler (
as.exe
) konvertiert den Assembler-Code in der Objektdatei "hello.o" in Maschinencode.4. Linker
Schließlich
ld.exe
verknüpft der linker ( ) den Objektcode mit dem Bibliothekscode, um eine ausführbare Datei "Hallo" zu erzeugen.quelle
Schauen Sie sich die URL an: http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
Der vollständige Abschlussprozess von C ++ wird in dieser URL klar vorgestellt.
quelle