¿Cómo funciona el enlace C ++ en la práctica?

¿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 comstackción que no entra en demasiados detalles: ¿cómo funciona el proceso de comstackción / vinculación?

EDITAR : He movido esta respuesta al duplicado: https://stackoverflow.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 comstackdor 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 comstackdores 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 

comstackdo 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 descomstackmos 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 comstackdor saber dónde "Hello world!" terminará en la memoria cuando se carga el progtwig?

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 comstackdor simplemente:

  • pone un valor de marcador 0x0 en la salida comstackda
  • proporciona información adicional al vinculador sobre cómo modificar el código comstackdo 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 descomstackdo, está exactamente dentro de los movabs $0x0,%rsi críticos movabs $0x0,%rsi , y aquellos que conocen la encoding 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ón 0xC

    • Calculation = S + A

      • S es el valor en la dirección que se reubica, por lo tanto 00 00 00 00 00 00 00 00
      • A es el sumndo que es 0 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 progtwig, 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 RE 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.

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 fonts. 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 comstack para ensamblar y finalmente emitir su resultado .

1 : el resultado de la comstackción de las diferentes unidades de traducción (más o menos, archivos fuente preprocesados)

Además de los ya mencionados ” Linkers and Loaders “, si querías saber cómo funciona un enlazador real y moderno, puedes comenzar aquí .