Przykład minimalnego relokacji adresu
Przenoszenie adresów jest jedną z kluczowych funkcji linkowania.
Przyjrzyjmy się więc, jak to działa na minimalnym przykładzie.
0) Wprowadzenie
Podsumowanie: relokacja edytuje .text
sekcję plików obiektowych do przetłumaczenia:
- adres pliku obiektu
- na końcowy adres pliku wykonywalnego
Musi to zrobić konsolidator, ponieważ kompilator widzi tylko jeden plik wejściowy naraz, ale musimy wiedzieć o wszystkich plikach obiektowych naraz, aby zdecydować, jak:
- rozwiązać niezdefiniowane symbole, takie jak zadeklarowane niezdefiniowane funkcje
- nie kolidują ze sobą wielu
.text
i .data
sekcji wielu plików obiektowych
Wymagania wstępne: minimalne zrozumienie:
Łączenie nie ma w szczególności nic wspólnego z C lub C ++: kompilatory po prostu generują pliki obiektowe. Następnie konsolidator przyjmuje je jako dane wejściowe, nigdy nie wiedząc, w jakim języku je skompilował. Równie dobrze mógłby to być Fortran.
Aby zmniejszyć problem, przyjrzyjmy się NASM x86-64 ELF Linux hello world:
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
skompilowany i złożony z:
nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
z NASM 2.10.09.
1). Tekst .o
Najpierw dekompilujemy .text
sekcję pliku obiektowego:
objdump -d hello_world.o
co daje:
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
kluczowe linie to:
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
który powinien przenieść adres ciągu hello world do rsi
rejestru, który jest przekazywany do wywołania systemowego write.
Ale poczekaj! Skąd kompilator może wiedzieć, gdzie "Hello world!"
znajdzie się w pamięci po załadowaniu programu?
Cóż, nie może, szczególnie po połączeniu kilku .o
plików z wieloma .data
sekcjami.
Tylko konsolidator może to zrobić, ponieważ tylko on będzie miał wszystkie te pliki obiektowe.
Więc kompilator po prostu:
- umieszcza wartość zastępczą
0x0
na skompilowanym wyjściu
- daje linkerowi dodatkowe informacje o tym, jak zmodyfikować skompilowany kod z poprawnymi adresami
Te „dodatkowe informacje” są zawarte w .rela.text
sekcji pliku obiektowego
2) .rela.text
.rela.text
oznacza „przeniesienie sekcji .text”.
Stosowane jest słowo relokacja, ponieważ linker będzie musiał przenieść adres z obiektu do pliku wykonywalnego.
Możemy zdemontować .rela.text
sekcję za pomocą:
readelf -r hello_world.o
który zawiera;
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
Format tej sekcji jest udokumentowany pod adresem : http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Każdy wpis mówi linkerowi o jednym adresie, który ma zostać przeniesiony, tutaj mamy tylko jeden dla łańcucha.
Trochę upraszczając, dla tej konkretnej linii mamy następujące informacje:
Offset = C
: jaki jest pierwszy bajt .text
tego wpisu.
Jeśli spojrzymy wstecz na zdekompilowany tekst, znajduje się on dokładnie w środku krytycznym movabs $0x0,%rsi
, a ci, którzy znają kodowanie instrukcji x86-64, zauważą, że koduje to 64-bitową część adresową instrukcji.
Name = .data
: adres wskazuje na .data
sekcję
Type = R_X86_64_64
, który określa dokładnie, jakie obliczenia należy wykonać, aby przetłumaczyć adres.
To pole jest w rzeczywistości zależne od procesora i dlatego jest udokumentowane w AMD64 System V ABI, sekcja 4.4 „Relokacja”.
Ten dokument mówi, że R_X86_64_64
tak:
Field = word64
: 8 bajtów, czyli 00 00 00 00 00 00 00 00
adres at0xC
Calculation = S + A
S
jest więc wartością pod adresem, który ma zostać przeniesiony00 00 00 00 00 00 00 00
A
jest dodatkiem, który jest 0
tutaj. To jest pole wpisu relokacji.
A więc S + A == 0
zostaniemy przeniesieni pod pierwszy adres .data
sekcji.
3). Tekst .out
Teraz spójrzmy na obszar tekstowy ld
wygenerowanego dla nas pliku wykonywalnego :
objdump -d hello_world.out
daje:
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
Więc jedyną rzeczą, która zmieniła się w pliku obiektowym, są linie krytyczne:
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
które teraz wskazują adres 0x6000d8
( d8 00 60 00 00 00 00 00
w little-endian) zamiast0x0
.
Czy to właściwa lokalizacja dla hello_world
ciągu?
Aby zdecydować, musimy sprawdzić nagłówki programu, które mówią Linuksowi, gdzie załadować każdą sekcję.
Demontujemy je za pomocą:
readelf -l hello_world.out
co daje:
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
To mówi nam, że .data
sekcja, która jest drugą, zaczyna się od VirtAddr
= 0x06000d8
.
Jedyną rzeczą w sekcji danych jest nasz ciąg hello world.
Poziom bonusowy