c++ länge - Wie funktioniert der Kompilierungs-/ Verknüpfungsprozess?




für meta (5)

Wie funktioniert der Kompilierungs- und Verknüpfungsprozess?

(Hinweis: Dies ist ein Eintrag in die C ++ - FAQ von . Wenn Sie die Idee, eine FAQ in diesem Formular bereitzustellen, kritisieren möchten, dann wäre das Posting auf meta, mit dem all dies begonnen wurde , der richtige Ort dafür Diese Frage wird im C ++ - Chatraum überwacht, wo die FAQ-Idee von Anfang an begann, so dass Ihre Antwort sehr wahrscheinlich von denjenigen gelesen wird, die die Idee hatten.)


Answers

Die Erstellung eines C ++ - Programms umfasst drei Schritte:

  1. Preprocessing: Der Präprozessor nimmt eine C ++ Quellcodedatei und behandelt die #include s, #include #define s und andere Präprozessordirektiven. Die Ausgabe dieses Schritts ist eine "reine" C ++ - Datei ohne Vorprozessor-Anweisungen.

  2. Compilation: Der Compiler übernimmt die Ausgabe des Pre-Prozessors und erzeugt daraus eine Objektdatei.

  3. Verknüpfen: Der Linker übernimmt die vom Compiler erzeugten Objektdateien und erzeugt entweder eine Bibliothek oder eine ausführbare Datei.

Vorverarbeitung

Der Präprozessor behandelt die Präprozessordirektiven wie #include und #define . Es ist agnostisch von der Syntax von C ++, weshalb es mit Vorsicht verwendet werden muss.

Es funktioniert jeweils mit einer C ++ - Quelldatei, indem #include Direktiven durch den Inhalt der entsprechenden Dateien ersetzt werden (was normalerweise nur Deklarationen sind), Ersetzen von Makros ( #define ) und Auswählen verschiedener Textabschnitte in Abhängigkeit von #if , #ifdef und #ifndef Direktiven.

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 Tokens, wenn es sinnvoll ist).

Nach alldem erzeugt der Präprozessor eine einzelne Ausgabe, die ein Strom von Token ist, der aus den oben beschriebenen Transformationen resultiert. Es fügt auch einige spezielle Markierungen hinzu, die dem Compiler mitteilen, woher die einzelnen Zeilen kommen, damit sie sinnvolle Fehlermeldungen erzeugen können.

Einige Fehler können in diesem Stadium mit geschickter Verwendung der Direktiven #if und #error .

Zusammenstellung

Der Kompilierungsschritt wird an jedem Ausgang des Präprozessors durchgeführt. Der Compiler analysiert den reinen C ++ - Quellcode (jetzt ohne Präprozessor-Direktiven) und konvertiert ihn in Assembler-Code. Ruft dann das zugrunde liegende Back-End (Assembler in der Toolchain) auf, das diesen Code in den Maschinencode einfügt, wodurch eine tatsächliche Binärdatei in einem bestimmten Format erzeugt wird (ELF, COFF, a.out, ...). 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 stört das nicht und produziert die Objektdatei, solange der Quellcode wohlgeformt ist.

Mit Compilern können Sie die Kompilierung an dieser Stelle normalerweise beenden. Dies ist sehr nützlich, weil Sie damit jede Quellcodedatei separat kompilieren können. Der Vorteil ist, dass Sie nicht alles neu kompilieren müssen, wenn Sie nur eine einzige Datei ändern.

Die erstellten Objektdateien können in speziellen Archiven, sogenannten statischen Bibliotheken, abgelegt werden, um später die Wiederverwendung zu erleichtern.

In diesem Stadium werden "normale" Compilerfehler, wie Syntaxfehler oder Fehler bei Überladungsfehlern, gemeldet.

Verknüpfung

Der Linker erzeugt die endgültige Kompilierungsausgabe aus den vom Compiler erzeugten Objektdateien. Diese Ausgabe kann entweder eine gemeinsam genutzte (oder dynamische) Bibliothek sein (und während der Name ähnlich ist, haben sie nicht viel mit den bereits erwähnten statischen Bibliotheken gemeinsam) oder eine ausführbare Datei.

Sie verbindet alle Objektdateien, indem sie die Referenzen auf undefinierte Symbole durch die korrekten Adressen ersetzt. 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 dies dem Linker mitteilen.

Zu diesem Zeitpunkt sind die häufigsten Fehler fehlende Definitionen oder doppelte Definitionen. Erstere bedeutet, dass entweder die Definitionen nicht existieren (dh sie sind nicht geschrieben) 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.


Auf der Standardfront:

  • Eine Übersetzungseinheit ist die Kombination aus Quelldateien, enthaltenen Headern und Quelldateien abzüglich aller Quelllinien, die von der Preprozessor-Direktive für bedingte Inklusion ü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 Templates (erzeugt Instantiierungseinheiten ) und die letzte ist die Verknüpfung.

