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 :
- Objektdatei Adresse
- in die endgültige Adresse der ausführbaren Datei
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:
- Undefinierte Symbole wie deklarierte undefinierte Funktionen auflösen
- nicht mehrere
.text
und .data
Abschnitte mehrerer Objektdateien kollidieren
Voraussetzungen: 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:
section .data
hello_world db "Hello world!", 10
section .text
global _start
_start:
; sys_write
mov rax, 1
mov rdi, 1
mov rsi, hello_world
mov rdx, 13
syscall
; sys_exit
mov rax, 60
mov rdi, 0
syscall
zusammengestellt und zusammengestellt mit:
nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
mit NASM 2.10.09.
1) .text von .o
Zuerst dekompilieren wir den .text
Abschnitt der Objektdatei:
objdump -d hello_world.o
was gibt:
0000000000000000 <_start>:
0: b8 01 00 00 00 mov $0x1,%eax
5: bf 01 00 00 00 mov $0x1,%edi
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
14: ba 0d 00 00 00 mov $0xd,%edx
19: 0f 05 syscall
1b: b8 3c 00 00 00 mov $0x3c,%eax
20: bf 00 00 00 00 mov $0x0,%edi
25: 0f 05 syscall
Die entscheidenden Zeilen sind:
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
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:
- Setzt einen Platzhalterwert
0x0
auf die kompilierte Ausgabe
- gibt dem Linker einige zusätzliche Informationen darüber, wie der kompilierte Code mit den guten Adressen geändert werden kann
Diese "zusätzlichen Informationen" sind im .rela.text
Abschnitt der Objektdatei enthalten
2) .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:
readelf -r hello_world.o
was beinhaltet;
Relocation section '.rela.text' at offset 0x340 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000c 000200000001 R_X86_64_64 0000000000000000 .data + 0
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
Abschnitt
Type = 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 die 00 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, der 0
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:
objdump -d hello_world.out
gibt:
00000000004000b0 <_start>:
4000b0: b8 01 00 00 00 mov $0x1,%eax
4000b5: bf 01 00 00 00 mov $0x1,%edi
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
4000c4: ba 0d 00 00 00 mov $0xd,%edx
4000c9: 0f 05 syscall
4000cb: b8 3c 00 00 00 mov $0x3c,%eax
4000d0: bf 00 00 00 00 mov $0x0,%edi
4000d5: 0f 05 syscall
Das einzige, was sich gegenüber der Objektdatei geändert hat, sind die kritischen Zeilen:
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
die jetzt auf die Adresse 0x6000d8
( d8 00 60 00 00 00 00 00
in Little-Endian) anstelle von zeigen 0x0
.
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:
readelf -l hello_world.out
was gibt:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000d7 0x00000000000000d7 R E 200000
LOAD 0x00000000000000d8 0x00000000006000d8 0x00000000006000d8
0x000000000000000d 0x000000000000000d RW 200000
Section to Segment mapping:
Segment Sections...
00 .text
01 .data
Dies sagt uns, dass der .data
Abschnitt, der der zweite ist, bei VirtAddr
= beginnt 0x06000d8
.
Und das einzige, was im Datenbereich steht, ist unser Hallo-Welt-String.
Bonuslevel