Esempio minimo di trasferimento dell'indirizzo
Il trasferimento degli indirizzi è una delle funzioni cruciali del collegamento.
Quindi diamo un'occhiata a come funziona con un esempio minimo.
0) Introduzione
Riepilogo: il riposizionamento modifica la .text
sezione dei file oggetto da tradurre:
- indirizzo del file oggetto
- nell'indirizzo finale dell'eseguibile
Questo deve essere fatto dal linker perché il compilatore vede solo un file di input alla volta, ma dobbiamo conoscere tutti i file oggetto contemporaneamente per decidere come:
- risolvere simboli indefiniti come funzioni dichiarate indefinite
- non creare conflitti tra più
.text
e .data
sezioni di più file oggetto
Prerequisiti: conoscenza minima di:
Il collegamento non ha nulla a che fare con C o C ++ in particolare: i compilatori generano semplicemente i file oggetto. Il linker quindi li prende come input senza mai sapere quale lingua li ha compilati. Potrebbe anche essere Fortran.
Quindi, per ridurre la crosta, studiamo un ciao mondo 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
compilato e assemblato con:
nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
con NASM 2.10.09.
1) .testo di .o
Per prima cosa decompiliamo la .text
sezione del file oggetto:
objdump -d hello_world.o
che dà:
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
le linee cruciali sono:
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
che dovrebbe spostare l'indirizzo della stringa hello world nel rsi
registro, che viene passato alla chiamata di sistema di scrittura.
Ma aspetta! Come può il compilatore sapere dove "Hello world!"
andrà a finire in memoria quando il programma viene caricato?
Bene, non può, specialmente dopo aver collegato un gruppo di .o
file insieme a più .data
sezioni.
Solo il linker può farlo poiché solo lui avrà tutti quei file oggetto.
Quindi il compilatore semplicemente:
- inserisce un valore segnaposto
0x0
sull'output compilato
- fornisce alcune informazioni extra al linker su come modificare il codice compilato con gli indirizzi validi
Queste "informazioni aggiuntive" sono contenute nella .rela.text
sezione del file oggetto
2) .rela.text
.rela.text
sta per "trasferimento della sezione .text".
La parola rilocazione viene utilizzata perché il linker dovrà riposizionare l'indirizzo dall'oggetto nell'eseguibile.
Possiamo smontare la .rela.text
sezione con:
readelf -r hello_world.o
che contiene;
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
Il formato di questa sezione è documentato fisso su: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Ogni voce indica al linker un indirizzo che deve essere riposizionato, qui ne abbiamo solo uno per la stringa.
Semplificando un po ', per questa particolare riga abbiamo le seguenti informazioni:
Offset = C
: qual è il primo byte del .text
che questa voce cambia.
Se guardiamo indietro al testo decompilato, è esattamente all'interno del critico movabs $0x0,%rsi
, e coloro che conoscono la codifica dell'istruzione x86-64 noteranno che questo codifica la parte dell'indirizzo a 64 bit dell'istruzione.
Name = .data
: l'indirizzo punta alla .data
sezione
Type = R_X86_64_64
, che specifica esattamente quale calcolo deve essere fatto per tradurre l'indirizzo.
Questo campo è effettivamente dipendente dal processore e quindi documentato nella sezione 4.4 "Trasferimento" dell'estensione ABI di AMD64 System V.
Quel documento dice che R_X86_64_64
:
Field = word64
: 8 byte, quindi l' 00 00 00 00 00 00 00 00
indirizzo0xC
Calculation = S + A
S
è il valore all'indirizzo che viene trasferito, quindi00 00 00 00 00 00 00 00
A
è l'addend che è 0
qui. Questo è un campo della voce del trasferimento.
Quindi S + A == 0
verremo trasferiti al primo indirizzo della .data
sezione.
3) .testo di .out
Ora guardiamo l'area di testo dell'eseguibile ld
generato per noi:
objdump -d hello_world.out
dà:
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
Quindi l'unica cosa che è cambiata dal file oggetto sono le righe critiche:
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
che ora puntano all'indirizzo 0x6000d8
( d8 00 60 00 00 00 00 00
in little-endian) invece di 0x0
.
È questa la posizione giusta per la hello_world
stringa?
Per decidere dobbiamo controllare gli header del programma, che dicono a Linux dove caricare ogni sezione.
Li smontiamo con:
readelf -l hello_world.out
che dà:
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
Questo ci dice che la .data
sezione, che è la seconda, inizia da VirtAddr
= 0x06000d8
.
E l'unica cosa nella sezione dati è la nostra stringa hello world.
Livello bonus