wake-up-neo.net

Wie funktioniert das Zusammenstellen / Verknüpfen?

Wie funktioniert das Zusammenstellen und Verknüpfen?

(Hinweis: Dies ist ein Eintrag zu Stack Overflow's C++ FAQ . Wenn Sie die Idee, ein FAQ in diesem Formular bereitzustellen, kritisieren möchten, dann wäre das Posting auf Meta, das all dies gestartet hat der richtige Ort, um dies zu tun. Die Antworten auf diese Frage werden im C++ Chatroom überwacht, in dem die Idee FAQ an erster Stelle stand, sodass Ihre Antwort sehr wahrscheinlich von denjenigen gelesen wird, die gekommen sind mit der Idee auf.)

385
Tony The Lion

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

  1. Vorverarbeitung: Der Präprozessor verwendet eine C++ - Quellcodedatei und verarbeitet die Anweisungen #include, #define Und andere Präprozessoranweisungen. Die Ausgabe dieses Schritts ist eine "reine" C++ - Datei ohne Vorprozessor-Direktiven.

  2. Kompilierung: Der Compiler nimmt die Ausgabe des Preprozessors und erzeugt daraus eine Objektdatei.

  3. Verknüpfen: Der Linker verwendet die vom Compiler erstellten Objektdateien und erstellt entweder eine Bibliothek oder eine ausführbare Datei.

Vorverarbeitung

Der Präprozessor behandelt die Präprozessor-Direktiven, wie #include Und #define. Es ist unabhängig von der Syntax von C++, weshalb es mit Vorsicht verwendet werden muss.

Es funktioniert jeweils für eine C++ - Quelldatei, indem #include - Anweisungen durch den Inhalt der jeweiligen Dateien ersetzt werden (normalerweise nur Deklarationen), Makros ersetzt werden (#define) Und verschiedene ausgewählt werden Textteile abhängig von den Anweisungen #if, #ifdef und #ifndef.

Der Präprozessor bearbeitet einen Stream von Präprozessortoken. Makrosubstitution ist definiert als Ersetzen von Tokens durch andere Tokens (der Operator ## Ermöglicht das Zusammenführen von zwei Tokens, wenn es sinnvoll ist).

Nach alledem erzeugt der Präprozessor eine einzelne Ausgabe, die ein Strom von Token ist, die aus den oben beschriebenen Transformationen resultieren. Es werden auch einige spezielle Markierungen hinzugefügt, die dem Compiler mitteilen, woher jede Zeile stammt, damit er diese verwenden kann, um sinnvolle Fehlermeldungen zu erzeugen.

Einige Fehler können zu diesem Zeitpunkt durch geschickte Verwendung der Direktiven #if Und #error Erzeugt werden.

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 Assembly-Code. Ruft dann das zugrunde liegende Back-End (Assembler in der Toolchain) auf, das diesen Code in Maschinencode zusammensetzt und eine tatsächliche Binärdatei in einem bestimmten Format (ELF, COFF, a.out, ...) erstellt. 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. Dem Compiler macht das nichts aus und er wird die Objektdatei gerne erstellen, solange der Quellcode gut strukturiert 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 alles nicht neu kompilieren müssen, wenn Sie nur eine einzelne Datei ändern.

Die erzeugten Objektdateien können in speziellen Archiven abgelegt werden, die als statische Bibliotheken bezeichnet werden, damit sie später leichter wiederverwendet werden können.

In diesem Stadium werden "normale" Compilerfehler wie Syntaxfehler oder Fehler bei der Überladungsauflösung 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 (und obwohl der Name ähnlich ist, haben sie mit den zuvor erwähnten statischen Bibliotheken nicht viel gemein) 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 den Linker darüber informieren.

Zu diesem Zeitpunkt sind die häufigsten Fehler fehlende Definitionen oder doppelte Definitionen. Ersteres bedeutet, dass entweder die Definitionen nicht existieren (d. H. Nicht geschrieben sind) oder dass die Objektdateien oder Bibliotheken, in denen sie sich befinden, nicht an den Linker übergeben wurden. Letzteres ist offensichtlich: Dasselbe Symbol wurde in zwei verschiedenen Objektdateien oder Bibliotheken definiert.

510

Dieses Thema wird auf CProgramming.com behandelt:
https://www.cprogramming.com/compilingandlinking.html

Hier ist, was der Autor dort schrieb:

Kompilieren ist nicht dasselbe wie eine ausführbare Datei erstellen! Stattdessen ist das Erstellen einer ausführbaren Datei ein mehrstufiger Prozess, der in zwei Komponenten unterteilt ist: Kompilieren und Verknüpfen. In der Realität funktioniert ein Programm aufgrund von Fehlern in der Verknüpfungsphase möglicherweise nicht, selbst wenn es "gut kompiliert" wird. Der gesamte Prozess des Wechsels von Quellcodedateien zu einer ausführbaren Datei wird möglicherweise besser als Build bezeichnet.

Zusammenstellung

Die Kompilierung bezieht sich auf die Verarbeitung von Quellcodedateien (.c, .cc oder .cpp) und die Erstellung einer Objektdatei. Dieser Schritt erstellt nichts, was der Benutzer tatsächlich ausführen kann. Stattdessen erstellt der Compiler lediglich die Maschinensprachenanweisungen, die der kompilierten Quellcodedatei entsprechen. Wenn Sie beispielsweise drei separate Dateien kompilieren (aber nicht verknüpfen), werden drei Objektdateien mit dem Namen .o oder .obj als Ausgabe erstellt (die Erweiterung hängt von Ihrem Compiler ab). Jede dieser Dateien enthält eine Übersetzung Ihrer Quellcodedatei in eine Maschinensprachendatei - Sie können sie jedoch noch nicht ausführen! Sie müssen sie in ausführbare Dateien verwandeln, die Ihr Betriebssystem verwenden kann. Hier kommt der Linker ins Spiel.

Verknüpfen

Verknüpfen bezieht sich auf die Erstellung einer einzelnen ausführbaren Datei aus mehreren Objektdateien. In diesem Schritt ist es üblich, dass sich der Linker über undefinierte Funktionen beschwert (üblicherweise main selbst). Wenn der Compiler während der Kompilierung die Definition für eine bestimmte Funktion nicht finden konnte, wird lediglich davon ausgegangen, dass die Funktion in einer anderen Datei definiert wurde. Wenn dies nicht der Fall ist, kann der Compiler dies auf keinen Fall wissen - er betrachtet nicht den Inhalt von mehr als einer Datei gleichzeitig. Andererseits kann der Linker mehrere Dateien betrachten und versuchen, Referenzen für die Funktionen zu finden, die nicht erwähnt wurden.

Möglicherweise werden Sie gefragt, warum es separate Kompilierungs- und Verknüpfungsschritte gibt. Erstens ist es wahrscheinlich einfacher, Dinge auf diese Weise zu implementieren. Der Compiler macht seine Sache und der Linker macht seine Sache - indem die Funktionen getrennt bleiben, wird die Komplexität des Programms verringert. Ein weiterer (offensichtlicherer) Vorteil ist, dass damit große Programme erstellt werden können, ohne dass der Kompilierungsschritt jedes Mal wiederholt werden muss, wenn eine Datei geändert wird. Stattdessen ist es unter Verwendung der sogenannten "bedingten Kompilierung" erforderlich, nur die Quelldateien zu kompilieren, die sich geändert haben. Im Übrigen reichen die Objektdateien als Eingabe für den Linker aus. Schließlich ist es so einfach, Bibliotheken mit vorkompiliertem Code zu implementieren: Erstellen Sie einfach Objektdateien und verknüpfen Sie sie wie jede andere Objektdatei. (Die Tatsache, dass jede Datei separat von Informationen kompiliert wird, die in anderen Dateien enthalten sind, wird übrigens als "separates Kompilierungsmodell" bezeichnet.)

Um alle Vorteile der Bedingungskompilierung nutzen zu können, ist es wahrscheinlich einfacher, ein Programm zu finden, das Ihnen hilft, als sich zu merken, welche Dateien Sie seit der letzten Kompilierung geändert haben. (Sie können natürlich auch jede Datei neu kompilieren, deren Zeitstempel größer ist als der Zeitstempel der entsprechenden Objektdatei.) Wenn Sie mit einer integrierten Entwicklungsumgebung (IDE) arbeiten, kann dies möglicherweise bereits für Sie erledigt werden. Wenn Sie Befehlszeilentools verwenden, gibt es ein nützliches Hilfsprogramm namens make, das mit den meisten * nix-Distributionen geliefert wird. Neben der bedingten Kompilierung gibt es verschiedene andere Funktionen für die Programmierung, z. B. das Zulassen verschiedener Kompilierungen Ihres Programms - beispielsweise, wenn Sie eine Version haben, die eine ausführliche Ausgabe zum Debuggen erzeugt.

Wenn Sie den Unterschied zwischen der Kompilierungsphase und der Link-Phase kennen, können Sie leichter nach Fehlern suchen. Compilerfehler sind normalerweise syntaktischer Natur - ein fehlendes Semikolon, eine zusätzliche Klammer. Verknüpfungsfehler haben in der Regel mit fehlenden oder mehrfachen Definitionen zu tun. Wenn Sie vom Linker den Fehler erhalten, dass eine Funktion oder Variable mehrmals definiert wurde, ist dies ein gutes Indiz dafür, dass der Fehler darin besteht, dass zwei Ihrer Quellcodedateien dieselbe Funktion oder Variable haben.

35
user2003323

Auf der Standardvorderseite:

  • a Übersetzungseinheit ist die Kombination von Quelldateien, eingeschlossenen Kopfzeilen und Quelldateien abzüglich aller Quellzeilen, die von der bedingten Einschluss-Präprozessor-Direktive übersprungen werden.

  • die Norm definiert 9 Phasen in der Übersetzung. Die ersten vier entsprechen der Vorverarbeitung, die nächsten drei sind die Zusammenstellung, die nächste ist die Instanziierung von Vorlagen (Erzeugen von Instanziierungseinheiten) 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 die Verknüpfungsphase und einige verteilen sie auf die beiden.

23
AProgrammer

Der Nachteil ist, dass eine CPU Daten von Speicheradressen lädt, Daten in Speicheradressen speichert und Befehle sequentiell außerhalb der Speicheradressen ausführt, wobei einige bedingte Sprünge in der Reihenfolge der verarbeiteten Befehle auftreten. Jede dieser drei Befehlskategorien beinhaltet das Berechnen einer Adresse für eine Speicherzelle, die in dem Maschinenbefehl verwendet werden soll. Da Maschinenbefehle abhängig von den jeweiligen Befehlen eine variable Länge haben und wir beim Erstellen unseres Maschinencodes eine variable Länge aneinanderreihen, ist ein zweistufiger Prozess zum Berechnen und Erstellen von Adressen erforderlich.

Zuerst legen wir die Speicherzuordnung so gut wie möglich fest, bevor wir wissen, was genau in jeder Zelle vor sich geht. Wir finden heraus, welche Bytes oder Wörter oder was auch immer die Anweisungen und Literale und Daten bilden. Wir beginnen gerade damit, Speicher zuzuweisen und die Werte zu erstellen, die das Programm im Laufe der Zeit erstellen werden, und notieren uns überall, wo wir zurückgehen und eine Adresse festlegen müssen. An dieser Stelle platzieren wir einen Dummy, um nur die Position aufzufüllen, damit wir die Speichergröße weiter berechnen können. Zum Beispiel könnte unser erster Maschinencode eine Zelle umfassen. Der nächste Maschinencode kann 3 Zellen umfassen, die eine Maschinencode-Zelle und zwei Adresszellen umfassen. Jetzt ist unser Adressenzeiger 4. Wir wissen, was in die Maschinenzelle geht, was der Operationscode ist, aber wir müssen warten, um zu berechnen, was in die Adressenzellen geht, bis wir wissen, wo sich diese Daten befinden werden, dh was der sein wird 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 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 absolute Sprungbefehle verwiesen wird. So arbeiten 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 ermöglicht eine gewisse Flexibilität bei der späteren Verwendung der Blöcke. Mit anderen Worten, sie können in den Speicher verschoben werden, beispielsweise indem 1000 zu jeder Adresse hinzugefügt werden, um den Block um 1000 Adresszellen zu vergrößern.

Was der Compiler ausgibt, ist ein grober Maschinencode, der noch nicht vollständig erstellt ist, 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 werden. Der Compiler gibt auch eine Liste von Symbolen aus, die Name/Adresse-Paare sind. Die Symbole beziehen sich auf einen Speicheroffset im Maschinencode des Moduls mit einem Namen. Der Offset ist der absolute Abstand zum Speicherort des Symbols im Modul.

Hier gelangen wir zum Linker. Der Linker schlägt zuerst alle diese Maschinencode-Blöcke von Ende zu Ende und notiert sich, 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 bewusst nicht die Fachsprache von Objektdateien, Symboltabellen usw. verwendet, die für mich Teil der Verwirrung ist.

14
Elliptical view

Schauen Sie sich die URL an: http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
Der vollständige Erfüllungsprozess von C++ wird in dieser URL klar vorgestellt.

9
Charles Wang

GCC kompiliert ein C/C++ - Programm in 4 Schritten in eine ausführbare Datei.

Zum Beispiel ein "gcc -o hello.exe hello.c "wird wie folgt durchgeführt:

1. Vorverarbeitung

Preprocessin über den GNU C Preprocessor (cpp.exe), der die Header (#include) enthält und die Makros erweitert (#define).

cpp hallo.c> hallo.i

Die resultierende Zwischendatei "hello.i" enthält den erweiterten Quellcode.

2. Zusammenstellung

Der Compiler kompiliert den vorverarbeiteten Quellcode in Assembly-Code für einen bestimmten Prozessor.

gcc -S hallo.i

Die Option -S gibt an, dass Assembly-Code anstelle von Objektcode generiert werden soll. Die resultierende Assembly-Datei lautet "hello.s".

. Montage

Der Assembler (as.exe) konvertiert den Assembly-Code in der Objektdatei "hello.o" in Maschinencode.

als -o hallo.o hallo.s

4. Linker

Schließlich verknüpft der Linker (ld.exe) den Objektcode mit dem Bibliothekscode, um eine ausführbare Datei "hello.exe" zu erstellen.

ld -o hello.exe hello.o ... Bibliotheken ...

5
kaps