Ejemplo mínimo de reubicación de dirección
La reubicación de direcciones es una de las funciones cruciales de la vinculación.
Así que echemos un vistazo a cómo funciona con un ejemplo mínimo.
0) Introducción
Resumen: la reubicación edita la .text
sección de archivos de objeto para traducir:
- dirección de archivo de objeto
- en la dirección final del ejecutable
El vinculador debe hacer esto porque el compilador solo ve un archivo de entrada a la vez, pero debemos conocer todos los archivos de objetos a la vez para decidir cómo:
- resolver símbolos indefinidos como funciones indefinidas declaradas
- no choque múltiples
.text
y .data
secciones de múltiples archivos de objetos
Requisitos previos: comprensión mínima de:
La vinculación no tiene nada que ver específicamente con C o C ++: los compiladores solo generan los archivos de objetos. El enlazador los toma como entrada sin saber qué idioma los compiló. Bien podría ser Fortran.
Entonces, para reducir la corteza, estudiemos un mundo hola Linux de NASM x86-64 ELF:
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 y ensamblado con:
nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
con NASM 2.10.09.
1) .texto de .o
Primero descompilamos la .text
sección del archivo objeto:
objdump -d hello_world.o
lo que da:
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
Las líneas cruciales son:
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
que debe mover la dirección de la cadena hello world al rsi
registro, que se pasa a la llamada al sistema de escritura.
¡Pero espera! ¿Cómo puede saber el compilador dónde "Hello world!"
terminará en la memoria cuando se cargue el programa?
Bueno, no puede, especialmente después de vincular un montón de .o
archivos junto con varias .data
secciones.
Solo el enlazador puede hacer eso, ya que solo él tendrá todos esos archivos de objetos.
Entonces el compilador solo:
- pone un valor de marcador de posición
0x0
en la salida compilada
- proporciona información adicional al vinculador sobre cómo modificar el código compilado con las buenas direcciones
Esta "información adicional" está contenida en el .rela.text
sección del archivo objeto
2) .rela.text
.rela.text
significa "reubicación de la sección .text".
La palabra reubicación se usa porque el vinculador tendrá que reubicar la dirección del objeto en el ejecutable.
Podemos desmontar la .rela.text
sección con:
readelf -r hello_world.o
que 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
El formato de esta sección está documentado de forma fija en: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Cada entrada le dice al enlazador sobre una dirección que necesita ser reubicada, aquí solo tenemos una para la cadena.
Simplificando un poco, para esta línea en particular tenemos la siguiente información:
Offset = C
: cuál es el primer byte del .text
que cambia esta entrada.
Si miramos hacia atrás al texto descompilado, está exactamente dentro de lo crítico movabs $0x0,%rsi
, y aquellos que conocen la codificación de instrucciones x86-64 notarán que esto codifica la parte de la dirección de 64 bits de la instrucción.
Name = .data
: la dirección apunta a la .data
sección
Type = R_X86_64_64
, que especifica exactamente qué cálculo se debe hacer para traducir la dirección.
Este campo en realidad depende del procesador y, por lo tanto, está documentado en la extensión 4.4 ABI del Sistema V de AMD64 Sección 4.4 "Reubicación".
Ese documento dice que R_X86_64_64
sí:
Field = word64
: 8 bytes, por lo tanto la 00 00 00 00 00 00 00 00
dirección at0xC
Calculation = S + A
S
es el valor en la dirección que se reubica, por lo tanto00 00 00 00 00 00 00 00
A
es el agregado que está 0
aquí. Este es un campo de la entrada de reubicación.
Entonces, S + A == 0
nos trasladaremos a la primera dirección de la .data
sección.
3) .texto de .out
Ahora veamos el área de texto del ejecutable ld
generado para nosotros:
objdump -d hello_world.out
da:
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
Entonces, lo único que cambió del archivo de objeto son las líneas críticas:
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
que ahora apuntan a la dirección 0x6000d8
( d8 00 60 00 00 00 00 00
en little-endian) en lugar de 0x0
.
¿Es esta la ubicación correcta para la hello_world
cadena?
Para decidir tenemos que verificar los encabezados del programa, que le dicen a Linux dónde cargar cada sección.
Los desmontamos con:
readelf -l hello_world.out
lo que da:
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
Esto nos dice que la .data
sección, que es la segunda, comienza en VirtAddr
= 0x06000d8
.
Y lo único en la sección de datos es nuestra cadena hello world.
Nivel de bonificación