c++ linker dynamic-linking

c++ - ¿Qué hacen los vinculadores?



linker dynamic-linking (4)

Ejemplo mínimo de reubicación de direcciones

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

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íticos movabs $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ó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 sumando 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 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.

DESCARGO DE RESPONSABILIDAD : He respondido una pregunta duplicada de esto, y solo encontré esta ahora. Voté para cerrar el engaño: ¿cómo funciona el enlace de C ++ en la práctica?

Siempre me he preguntado. Sé que los compiladores convierten el código que escribes en binarios, pero ¿qué hacen los vinculadores? Ellos siempre han sido un misterio para mí.

Yo más o menos entiendo lo que es ''vincular''. Es cuando las referencias a bibliotecas y marcos se agregan al binario. No entiendo nada más allá de eso. Para mí, "simplemente funciona". También entiendo los conceptos básicos de la vinculación dinámica, pero nada demasiado profundo.

¿Podría alguien explicar los términos?


Cuando el compilador produce un archivo objeto, incluye entradas para símbolos que están definidos en ese archivo objeto y referencias a símbolos que no están definidos en ese archivo objeto. El enlazador los toma y los junta para que (cuando todo funciona correctamente) todas las referencias externas de cada archivo se satisfagan con símbolos definidos en otros archivos de objeto.

Luego combina todos esos archivos de objeto y asigna direcciones a cada uno de los símbolos, y cuando un archivo objeto tiene una referencia externa a otro archivo objeto, rellena la dirección de cada símbolo donde sea que lo use otro objeto. En un caso típico, también creará una tabla con las direcciones absolutas utilizadas, por lo que el cargador puede / "arreglará" las direcciones cuando se cargue el archivo (es decir, agregará la dirección de carga base a cada una de ellas). direcciones para que todos se refieran a la dirección de memoria correcta).

Muchos enlazadores modernos también pueden realizar algunos (en algunos casos mucho ) de otras "cosas", como optimizar el código de maneras que solo son posibles una vez que todos los módulos son visibles (por ejemplo, eliminar funciones incluidas). porque era posible que algún otro módulo los llamara, pero una vez que todos los módulos se unen, es evidente que nunca los llama nada).


En lenguajes como ''C'', los módulos individuales de código se compilan tradicionalmente por separado en blobs de código de objeto, que está listo para ejecutarse en todos los aspectos excepto que todas las referencias que ese módulo hace fuera de sí mismo (es decir, a bibliotecas u otros módulos) tienen aún no se han resuelto (es decir, están en blanco, a la espera de que alguien entre y haga todas las conexiones).

Lo que hace el enlazador es mirar todos los módulos juntos, ver a qué se debe conectar cada módulo fuera de sí mismo, y observar todas las cosas que está exportando. Luego arregla todo y produce un ejecutable final, que luego se puede ejecutar.

Donde también se realiza la vinculación dinámica, la salida del enlazador aún no se puede ejecutar; todavía hay algunas referencias a bibliotecas externas que aún no se han resuelto, y el SO las resuelve en el momento en que carga la aplicación (o posiblemente incluso más tarde durante la carrera).


Para comprender los enlazadores, primero es útil comprender qué sucede "debajo del capó" cuando convierte un archivo fuente (como un archivo C o C ++) en un archivo ejecutable (un archivo ejecutable es un archivo que se puede ejecutar en su máquina o la máquina de otra persona ejecutando la misma arquitectura de máquina).

Debajo del capó, cuando se compila un programa, el compilador convierte el archivo de origen en código de byte de objeto. Este código de bytes (a veces llamado código objeto) es instrucciones mnemotécnicas que solo la arquitectura de su computadora entiende. Tradicionalmente, estos archivos tienen una extensión .OBJ.

Después de que se crea el archivo objeto, el enlazador entra en juego. La mayoría de las veces, un programa real que hace algo útil necesitará referenciar otros archivos. En C, por ejemplo, un programa simple para imprimir su nombre en la pantalla consistiría en:

printf("Hello Kristina!/n");

Cuando el compilador compiló su programa en un archivo obj, simplemente pone una referencia a la función printf . El enlazador resuelve esta referencia. La mayoría de los lenguajes de programación tienen una biblioteca estándar de rutinas para cubrir las cosas básicas que se esperan de ese lenguaje. El vinculador vincula su archivo OBJ con esta biblioteca estándar. El vinculador también puede vincular su archivo OBJ con otros archivos OBJ. Puede crear otros archivos OBJ que tengan funciones a las que pueda llamar otro archivo OBJ. El enlazador funciona casi como copiar y pegar de un procesador de textos. "Copia" todas las funciones necesarias a las que su programa hace referencia y crea un solo ejecutable. En ocasiones, otras bibliotecas que se copian dependen de otros archivos OBJ o de biblioteca. A veces un enlazador tiene que ser bastante recursivo para hacer su trabajo.

Tenga en cuenta que no todos los sistemas operativos crean un solo ejecutable. Windows, por ejemplo, usa DLL que mantienen todas estas funciones juntas en un solo archivo. Esto reduce el tamaño de su ejecutable, pero hace que su ejecutable dependa de estos archivos DLL específicos. DOS solía usar cosas llamadas superposiciones (archivos .OVL). Esto tenía muchos propósitos, pero uno era mantener juntas las funciones utilizadas comúnmente en un archivo (otro propósito al que sirvió, en caso de que se lo pregunte, era poder colocar programas grandes en la memoria. DOS tiene una limitación en la memoria y las superposiciones podrían estar "descargado" de la memoria y otras superposiciones podrían "cargarse" encima de esa memoria, de ahí el nombre, "superposiciones"). Linux tiene bibliotecas compartidas, que es básicamente la misma idea que las DLL (los tipos duros de Linux que conozco me dirían que hay MUCHAS GRANDES diferencias).

Espero que esto te ayude a entender!