c++ - titel - title tag länge




Wie funktioniert der Kompilierungs-/Verknüpfungsprozess? (4)

Wie funktioniert der Kompilierungs- und Verknüpfungsprozess?

(Hinweis: Dies ist ein Eintrag in die C ++ - FAQ von Stack Overflow . 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.)


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.


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

Hier ist, was der Autor schrieb:

Das Kompilieren ist nicht das Gleiche wie das Erstellen einer ausführbaren Datei! 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 kann es sogar dann, wenn ein Programm "gut kompiliert" wird, aufgrund von Fehlern während der Verknüpfungsphase nicht wirklich funktionieren. Der gesamte Prozess, von Quellcodedateien zu einer ausführbaren Datei zu wechseln, wird besser als Build bezeichnet.

Zusammenstellung

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 erzeugt der Compiler lediglich die Maschinensprachanweisungen, die der Quellcodedatei entsprechen, die kompiliert wurde. 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 umwandeln, die Ihr Betriebssystem verwenden kann. Hier kommt der Linker ins Spiel.

Verknüpfung

Die Verknüpfung bezieht sich auf die Erstellung einer einzelnen ausführbaren Datei aus mehreren Objektdateien. In diesem Schritt ist es üblich, dass der Linker über undefinierte Funktionen (allgemein Main selbst) klagen wird. Wenn der Compiler während der Kompilierung die Definition für eine bestimmte Funktion nicht finden konnte, nahm er einfach an, dass die Funktion in einer anderen Datei definiert wurde. Wenn dies nicht der Fall ist, gibt es keine Möglichkeit, dass der Compiler es weiß - es schaut nicht auf den Inhalt von mehr als einer Datei gleichzeitig. Der Linker kann andererseits mehrere Dateien betrachten und versuchen, Referenzen für die Funktionen zu finden, die nicht erwähnt wurden.

Sie könnten fragen, warum es separate Kompilierungs- und Verknüpfungsschritte gibt. Erstens ist es wahrscheinlich einfacher, die Dinge so zu implementieren. Der Compiler macht seine Sache, und der Linker macht seine Sache - indem er die Funktionen getrennt hält, wird die Komplexität des Programms reduziert. Ein anderer (offensichtlicherer) Vorteil besteht darin, dass dies das Erstellen großer Programme ermöglicht, ohne dass der Kompilierungsschritt jedes Mal wiederholt werden muss, wenn eine Datei geändert wird. Bei der so genannten "bedingten Kompilierung" müssen stattdessen nur die Quelldateien kompiliert werden, die sich geändert haben. Im übrigen sind die Objektdateien ausreichend für den Linker. 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 die Vorteile der Zustandskompilierung vollständig nutzen zu können, ist es wahrscheinlich einfacher, ein Programm zu bekommen, das Ihnen hilft, als zu versuchen, sich daran zu erinnern, welche Dateien Sie seit der letzten Kompilierung geändert haben. (Sie könnten natürlich nur jede Datei neu kompilieren, deren Timestamp größer als der Timestamp der entsprechenden Objektdatei ist.) Wenn Sie mit einer integrierten Entwicklungsumgebung (IDE) arbeiten, kann dies bereits für Sie erledigt sein. Wenn Sie Befehlszeilen-Tools verwenden, gibt es ein raffiniertes Dienstprogramm namens make, das mit den meisten * nix-Distributionen geliefert wird. Neben der bedingten Kompilierung gibt es noch weitere nützliche Funktionen zum Programmieren, z. B. verschiedene Kompilierungen Ihres Programms - zum Beispiel, wenn Sie eine Version haben, die eine ausführliche Ausgabe zum Debuggen erzeugt.

Wenn Sie den Unterschied zwischen der Kompilierungs- 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 meist mit fehlenden oder mehreren Definitionen zu tun. Wenn Sie eine Fehlermeldung erhalten, dass eine Funktion oder Variable mehrfach vom Linker definiert wurde, ist dies ein guter Hinweis darauf, dass der Fehler darin besteht, dass zwei Ihrer Quellcodedateien die gleiche Funktion oder Variable haben.






c++-faq