Um Linker zu verstehen, ist es hilfreich, zunächst zu verstehen, was "unter der Haube" passiert, wenn Sie eine Quelldatei (z. B. eine C- oder C ++ - Datei) in eine ausführbare Datei konvertieren (eine ausführbare Datei ist eine Datei, die auf Ihrem Computer oder ausgeführt werden kann) die Maschine einer anderen Person, auf der dieselbe Maschinenarchitektur ausgeführt wird).
Unter der Haube konvertiert der Compiler beim Kompilieren eines Programms die Quelldatei in Objektbytecode. Dieser Bytecode (manchmal auch als Objektcode bezeichnet) ist eine mnemonische Anweisung, die nur Ihre Computerarchitektur versteht. Traditionell haben diese Dateien die Erweiterung .OBJ.
Nachdem die Objektdatei erstellt wurde, kommt der Linker ins Spiel. Meistens muss ein echtes Programm, das irgendetwas Nützliches tut, auf andere Dateien verweisen. In C besteht ein einfaches Programm zum Drucken Ihres Namens auf dem Bildschirm beispielsweise aus:
printf("Hello Kristina!\n");
Wenn der Compiler Ihr Programm in eine obj-Datei kompiliert hat, verweist er einfach auf die printf
Funktion. Der Linker löst diese Referenz auf. Die meisten Programmiersprachen verfügen über eine Standardbibliothek von Routinen, um die grundlegenden Dinge abzudecken, die von dieser Sprache erwartet werden. Der Linker verknüpft Ihre OBJ-Datei mit dieser Standardbibliothek. Der Linker kann Ihre OBJ-Datei auch mit anderen OBJ-Dateien verknüpfen. Sie können andere OBJ-Dateien mit Funktionen erstellen, die von einer anderen OBJ-Datei aufgerufen werden können. Der Linker funktioniert fast wie das Kopieren und Einfügen eines Textverarbeitungsprogramms. Es "kopiert" alle notwendigen Funktionen, auf die Ihr Programm verweist, und erstellt eine einzige ausführbare Datei. Manchmal sind andere Bibliotheken, die kopiert werden, von anderen OBJ- oder Bibliotheksdateien abhängig. Manchmal muss ein Linker ziemlich rekursiv werden, um seine Arbeit zu erledigen.
Beachten Sie, dass nicht alle Betriebssysteme eine einzige ausführbare Datei erstellen. Windows verwendet beispielsweise DLLs, die alle diese Funktionen in einer einzigen Datei zusammenhalten. Dies reduziert die Größe Ihrer ausführbaren Datei, macht Ihre ausführbare Datei jedoch von diesen spezifischen DLLs abhängig. DOS verwendete früher Overlays (.OVL-Dateien). Dies hatte viele Zwecke, aber einer bestand darin, häufig verwendete Funktionen in einer Datei zusammenzuhalten (ein weiterer Zweck, falls Sie sich fragen, bestand darin, große Programme in den Speicher einpassen zu können. DOS hat eine Speicherbeschränkung und Überlagerungen können aus dem Speicher "entladen" werden und andere Überlagerungen könnten über diesen Speicher "geladen" werden, daher der Name "Überlagerungen"). Linux hat gemeinsam genutzte Bibliotheken, was im Grunde die gleiche Idee wie DLLs ist (Hardcore-Linux-Leute, die ich kenne, würden mir sagen, dass es VIELE GROSSE Unterschiede gibt).
Hoffe das hilft dir zu verstehen!
Minimales Beispiel für Adressverlagerung
Die Adressverlagerung ist eine der entscheidenden Funktionen der Verknüpfung.
Schauen wir uns also anhand eines minimalen Beispiels an, wie es funktioniert.
0) Einleitung
Zusammenfassung: Beim Verschieben wird der
.text
Abschnitt der zu übersetzenden Objektdateien bearbeitet :Dies muss vom Linker durchgeführt werden, da der Compiler jeweils nur eine Eingabedatei sieht. Wir müssen jedoch alle Objektdateien gleichzeitig kennen, um entscheiden zu können, wie:
.text
und.data
Abschnitte mehrerer Objektdateien kollidierenVoraussetzungen: minimales Verständnis von:
Das Verknüpfen hat nichts mit C oder C ++ zu tun: Compiler generieren nur die Objektdateien. Der Linker nimmt sie dann als Eingabe, ohne jemals zu wissen, welche Sprache sie zusammengestellt hat. Es könnte genauso gut Fortran sein.
Um die Kruste zu verringern, untersuchen wir eine Hallo-Welt für NASM x86-64 ELF Linux:
zusammengestellt und zusammengestellt mit:
mit NASM 2.10.09.
1) .text von .o
Zuerst dekompilieren wir den
.text
Abschnitt der Objektdatei:was gibt:
Die entscheidenden Zeilen sind:
Dadurch sollte die Adresse des Hallo-Welt-Strings in das
rsi
Register verschoben werden , das an den Schreibsystemaufruf übergeben wird.Aber warte! Wie kann der Compiler möglicherweise wissen, wo
"Hello world!"
er beim Laden des Programms im Speicher landet?Nun, es kann nicht, besonders nachdem wir eine Reihe von
.o
Dateien mit mehreren.data
Abschnitten verknüpft haben .Dies kann nur der Linker tun, da nur er alle diese Objektdateien hat.
Also der Compiler einfach:
0x0
auf die kompilierte AusgabeDiese "zusätzlichen Informationen" sind im
.rela.text
Abschnitt der Objektdatei enthalten2) .rela.text
.rela.text
steht für "Verlagerung des .text-Abschnitts".Das Wort Relocation wird verwendet, da der Linker die Adresse vom Objekt in die ausführbare Datei verschieben muss.
Wir können den
.rela.text
Abschnitt zerlegen mit:was beinhaltet;
Das Format dieses Abschnitts ist fest dokumentiert unter: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Jeder Eintrag teilt dem Linker eine Adresse mit, die verschoben werden muss. Hier haben wir nur eine für die Zeichenfolge.
Um es ein wenig zu vereinfachen, für diese bestimmte Zeile haben wir die folgenden Informationen:
Offset = C
: Was ist das erste Byte von, das.text
dieser Eintrag ändert.Wenn wir auf den dekompilierten Text zurückblicken, befindet er sich genau im kritischen Bereich
movabs $0x0,%rsi
, und diejenigen, die die x86-64-Befehlskodierung kennen, werden feststellen, dass dies den 64-Bit-Adressenteil des Befehls codiert.Name = .data
: Die Adresse zeigt auf den.data
AbschnittType = R_X86_64_64
, die genau angibt, welche Berechnung durchgeführt werden muss, um die Adresse zu übersetzen.Dieses Feld ist tatsächlich prozessorabhängig und daher in der AMD64 System V ABI-Erweiterung Abschnitt 4.4 "Relocation" dokumentiert .
In diesem Dokument heißt
R_X86_64_64
es:Field = word64
: 8 Bytes, also die00 00 00 00 00 00 00 00
at-Adresse0xC
Calculation = S + A
S
ist also der Wert an der Adresse, die verschoben wird00 00 00 00 00 00 00 00
A
ist der Zusatz, der0
hier ist. Dies ist ein Feld des Umzugseintrags.Also
S + A == 0
und wir werden an die allererste Adresse des.data
Abschnitts verlegt.3) .text von .out
Schauen
ld
wir uns nun den Textbereich der für uns generierten ausführbaren Datei an:gibt:
Das einzige, was sich gegenüber der Objektdatei geändert hat, sind die kritischen Zeilen:
die jetzt auf die Adresse
0x6000d8
(d8 00 60 00 00 00 00 00
in Little-Endian) anstelle von zeigen0x0
.Ist dies der richtige Ort für die
hello_world
Zeichenfolge?Um zu entscheiden, müssen wir die Programm-Header überprüfen, die Linux mitteilen, wo die einzelnen Abschnitte geladen werden sollen.
Wir zerlegen sie mit:
was gibt:
Dies sagt uns, dass der
.data
Abschnitt, der der zweite ist, beiVirtAddr
= beginnt0x06000d8
.Und das einzige, was im Datenbereich steht, ist unser Hallo-Welt-String.
Bonuslevel
PIE
Verknüpfen: Was ist die Option -fPIE für positionsunabhängige ausführbare Dateien in gcc und ld?quelle
In Sprachen wie 'C' werden einzelne Codemodule traditionell separat zu Blobs von Objektcode kompiliert, die in jeder Hinsicht ausgeführt werden können, außer dass alle Verweise, die das Modul außerhalb von sich selbst macht (dh auf Bibliotheken oder andere Module) noch nicht gelöst (dh sie sind leer, bis jemand vorbeikommt und alle Verbindungen herstellt).
Was der Linker tut, ist, alle Module zusammen zu betrachten, zu sehen, was jedes Modul benötigt, um sich mit außerhalb von sich selbst zu verbinden, und alle Dinge zu betrachten, die es exportiert. Anschließend wird das Problem behoben und eine endgültige ausführbare Datei erstellt, die dann ausgeführt werden kann.
Wenn auch eine dynamische Verknüpfung stattfindet, kann die Ausgabe des Linkers immer noch nicht ausgeführt werden. Es gibt noch einige Verweise auf externe Bibliotheken, die noch nicht aufgelöst wurden, und sie werden vom Betriebssystem zum Zeitpunkt des Ladens der App (oder möglicherweise) aufgelöst noch später während des Laufs).
quelle
Wenn der Compiler eine Objektdatei erstellt, enthält er Einträge für Symbole, die in dieser Objektdatei definiert sind, sowie Verweise auf Symbole, die in dieser Objektdatei nicht definiert sind. Der Linker nimmt diese und setzt sie zusammen, sodass (wenn alles richtig funktioniert) alle externen Referenzen aus jeder Datei durch Symbole erfüllt werden, die in anderen Objektdateien definiert sind.
Anschließend werden alle diese Objektdateien miteinander kombiniert und jedem der Symbole Adressen zugewiesen. Wenn eine Objektdatei einen externen Verweis auf eine andere Objektdatei enthält, wird die Adresse jedes Symbols dort ausgefüllt, wo sie von einem anderen Objekt verwendet wird. In einem typischen Fall wird auch eine Tabelle mit allen verwendeten absoluten Adressen erstellt, sodass der Loader die Adressen beim Laden der Datei "reparieren" kann / wird (dh er fügt jeder dieser Adressen die Basisladeadresse hinzu Adressen, damit sie sich alle auf die richtige Speicheradresse beziehen).
Nicht wenige moderne Linker können auch einige (in einigen Fällen viele ) andere "Dinge" ausführen, z. B. die Optimierung des Codes auf eine Weise, die nur möglich ist, wenn alle Module sichtbar sind (z. B. Entfernen der enthaltenen Funktionen) weil es möglich war , dass ein anderes Modul sie aufruft, aber sobald alle Module zusammengesetzt sind, ist es offensichtlich, dass nichts sie jemals aufruft).
quelle