Exemplo mínimo de realocação de endereços
A realocação de endereços é uma das funções cruciais da vinculação.
Então, vamos dar uma olhada em como funciona com um exemplo mínimo.
0) Introdução
Resumo: a realocação edita a .text
seção dos arquivos de objeto para traduzir:
- endereço do arquivo de objeto
- para o endereço final do executável
Isso deve ser feito pelo vinculador, porque o compilador vê apenas um arquivo de entrada por vez, mas precisamos conhecer todos os arquivos de objetos de uma só vez para decidir como:
- resolver símbolos indefinidos como funções indefinidas declaradas
- não colidir múltiplos
.text
e .data
seções de múltiplos arquivos de objeto
Pré-requisitos: entendimento mínimo de:
A vinculação não tem nada a ver com C ou C ++ especificamente: os compiladores apenas geram os arquivos de objeto. O vinculador as aceita como entrada, sem nunca saber qual idioma as compilou. Pode muito bem ser Fortran.
Então, para reduzir a crosta, vamos estudar um olá mundo 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
compilado e montado com:
nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
com NASM 2.10.09.
1) .texto de .o
Primeiro, descompilamos a .text
seção do arquivo de objeto:
objdump -d hello_world.o
que 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
as linhas cruciais são:
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
que deve mover o endereço da cadeia hello world para o rsi
registro, que é passado para a chamada do sistema de gravação.
Mas espere! Como o compilador pode saber onde "Hello world!"
ficará a memória quando o programa for carregado?
Bem, não pode, especialmente depois de vincularmos um monte de .o
arquivos a várias .data
seções.
Somente o vinculador pode fazer isso, pois somente ele terá todos esses arquivos de objeto.
Então, o compilador apenas:
- coloca um valor de espaço reservado
0x0
na saída compilada
- fornece algumas informações extras ao vinculador sobre como modificar o código compilado com os bons endereços
Esta "informação extra" está contida na .rela.text
seção do arquivo de objeto
2) .rela.text
.rela.text
significa "realocação da seção .text".
A palavra realocação é usada porque o vinculador precisará realocar o endereço do objeto para o executável.
Podemos desmontar a .rela.text
seção com:
readelf -r hello_world.o
que contém;
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
O formato desta seção é corrigido documentado em: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Cada entrada informa ao vinculador sobre um endereço que precisa ser realocado, aqui temos apenas um para a sequência.
Simplificando um pouco, para esta linha em particular, temos as seguintes informações:
Offset = C
: qual é o primeiro byte do .text
que esta entrada altera.
Se olharmos para o texto descompilado, ele estará exatamente dentro do crítico movabs $0x0,%rsi
e aqueles que conhecem a codificação da instrução x86-64 perceberão que isso codifica a parte do endereço de 64 bits da instrução.
Name = .data
: o endereço aponta para a .data
seção
Type = R_X86_64_64
, que especifica exatamente o que cálculo deve ser feito para converter o endereço.
Esse campo é realmente dependente do processador e, portanto, documentado na seção 4.464 "Relocação" da extensão AMD64 System V ABI .
Esse documento diz que R_X86_64_64
:
Field = word64
: 8 bytes, portanto, o 00 00 00 00 00 00 00 00
endereço0xC
Calculation = S + A
S
é o valor no endereço que está sendo realocado,00 00 00 00 00 00 00 00
A
é o adendo que está 0
aqui. Este é um campo da entrada de realocação.
Então S + A == 0
, seremos realocados para o primeiro endereço da .data
seção.
3). Texto de .out
Agora vamos ver a área de texto do executável ld
gerado para nós:
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
Portanto, a única coisa que mudou no arquivo de objeto são as linhas críticas:
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
que agora apontam para o endereço 0x6000d8
( d8 00 60 00 00 00 00 00
em little-endian) em vez de 0x0
.
Esse é o local certo para a hello_world
string?
Para decidir, precisamos verificar os cabeçalhos do programa, que informam ao Linux onde carregar cada seção.
Nós os desmontamos com:
readelf -l hello_world.out
que 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
Isso nos diz que a .data
seção, que é a segunda, começa em VirtAddr
= 0x06000d8
.
E a única coisa na seção de dados é a nossa string hello world.
Nível de bônus