Ich versuche Dinge wie Linker und Lader besser zu verstehen.
Zu welchem Bereich der Informatik gehören sie? Compiler, Betriebssystem, Computerarchitektur?
Wo kommen Linker und Loader während der Entwicklung ins Spiel?
Ich versuche Dinge wie Linker und Lader besser zu verstehen.
Zu welchem Bereich der Informatik gehören sie? Compiler, Betriebssystem, Computerarchitektur?
Wo kommen Linker und Loader während der Entwicklung ins Spiel?
Antworten:
Die genaue Beziehung variiert etwas. Zunächst werde ich (fast) das einfachste Modell betrachten, das von etwas wie MS-DOS verwendet wird, bei dem eine ausführbare Datei immer statisch verknüpft ist. Betrachten wir zum Beispiel das kanonische "Hallo Welt!" Programm, von dem wir annehmen werden, dass es in C geschrieben ist.
Der Compiler wird dies in ein paar Teile kompilieren. Es wird das String-Literal "Hello, World!" Nehmen und es in einen Abschnitt einfügen, der als konstante Daten markiert ist, und es wird ein Name für diesen bestimmten String (z. B. "$ L1") synthetisiert. Der Aufruf wird printf
in einen anderen Abschnitt kompiliert , der als Code markiert ist. In diesem Fall heißt es main
(oder häufig _main
). Es gibt auch etwas zu sagen, dass dieser Codeabschnitt N Bytes lang ist und (wichtig) einen Aufruf von printf
Offset M in diesem Code enthält.
Sobald der Compiler damit fertig ist, wird der Linker ausgeführt. Es wird normalerweise als Teil der Entwicklungs-Toolkette betrachtet (obwohl es Ausnahmen gibt - MS-DOS enthielt früher einen Linker, obwohl es selten oder nie verwendet wurde). Obwohl es normalerweise nicht extern sichtbar ist, werden normalerweise einige Befehlszeilenargumente übergeben, von denen eines eine Objektdatei mit einem Startcode und ein anderes die Datei angibt, die die C-Standardbibliothek enthält.
Der Linker überprüft dann die Objektdatei, die den Startcode enthält, und stellt fest, dass sie beispielsweise 1112 Byte lang ist, und ruft dazu den _main
Offset 784 auf.
Auf dieser Grundlage wird eine Symboltabelle erstellt. Es wird einen Eintrag geben, der besagt, dass ".startup" (oder welcher Name auch immer) 1112 Bytes lang ist, und (bis jetzt) bezieht sich nichts auf diesen Namen. Es wird einen weiteren Eintrag geben, der besagt, dass "printf" eine derzeit unbekannte Länge ist, auf die jedoch von ".startup + 784" verwiesen wird.
Anschließend wird die angegebene Bibliothek (oder die angegebenen Bibliotheken) durchsucht, um zu versuchen, Definitionen der Namen in der Symboltabelle zu finden, die derzeit nicht definiert sind - in diesem Fall printf
. Es findet die Objektdatei für printf, die besagt, dass sie 4087 Bytes lang ist, und enthält Verweise auf andere Routinen, um beispielsweise ein Int in eine Zeichenfolge zu konvertieren, sowie Dinge wie putchar
(oder vielleicht fputc
), um die resultierende Zeichenfolge in die Ausgabe zu schreiben Datei.
Der Linker scannt erneut, um rekursiv nach Definitionen dieser Symbole zu suchen, bis er zu einer von zwei Schlussfolgerungen gelangt: Entweder werden Definitionen aller Symbole gefunden, oder es gibt ein Symbol, für das er keine Definition finden kann.
Wenn eine Referenz gefunden wurde, aber keine Definition, wird sie angehalten und eine Fehlermeldung ausgegeben, die normalerweise etwas über ein "undefiniertes externes XXX" aussagt, und es liegt an Ihnen, herauszufinden, welche andere Bibliothek oder Objektdatei Sie verknüpfen müssen .
Wenn es Definitionen aller Symbole findet, fährt es mit der nächsten Phase fort: Es geht durch die Liste der Stellen, die sich auf jedes Symbol beziehen, und gibt die Adresse ein, an der dieses Symbol gespeichert wurde (z. B.) ) Wenn der Startcode aufgerufen wird main
, wird die Adresse 1112
als Adresse von main eingegeben. Sobald dies alles erledigt ist, werden der gesamte Code und die Daten in eine ausführbare Datei geschrieben.
Es gibt noch ein paar andere kleine Details, die wahrscheinlich erwähnt werden müssen: In der Regel werden Code und Daten getrennt gehalten, und nach Abschluss werden alle an (mehr oder weniger) aufeinanderfolgenden Adressen (z. B. allen Teilen) zusammengefasst Code, dann alle Daten). In der Regel gibt es auch einige Regeln zum Kombinieren von Definitionen für Abschnitte / Segmente. Wenn beispielsweise verschiedene Objektdateien alle Codesegmente enthalten, werden die Codeteile einfach nacheinander angeordnet. Wenn zwei oder mehr identische Zeichenfolgenliterale (oder andere Konstanten) definiert sind, werden diese normalerweise zusammengeführt, sodass sich alle auf dieselbe Stelle beziehen. Es gibt auch einige Regeln, was zu tun ist, wenn doppelte Definitionen desselben Symbols gefunden werden. In einem typischen Fall ist dies einfach ein Fehler. In einigen Fällen wird es Dinge wie "Wenn jemand anderes es auch definiert, betrachten Sie es nicht als Fehler - verwenden Sie einfach diese Definition anstelle dieser.
Sobald es Einträge für alle Symbole hat, muss der Linker die "Teile" anordnen und ihnen Adressen zuweisen. Die Reihenfolge, in der die Teile angeordnet werden, variiert etwas - normalerweise gibt es einige Flags für die Arten der verschiedenen Teile, sodass (zum Beispiel) alle konstanten Daten nebeneinander und alle Codeteile nebeneinander landen einander und so weiter. In unserem einfachen MS-DOS-ähnlichen System spielt das meiste davon jedoch keine große Rolle.
Das bringt uns zur nächsten Phase: dem Lader. Der Loader ist normalerweise Teil des Betriebssystems, das die ausführbare Datei lädt. In alten Versionen (z. B. CP / M-, MS_DOS .com-Dateien) hat der Loader nur Daten aus einer ausführbaren Datei in den Speicher gelesen und dann an einer bestimmten Adresse ausgeführt. Etwas neuere Loader (z. B. für MS-DOS .exe-Dateien) werden dies tun Beginnen Sie (mehr oder weniger) auf die gleiche Weise: Lesen Sie eine Datei in den Speicher. In diesem Fall werden jedoch basierend auf den vom Linker dort eingegebenen Einträgen alle absoluten Verweise in der ausführbaren Datei "korrigiert", um auf die zu verweisen Richtige Adresse. Im obigen Beispiel wurde auf unseren Startcode verwiesenmain
an der Adresse 1112, aber die ausführbare Datei wird an einer Basisadresse von (sagen wir) 4000 geladen. In diesem Fall wird der Loader diese Adresse so einstellen, dass sie auf 5112 verweist. In diesem einfachen System ist der Loader jedoch immer noch ein ziemlich einfacher Code - im Grunde nur durch die Liste der Umzüge gehen und jedem die Basisadresse hinzufügen.
Betrachten wir nun ein etwas moderneres Betriebssystem, das so etwas wie gemeinsam genutzte Objektdateien oder DLLs unterstützt. Dadurch wird ein Teil der Arbeit vom Linker auf den Loader verlagert. Insbesondere für ein Symbol, das in einer .so / DLL definiert ist, versucht der Linker nicht , selbst eine Adresse zuzuweisen.
Stattdessen wird ein Symboltabelleneintrag erstellt, der im Wesentlichen "definiert in .so / DLL-Datei XXX" lautet. Wenn der Linker die ausführbare Datei schreibt, werden die meisten dieser Symboltabelleneinträge im Grunde genommen nur in die ausführbare Datei kopiert und sagen "Symbol XXX ist in der Datei JJJ definiert". Es ist dann Sache des Laders, die Datei JJJ und die Adresse des Symbols XXX in dieser Datei zu finden und die richtige Adresse einzugeben, wo immer sie in der ausführbaren Datei verwendet wird. Ähnlich wie im Linker ist dies rekursiv, sodass sich DLL A möglicherweise auf Symbole in DLL B bezieht, die sich auf DLL C beziehen können, und so weiter. Obwohl die Kette von der ausführbaren Datei zu allen Definitionen lang sein kann, ist die Grundidee des Prozesses ziemlich einfach: Durchsuchen Sie die Liste der externen Referenzen und finden Sie für jede eine Definition. Beachten Sie auch, dass in den meisten Fällen
Auch hier sind einige verschiedene Dinge zu beachten. Beispielsweise erfolgt die Freigabe normalerweise nur abschnittsweise, nicht dateiweise. Wenn eine Datei beispielsweise Code und einige (nicht konstante) Daten enthält, verwenden alle Prozesse dieselben Codeabschnitte, aber jeder erhält eine eigene Kopie der Daten.
Um mehr über Linker zu erfahren, werden sie im Allgemeinen in Kombination mit Compilern diskutiert. Sie dienen dazu, Ihre verschiedenen Module zu einer zusammenhängenden Einheit zusammenzufügen und die Adressen innerhalb dieses Codes zu finalisieren. Einige versuchen möglicherweise sogar, Optimierungen durchzuführen.
Um mehr über Loader zu erfahren, werden sie im Allgemeinen in Kombination mit dem Schreiben von Compilern für bestimmte Architekturen diskutiert, es sei denn, Sie meinen Loader als Synonym für Linker. Ich stelle mir den Loader als Teil des Headers der ausführbaren Datei vor, der dem Betriebssystem mitteilt, wie Ihre kompilierte Software geöffnet und ausgeführt werden soll.
Ich bin damit einverstanden, dass das Lesen der Wikipedia-Artikel wahrscheinlich mehr Informationen liefert, als Sie suchen. Wo sie in die Entwicklung kommen ... im Allgemeinen liegen sie außerhalb der Kontrolle des Projekts und sind Teil der Auswahl des Betriebssystems und des Entwicklungspakets, das Sie verwenden möchten. Es ist sehr selten, dass Sie (zum Beispiel) MSVC verwenden, aber einen GCC-basierten Linker ausführen möchten ... möglicherweise nicht einmal möglich. Der EINZIGE Ort, an dem ich jemals einen nicht standardmäßigen Linker verwendet habe, war bei IBM, als wir Entwicklungskopien verwendeten.
Wenn Sie spezifischere, spezifische Fragen zu diesen Themen haben, werden Sie wahrscheinlich eine viel bessere Antwort finden.
Linker und Lader sind zwei verwandte, aber getrennte Konzepte.
Linker sind Teil der Compilertheorie. Wenn Sie ein Projekt kompilieren, das aus mehr als einem Modul (Quellcodedatei) besteht, gibt der Compiler häufig eine einzelne Zwischendatei für jedes Quellmodul aus. Dies hat mehrere Vorteile. Einer davon ist, dass Sie nicht das gesamte Projekt neu erstellen müssen, wenn Sie nur eine lokale Änderung vorgenommen haben, wenn Sie nur Änderungen an einer Datei vornehmen und diese dann neu kompilieren müssen.
Dies bedeutet jedoch, dass der Compiler, wenn Sie Code in einem Modul haben, der eine Funktion in einem anderen Modul aufruft, keine CALL
Anweisung dazu generieren kann, da er nicht die Position dieser anderen Funktion hat. Es befindet sich in einer anderen Zwischendatei, und der genaue Speicherort der Funktion kann sich ändern, wenn Sie die Quelldatei des Vermittlers lokal ändern und neu kompilieren. Stattdessen wird ein "externes Referenz-Token" eingefügt (genau das, was das ist oder wie es aussieht, spielt keine Rolle, stellen Sie es sich einfach als abstraktes Konzept vor), das besagt: "Ich brauche diese Funktion, deren genaue Adresse ich nicht kenne in dem Augenblick."
Sobald alles in Zwischendateien kompiliert wurde, beendet der Linker den Job. Es durchläuft alle Zwischendateien und verknüpft sie zu einer endgültigen Binärdatei. Da es Dinge zusammenfügt, kennt es die tatsächlichen Adressen aller Funktionen und kann so die externen Referenz-Token durch tatsächliche CALL
Anweisungen an den richtigen Stellen in der Binärdatei ersetzen .
Der Loader hingegen gehört zum Betriebssystem, nicht zum Compiler. Seine Aufgabe besteht darin, die Binärdatei in den Speicher zu laden, damit sie ausgeführt werden kann, und den Verknüpfungsprozess abzuschließen, da der Linker nur den ihm bekannten Code auflösen kann. Wenn Ihr Programm DLLs verwendet, befinden sich diese sogar außerhalb der kompilierten Binärdatei, sodass der Linker ihre Adresse nicht kennt. Es hinterlässt externe Referenztoken in der endgültigen Binärdatei in einem Format, das der Loader des Betriebssystems kennt, und dann geht der Loader durch und ordnet diese Token den tatsächlichen Funktionsadressen in den DLLs zu, sobald alles in den Speicher geladen wurde.
Computer arbeiten grundsätzlich mit Binärzahlen.
Die Menschen sprechen ihre Muttersprache.
Programmiersprachen dienen der Kommunikation zwischen Menschen und Computern.
Wenn Sie sagen: Addieren Sie 2 und 3 und subtrahieren Sie dann 1 davon. Ich bezweifle, dass der Computer etwas verstehen würde (möglicherweise in einer Programmiersprache).
Sie müssen also Ihren Quellcode in ein Format übersetzen, das der Computer versteht. Daher benötigen Sie einen Compiler, der eine Programmiersprache in den sogenannten Objektcode übersetzt. Objektcode ist jedoch noch nicht die Sprache, die ein Computer versteht und direkt ausführt. Es braucht also einen Linker, der eine ausführbare Datei erstellt, die Anweisungen in der sogenannten Maschinensprache enthält. Eine Maschinensprache ist eine Reihe von Operationen, die in Binärzahlen codiert sind, die der Prozessor versteht. Alle binären Anweisungen haben ihre Struktur und werden von einem Prozessorhersteller veröffentlicht. Sie können auf der Intel-Website danach suchen und sehen, wie sie aussehen.