c++ - quimicos - estructura de lewis
¿Cómo funciona el enlace C++ en la práctica? (3)
Esta pregunta ya tiene una respuesta aquí:
- ¿Qué hacen los vinculadores? 4 respuestas
¿Cómo funciona el enlace C ++ en la práctica? Lo que estoy buscando es una explicación detallada sobre cómo ocurre la vinculación, y no qué comandos hacen los enlaces.
Ya hay una pregunta similar sobre la compilación que no entra en demasiados detalles: ¿cómo funciona el proceso de compilación / vinculación?
Además de los ya mencionados " Linkers and Loaders ", si querías saber cómo funciona un enlazador real y moderno, puedes comenzar airs.com/blog/archives/38 .
En realidad, uno podría decir que la vinculación es relativamente simple.
En el sentido más simple, se trata de agrupar los archivos de objetos 1, ya que los que ya contienen el ensamblaje emitido para cada una de las funciones / globales / datos ... contenidos en sus respectivas fuentes. El enlazador puede ser extremadamente tonto y tratar todo como un símbolo (nombre) y su definición (o contenido).
Obviamente, el vinculador necesita producir un archivo que respete un determinado formato (el formato ELF generalmente en Unix) y separará las diversas categorías de código / datos en diferentes secciones del archivo, pero eso es solo el envío.
Las dos complicaciones que conozco son:
la necesidad de duplicar los símbolos: algunos símbolos están presentes en varios archivos de objeto y solo uno debe crearse en la biblioteca / ejecutable resultante; es el trabajo del enlazador para incluir solo una de las definiciones
optimización de tiempo de enlace: en este caso los archivos de objetos no contienen el ensamblaje emitido sino una representación intermedia y el enlazador combina todos los archivos de objetos, aplica pases de optimización (en línea, por ejemplo), los compila para ensamblar y finalmente emitir su resultado .
1 : el resultado de la compilación de las diferentes unidades de traducción (más o menos, archivos fuente preprocesados)
EDITAR : He movido esta respuesta al duplicado: https://.com/a/33690144/895245
Esta respuesta se centra en la reubicación de direcciones , que es una de las funciones cruciales de la vinculación.
Se usará un ejemplo mínimo para aclarar el concepto.
0) Introducción
Resumen: la reubicación edita la sección .text
de los archivos de objetos para traducir:
- dirección de archivo de objeto
- en la dirección final del ejecutable
Esto debe hacerlo el vinculador 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 hacerlo:
- resolver símbolos indefinidos como funciones indefinidas declaradas
- no
.text
múltiples secciones.text
y.data
de múltiples archivos de objeto
Prerrequisitos: comprensión mínima de:
- ensamblaje x86-64 o IA-32
- estructura global de un archivo ELF. He hecho un tutorial para eso
El enlace no tiene nada que ver con C o C ++ específicamente: los compiladores solo generan los archivos objeto. El vinculador los toma como entrada sin saber qué idioma los compiló. Podría ser Fortran.
Para reducir la corteza, estudiemos un 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
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) .text de .o
Primero descompilamos la sección .text
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 debería mover la dirección de la cadena hello world al registro rsi
, que se pasa a la llamada del sistema de escritura.
¡Pero espera! ¿Cómo puede el compilador saber dónde "Hello world!"
terminará en la memoria cuando se carga el programa?
Bueno, no puede, especialmente después de que vinculamos un grupo de archivos .o
junto con múltiples secciones .data
.
Solo el enlazador puede hacerlo, ya que solo él tendrá todos esos archivos de objeto.
Entonces el compilador simplemente:
- pone un valor de marcador
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 la sección .rela.text
del archivo de objeto
2) .rela.text
.rela.text
significa "reubicación de la sección .text".
La palabra reubicación se usa porque el enlazador tendrá que reubicar la dirección del objeto en el ejecutable.
Podemos desmontar la sección .rela.text
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á resuelto documentado en: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Cada entrada le dice al vinculador acerca de una dirección que necesita ser reubicada, aquí tenemos solo 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 los
movabs $0x0,%rsi
críticosmovabs $0x0,%rsi
, y aquellos que conocen la codificación de la instrucción 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 sección.data
Type = R_X86_64_64
, que especifica qué tipo de cálculo se debe realizar para traducir la dirección.Este campo es realmente dependiente del procesador y, por lo tanto, está documentado en la sección de extensión AMD64 Sistema V ABI 4.4 "Reubicación".
Ese documento dice que
R_X86_64_64
hace:Field = word64
: 8 bytes, por lo tanto,00 00 00 00 00 00 00 00
en la dirección0xC
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 sumando que es0
aquí. Este es un campo de la entrada de reubicación.
Entonces
S + A == 0
y seremos reubicados a la primera dirección de la sección.data
.-
3) .text de .out
Ahora veamos el área de texto del ld
ejecutable generado por 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 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 este el lugar correcto para la cadena hello_world
?
Para decidir, debemos verificar los encabezados del programa, que indican a Linux dónde cargar cada sección.
Los desarmamos 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 sección .data
, que es la segunda, comienza en VirtAddr
= 0x06000d8
.
Y lo único en la sección de datos es nuestra cadena hello world.