521 Stimmen

Wie funktioniert der Kompilierungs-/Verknüpfungsprozess?

Wie funktioniert der Kompilierungs- und Verknüpfungsprozess?

(Hinweis: Dies ist ein Eintrag zu <a href="https://stackoverflow.com/questions/tagged/c++-faq">Stack Overflow's C++ FAQ </a>. Wenn Sie die Idee, eine FAQ in dieser Form anzubieten, kritisieren wollen, dann <a href="https://meta.stackexchange.com/questions/68647/setting-up-a-faq-for-the-c-tag">das Posting auf Meta, mit dem alles begann </a>wäre der richtige Ort, um dies zu tun. Die Antworten auf diese Frage werden in der <a href="https://chat.stackoverflow.com/rooms/10/c-lounge">C++ Chatraum </a>, wo die Idee für die FAQ entstand, so dass Ihre Antwort mit großer Wahrscheinlichkeit von denjenigen gelesen wird, die die Idee hatten).

693voto

R. Martinho Fernandes Punkte 217895

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

  1. Vorverarbeitung: Der Präprozessor nimmt eine C++-Quellcodedatei und bearbeitet die #include s, #define s und andere Präprozessoranweisungen. Die Ausgabe dieses Schrittes ist eine "reine" C++-Datei ohne Präprozessoranweisungen.

  2. Kompilierung: Der Compiler nimmt die Ausgabe des Präprozessors und erstellt daraus eine Objektdatei.

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

Vorverarbeitung

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

Er arbeitet mit jeweils einer C++-Quelldatei, indem er die #include Direktiven mit dem Inhalt der jeweiligen Dateien (der in der Regel nur aus Deklarationen besteht), die Ersetzung von Makros ( #define ), und die Auswahl verschiedener Textabschnitte in Abhängigkeit von #if , #ifdef y #ifndef Richtlinien.

Der Präprozessor arbeitet mit einem Strom von Vorverarbeitungs-Token. Makrosubstitution ist definiert als das Ersetzen von Token durch andere Token (der Operator ## ermöglicht das Zusammenführen zweier Token, wenn dies sinnvoll ist).

Nach all dem erzeugt der Präprozessor eine einzige Ausgabe, die ein Strom von Token ist, die aus den oben beschriebenen Transformationen resultieren. Er fügt auch einige spezielle Markierungen hinzu, die dem Compiler mitteilen, woher jede Zeile stammt, so dass er diese verwenden kann, um sinnvolle Fehlermeldungen zu erzeugen.

Einige Fehler können in diesem Stadium durch geschickte Verwendung der #if y #error Richtlinien.

Zusammenstellung

Der Kompilierungsschritt wird für jede Ausgabe des Präprozessors durchgeführt. Der Compiler analysiert den reinen C++-Quellcode (jetzt ohne Präprozessoranweisungen) und wandelt ihn in Assemblercode um. Dann ruft er das zugrundeliegende Back-End auf (Assembler in der Toolchain), das diesen Code in Maschinencode umwandelt und eine Binärdatei in einem bestimmten Format (ELF, COFF, a.out, ...) erzeugt. Diese Objektdatei enthält den kompilierten Code (in binärer Form) für die in der Eingabe definierten Symbole. Auf Symbole in Objektdateien wird mit Namen verwiesen.

Objektdateien können auf Symbole verweisen, die nicht definiert sind. Dies ist der Fall, wenn Sie eine Deklaration verwenden und keine Definition dafür angeben. Den Compiler stört dies nicht und er erzeugt die Objektdatei gerne, solange der Quellcode wohlgeformt ist.

Compiler lassen Sie die Kompilierung in der Regel an dieser Stelle abbrechen. Dies ist sehr nützlich, weil Sie damit jede Quellcodedatei einzeln kompilieren können. Dies hat den Vorteil, dass Sie nicht neu kompilieren müssen alles wenn Sie nur eine einzige Datei ändern.

Die erzeugten 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 Fehler bei der Auflösung von Überladungen gemeldet.

Verlinkung

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 obwohl der Name ähnlich ist, haben sie nicht viel mit den zuvor erwähnten statischen Bibliotheken gemeinsam) oder eine ausführbare Datei.

Es verknüpft alle Objektdateien, indem es die Verweise auf undefinierte Symbole durch die richtigen Adressen ersetzt. Jedes dieser Symbole kann in anderen Objektdateien oder in Bibliotheken definiert sein. Wenn sie in anderen Bibliotheken als der Standardbibliothek definiert sind, müssen Sie sie dem Linker mitteilen.

In diesem Stadium sind die häufigsten Fehler fehlende oder doppelte Definitionen. Ersteres bedeutet, dass die Definitionen entweder nicht existieren (d.h. nicht geschrieben sind) oder dass die Objektdateien oder Bibliotheken, in denen sie sich befinden, dem Linker nicht übergeben wurden. Letzteres ist offensichtlich: Das gleiche Symbol wurde in zwei verschiedenen Objektdateien oder Bibliotheken definiert.

50 Stimmen

In der Kompilierungsphase wird vor der Umwandlung in eine Objektdatei auch der Assembler aufgerufen.

5 Stimmen

Wo werden Optimierungen vorgenommen? Auf den ersten Blick sieht es so aus, als ob dies bei der Kompilierung geschehen würde, aber andererseits kann ich mir vorstellen, dass die Optimierung erst nach dem Linken erfolgen kann.

9 Stimmen

@BartvanHeukelom Traditionell wurde dies während der Kompilierung durchgeführt, aber moderne Compiler unterstützen die so genannte "Link-Time-Optimierung", die den Vorteil hat, dass sie über Übersetzungseinheiten hinweg optimiert werden kann.

60voto

user2003323 Punkte 1

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

Hier ist, was der Autor dort schrieb:

Kompilieren ist nicht ganz dasselbe wie das Erstellen einer ausführbaren Datei! Stattdessen ist die Erstellung einer ausführbaren Datei ein mehrstufiger Prozess, der in zwei Komponenten unterteilt: Kompilierung und Verknüpfung. In Wirklichkeit kann ein Programm, auch wenn es "gut kompiliert" wird, kann es aufgrund von Fehlern in der der Linking-Phase. Der gesamte Prozess, der von den Quellcodedateien zu einer ausführbaren Datei könnte man besser als Build bezeichnen.

Zusammenstellung

Die Kompilierung bezieht sich auf die Verarbeitung von Quellcode-Dateien (.c, .cc, oder .cpp) und die Erstellung einer "Objekt"-Datei. Dieser Schritt erzeugt keine etwas, das der Benutzer tatsächlich ausführen kann. Stattdessen erzeugt der Compiler lediglich lediglich die Anweisungen in Maschinensprache, die der Quellcodedatei entsprechen, die kompiliert wurde. Wenn Sie zum Beispiel drei separate Dateien kompilieren (aber drei separate Dateien kompilieren (aber nicht linken), erhalten Sie drei Objektdateien als Ausgabe erstellt, jede mit dem Namen .o oder .obj (die Erweiterung hängt von Ihrem Compiler ab). Jede dieser Dateien enthält eine Übersetzung Ihrer Quellcodedatei in eine Maschinensprache Maschinensprachendatei - aber Sie können sie noch nicht ausführen! Sie müssen sie umwandeln in ausführbare Dateien verwandeln, die Ihr Betriebssystem verwenden kann. Das ist die Aufgabe des Linker ins Spiel.

Verlinkung

Linking bezieht sich auf die Erstellung einer einzelnen ausführbaren Datei aus mehreren Objektdateien. Bei diesem Schritt ist es üblich, dass der Linker über undefinierte Funktionen (in der Regel main selbst) beschwert. Während der Kompilierung, wenn der Compiler die Definition für eine Funktion nicht finden konnte bestimmte Funktion nicht finden konnte, ging er einfach davon aus, dass die Funktion in einer anderen Datei definiert ist. Wenn dies nicht der Fall ist, gibt es keine Möglichkeit für den Compiler keine Möglichkeit, dies herauszufinden, da er nicht den Inhalt von mehr als einer einer Datei zur gleichen Zeit. Der Linker hingegen kann sich mehrere Dateien ansehen mehrere Dateien und versucht, Verweise für die Funktionen zu finden, die nicht erwähnt wurden.

Sie fragen sich vielleicht, warum es getrennte Schritte für die Kompilierung und die Verknüpfung gibt. Erstens ist es wahrscheinlich einfacher, die Dinge auf diese Weise zu implementieren. Der Compiler macht sein Ding, und der Linker macht sein Ding - indem man die Funktionen getrennt bleiben, wird die Komplexität des Programms reduziert. Ein weiterer (offensichtlicherer) Vorteil ist, dass dies die Erstellung von großen Programme zu erstellen, ohne den Kompilierungsschritt jedes Mal wiederholen zu müssen, wenn eine Datei geändert wird. Stattdessen ist es bei der so genannten "bedingten Kompilierung" nur die Quelldateien kompiliert werden, die sich geändert haben; für den Rest reichen die Objektdateien als Input für den Linker aus. Schließlich ist es auf diese Weise einfach, Bibliotheken mit vorkompiliertem Code zu implementieren: Erstellen Sie einfach Objektdateien und binden Sie sie wie jede andere Objektdatei. (Die Tatsache, dass jede Datei separat kompiliert wird von den in anderen Dateien enthaltenen Informationen kompiliert wird, nennt man übrigens die "separates Kompilierungsmodell" genannt.)

Um die Vorteile der Bedingungserstellung voll auszuschöpfen, ist es wahrscheinlich einfacher, sich von einem Programm helfen zu lassen, als zu versuchen, sich zu merken, welche Dateien Sie seit der letzten Kompilierung geändert haben. (Sie könnten natürlich, einfach jede Datei neu kompilieren, deren Zeitstempel größer ist als der Zeitstempel der entsprechenden Objektdatei ist.) Wenn Sie mit einer integrierten Entwicklungsumgebung (IDE) arbeiten, kümmert sich diese vielleicht schon dies für Sie. Wenn Sie Kommandozeilentools verwenden, gibt es ein schickes Dienstprogramm namens make, das mit den meisten *nix-Distributionen geliefert wird. Zusammen mit hat es neben der bedingten Kompilierung einige andere nette Funktionen für die Programmierung, wie z.B. das Zulassen verschiedener Kompilierungen Ihres Programms -- zum Beispiel, wenn Sie eine Version haben, die eine ausführliche Ausgabe zum Debuggen erzeugt.

Den Unterschied zwischen der Kompilierungsphase und der Verknüpfung kennen Phase zu kennen, kann die Suche nach Fehlern erleichtern. Compiler-Fehler sind normalerweise syntaktischer Natur - ein fehlendes Semikolon, eine zusätzliche Klammer. Linking-Fehler haben in der Regel mit fehlenden oder mehrfachen Definitionen zu tun. Wenn Sie eine Fehlermeldung erhalten, dass eine Funktion oder Variable mehrfach definiert ist, ist das ein guter Hinweis darauf, dass dass der Fehler darin besteht, dass zwei Ihrer Quellcodedateien die gleiche Funktion oder Variable enthalten.

2 Stimmen

Was ich nicht verstehe ist, dass wenn der Präprozessor Dinge wie #includes verwaltet, um eine Superdatei zu erstellen, dann surly gibt es nichts zu verknüpfen, nachdem das?

0 Stimmen

@binarysmacer Schauen Sie, ob das, was ich unten geschrieben habe, für Sie einen Sinn ergibt. Ich habe versucht, das Problem von innen nach außen zu beschreiben.

8 Stimmen

@binarysmacker Für einen Kommentar ist es zu spät, aber vielleicht ist das für andere nützlich. youtu.be/D0TazQIkc8Q Grundsätzlich schließen Sie Headerdateien ein, und diese Headerdateien enthalten im Allgemeinen nur die Deklarationen von Variablen/Funktionen und nicht deren Definitionen, die Definitionen könnten in einer separaten Quelldatei vorhanden sein, so dass der Präprozessor nur die Deklarationen und nicht die Definitionen einschließt.

60voto

kaps Punkte 187

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

Zum Beispiel, gcc -o hello hello.c wird wie folgt durchgeführt:

1. Vorverarbeitung

Vorverarbeitung mit dem GNU C Preprocessor ( cpp.exe ), die Folgendes umfasst die Kopfzeilen ( #include ) und erweitert die Makros ( #define ).

cpp hello.c > hello.i

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

2. Zusammenstellung

Der Compiler kompiliert den vorverarbeiteten Quellcode in Assemblercode für einen bestimmten Prozessor.

gcc -S hello.i

Die Option -S legt fest, dass Assemblercode anstelle von Objektcode erzeugt werden soll. Die resultierende Assemblerdatei ist "hello.s".

3. Montage

Der Assembler ( as.exe ) wandelt den Assemblercode in Maschinencode in der Objektdatei "hello.o" um.

as -o hello.o hello.s

4. Linker

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

    ld -o hello hello.o _...libraries..._

0 Stimmen

Ld: warning: cannot find entry symbol main; defaulting to 0000000000400040 - Fehler bei der Verwendung von ld. Mein Code ist ein Helloworld. Der Prozess ist in Ubuntu getan.

27voto

AProgrammer Punkte 49452

An der Standardfront:

  • a Übersetzungseinheit ist die Kombination aus Quelldateien, eingeschlossenen Kopfzeilen und Quelldateien abzüglich der durch die Präprozessoranweisung "Conditional Inclusion" übersprungenen Quellzeilen.

  • Die Norm definiert 9 Phasen der Übersetzung. Die ersten vier entsprechen der Vorverarbeitung, die nächsten drei der Kompilierung, die nächste der Instanziierung von Vorlagen (Erzeugung von Instanziierungseinheiten ) 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 verschieben sie auf die Linking-Phase und einige verteilen sie auf beide Phasen.

15 Stimmen

Können Sie alle 9 Phasen auflisten? Das wäre eine schöne Ergänzung zur Antwort, denke ich :)

0 Stimmen

0 Stimmen

@jalf, fügen Sie einfach die Template-Instanziierung kurz vor der letzten Phase in der von @sbi angegebenen Antwort ein. IIRC gibt es subtile Unterschiede im genauen Wortlaut bei der Behandlung von breiten Zeichen, aber ich glaube nicht, dass sie in den Diagrammbeschriftungen auftauchen.

26voto

Elliptical view Punkte 2740

Eine CPU lädt Daten aus Speicheradressen, speichert Daten in Speicheradressen und führt Befehle sequentiell aus Speicheradressen aus, mit einigen bedingten Sprüngen in der Reihenfolge der verarbeiteten Befehle. Jede dieser drei Befehlskategorien beinhaltet die Berechnung einer Adresse für eine Speicherzelle, die in dem Maschinenbefehl verwendet wird. Da die Länge der Maschinenbefehle je nach dem jeweiligen Befehl variabel ist und wir bei der Erstellung unseres Maschinencodes eine variable Länge aneinanderreihen, gibt es einen zweistufigen Prozess bei der Berechnung und Erstellung der Adressen.

Zunächst legen wir die Speicherzuweisung so gut wie möglich fest, bevor wir wissen, was genau in jede Zelle kommt. Wir legen die Bytes oder Wörter oder was auch immer fest, die die Anweisungen und Literale und alle Daten bilden. Wir beginnen einfach damit, Speicher zuzuweisen und die Werte zu bilden, die das Programm erstellen werden, und notieren uns jede Stelle, an der wir zurückgehen und eine Adresse korrigieren müssen. An dieser Stelle setzen wir einen Dummy ein, um die Stelle aufzufüllen, damit wir die Speichergröße weiter berechnen können. Unser erster Maschinencode könnte zum Beispiel eine Zelle benötigen. Der nächste Maschinencode könnte 3 Zellen beanspruchen, was eine Maschinencodezelle und zwei Adresszellen beinhaltet. Jetzt ist unser Adresszeiger 4. Wir wissen, was in die Maschinenzelle kommt, nämlich der Operationscode, aber wir müssen mit der Berechnung dessen, was in die Adresszellen kommt, warten, bis wir wissen, wo sich diese Daten befinden werden, d.h. was die Maschinenadresse dieser Daten sein wird.

Wenn es nur eine Quelldatei gäbe, könnte ein Compiler theoretisch ohne Linker vollständig ausführbaren Maschinencode erzeugen. In einem zweistufigen Prozess könnte er alle tatsächlichen Adressen aller Datenzellen berechnen, auf die sich die Lade- oder Speicheranweisungen der Maschine beziehen. Und er könnte alle absoluten Adressen berechnen, die von absoluten Sprunganweisungen referenziert werden. So arbeiten einfachere Compiler, wie der in Forth, ohne Linker.

Ein Linker ist etwas, das es ermöglicht, Codeblöcke separat zu kompilieren. Dies kann den gesamten Prozess der Codeerstellung beschleunigen und ermöglicht eine gewisse Flexibilität bei der späteren Verwendung der Blöcke, d. h. sie können im Speicher verschoben werden, indem z. B. 1000 zu jeder Adresse hinzugefügt wird, um den Block um 1000 Adresszellen nach oben zu verschieben.

Was der Compiler ausgibt, ist also ein grober Maschinencode, der noch nicht vollständig aufgebaut ist, aber so angelegt ist, dass wir die Größe von allem kennen, mit anderen Worten, dass wir damit beginnen können, zu berechnen, wo sich alle absoluten Adressen befinden werden. Der Compiler gibt auch eine Liste von Symbolen aus, bei denen es sich um Namen/Adresspaare handelt. Die Symbole verknüpfen einen Speicheroffset im Maschinencode des Moduls mit einem Namen. Der Offset ist der absolute Abstand zum Speicherplatz des Symbols im Modul.

Das ist der Punkt, an dem wir zum Linker kommen. Der Linker fügt zunächst alle diese Blöcke des Maschinencodes aneinander und notiert, wo jeder einzelne beginnt. Dann errechnet er die zu fixierenden Adressen, indem er den relativen Offset innerhalb eines Moduls und die absolute Position des Moduls im größeren Layout addiert.

Ich habe dies natürlich stark vereinfacht, damit Sie versuchen können, es zu verstehen, und ich habe absichtlich nicht den Jargon der Objektdateien, Symboltabellen usw. verwendet, der für mich Teil der Verwirrung ist.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X