In der Praxis wird die achte Phase (die Instanziierung von Templates) oft während des Kompilierungsprozesses durchgeführt, aber einige Compiler verzögern sie auf die Verknüpfungsphase und einige verbreiten sie in den beiden.


Der Nachteil besteht darin, dass eine CPU Daten von Speicheradressen lädt, Daten in Speicheradressen speichert und Befehle sequentiell aus Speicheradressen ausführt, mit einigen bedingten Sprüngen in der Folge von verarbeiteten Anweisungen. Jede dieser drei Kategorien von Anweisungen beinhaltet das Berechnen einer Adresse zu einer Speicherzelle, die in dem Maschinenbefehl verwendet werden soll. Da Maschinenbefehle abhängig von dem jeweiligen Befehl eine variable Länge haben und wir eine variable Länge von ihnen zusammenreihen, während wir unseren Maschinencode erstellen, ist ein zweistufiger Prozess bei der Berechnung und Erstellung von Adressen erforderlich.

Zuerst legen wir die Speicherzuweisung so gut wie möglich fest, bevor wir wissen, was genau in jeder Zelle passiert. Wir ermitteln die Bytes oder Wörter oder was auch immer die Anweisungen und Literale und alle Daten bilden. Wir fangen einfach an, Speicher zuzuweisen und die Werte zu erstellen, die das Programm erstellen werden, und notieren uns überall, wo wir zurückgehen und eine Adresse korrigieren müssen. An dieser Stelle setzen wir einen Dummy, um den Speicherplatz zu füllen, damit wir die Speichergröße berechnen können. Zum Beispiel könnte unser erster Maschinencode eine Zelle benötigen. Der nächste Maschinencode könnte 3 Zellen umfassen, die eine Maschinencodezelle und zwei Adresszellen umfassen. Jetzt ist unser Adresszeiger 4. Wir wissen, was in der Maschinenzelle passiert, das ist der Op-Code, aber wir müssen warten, um zu berechnen, was in den Adresszellen passiert, bis wir wissen, wo sich diese Daten befinden Maschinenadresse dieser Daten.

Wenn es nur eine Quelldatei gäbe, könnte ein Compiler theoretisch vollständig ausführbaren Maschinencode ohne einen Linker erzeugen. In einem Zwei-Durchlauf-Prozess könnte er alle tatsächlichen Adressen für alle Datenzellen berechnen, auf die Maschinenlast- oder Speicheranweisungen Bezug nehmen. Und es könnte alle absoluten Adressen berechnen, auf die absolute Sprunganweisungen verweisen. So arbeiten einfachere Compiler, wie der in Forth, ohne Linker.

Ein Linker ist etwas, das ermöglicht, Blöcke von Code separat zu kompilieren. Dies kann den Gesamtprozess des Code-Aufbaus beschleunigen und ermöglicht eine gewisse Flexibilität bei der späteren Verwendung der Blöcke, das heißt, sie können im Speicher verschoben werden, z. B. Hinzufügen von 1000 zu jeder Adresse, um den Block um 1000 Adresszellen zu verschieben.

Was der Compiler ausgibt, ist ein grober Maschinencode, der noch nicht vollständig aufgebaut ist, aber so ausgelegt ist, dass wir die Größe von allem kennen, mit anderen Worten, wir können damit beginnen, zu berechnen, wo alle absoluten Adressen liegen. Der Compiler gibt auch eine Liste von Symbolen aus, die Name / Adresse-Paare sind. Die Symbole verbinden einen Speicheroffset im Maschinencode im Modul mit einem Namen. Der Offset ist die absolute Entfernung zum Speicherplatz des Symbols im Modul.

Da kommen wir zum Linker. Der Linker schlägt zunächst alle diese Blöcke des Maschinencodes von Ende zu Ende zusammen und notiert, wo jeder beginnt. Dann berechnet er die zu fixierenden Adressen, indem er den relativen Offset innerhalb eines Moduls und die absolute Position des Moduls im größeren Layout addiert.

Offensichtlich habe ich das vereinfacht, so dass Sie versuchen können, es zu verstehen, und ich habe absichtlich nicht den Jargon von Objektdateien, Symboltabellen usw. benutzt, was für mich Teil der Verwirrung ist.



Vorschlagen einer Alternative für sbi's Punkt 3

a->b wird nur verwendet, wenn a ein Zeiger ist. Es ist eine Abkürzung für (*a).b , das b (*a).b des Objekts, auf das a zeigt. C ++ hat zwei Arten von Zeigern, "normale" und intelligente Zeiger. Für normale Zeiger wie A* a implementiert der Compiler -> . Für Smartpointer wie std::shared_ptr<A> a , -> eine shared_ptr der Klasse shared_ptr .

Begründung: Die Zielgruppe dieser FAQ schreibt keine Smart Pointer. Sie müssen es nicht wissen -> heißt wirklich operator->() , oder es ist die einzige Methode für den Zugriff auf Mitglieder, die überladen werden kann.







c++ compiler-construction linker c++-faq