Para comprender los enlazadores, es útil entender primero qué sucede "bajo el capó" cuando convierte un archivo fuente (como un archivo C o C ++) en un archivo ejecutable (un archivo ejecutable es un archivo que puede ejecutarse en su máquina o la máquina de otra persona que ejecuta la misma arquitectura de máquina).
Bajo el capó, cuando se compila un programa, el compilador convierte el archivo fuente en código de bytes de objeto. Este código de bytes (a veces llamado código de objeto) son instrucciones mnemónicas que solo la arquitectura de su computadora comprende. Tradicionalmente, estos archivos tienen una extensión .OBJ.
Una vez creado el archivo objeto, el enlazador entra en juego. La mayoría de las veces, un programa real que haga algo útil deberá hacer referencia a 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 alprintf
función. 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 hace referencia su programa y crea un solo ejecutable. A veces, otras bibliotecas que se copian dependen de otros archivos OBJ o de biblioteca. A veces un enlazador tiene que volverse bastante recursivo para hacer su trabajo.
Tenga en cuenta que no todos los sistemas operativos crean un solo ejecutable. Windows, por ejemplo, usa archivos 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 estas DLL específicas. DOS solía usar cosas llamadas superposiciones (archivos .OVL). Esto tenía muchos propósitos, pero uno era mantener juntas las funciones de uso común en 1 archivo (otro propósito al que sirvió, en caso de que se lo pregunte, era poder ajustar programas grandes en la memoria. DOS tiene una limitación en la memoria y las superposiciones podrían ser "descargado" de la memoria y otras superposiciones podrían "cargarse" encima de esa memoria, de ahí el nombre, "superposiciones"). Linux ha compartido bibliotecas, que es básicamente la misma idea que las DLL (los chicos de Linux que conozco dirían que hay MUCHAS GRANDES diferencias).
Espero que esto te ayude a entender!
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: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:
.text
y.data
secciones de múltiples archivos de objetosRequisitos 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:
compilado y ensamblado con:
con NASM 2.10.09.
1) .texto de .o
Primero descompilamos la
.text
sección del archivo objeto:lo que da:
Las líneas cruciales son:
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:
0x0
en la salida compiladaEsta "información adicional" está contenida en el
.rela.text
sección del archivo objeto2) .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:que contiene;
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ónType = 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 la00 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:da:
Entonces, lo único que cambió del archivo de objeto son las líneas críticas:
que ahora apuntan a la dirección
0x6000d8
(d8 00 60 00 00 00 00 00
en little-endian) en lugar de0x0
.¿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:
lo que da:
Esto nos dice que la
.data
sección, que es la segunda, comienza enVirtAddr
=0x06000d8
.Y lo único en la sección de datos es nuestra cadena hello world.
Nivel de bonificación
PIE
vinculación: ¿Cuál es la opción -fPIE para ejecutables independientes de la posición en gcc y ld?fuente
En lenguajes como 'C', los módulos individuales de código se compilan tradicionalmente por separado en blobs de código objeto, que está listo para ejecutarse en todos los aspectos además de 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 venga y haga todas las conexiones).
Lo que hace el enlazador es mirar todos los módulos juntos, ver lo que cada módulo necesita para conectarse al exterior, y ver todas las cosas que está exportando. Luego lo arregla todo y produce un ejecutable final, que luego se puede ejecutar.
Donde también se está ejecutando el enlace dinámico, la salida del enlazador todavía 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).
fuente
Cuando el compilador produce un archivo de objeto, incluye entradas para símbolos que están definidos en ese archivo de objeto y referencias a símbolos que no están definidos en ese archivo de objeto. El vinculador los toma y los junta para que (cuando todo funcione bien) todas las referencias externas de cada archivo se satisfagan mediante símbolos que se definen en otros archivos de objetos.
Luego combina todos esos archivos de objetos y asigna direcciones a cada uno de los símbolos, y cuando un archivo de objeto tiene una referencia externa a otro archivo de objeto, completa la dirección de cada símbolo donde sea que lo use otro objeto. En un caso típico, también creará una tabla de las direcciones absolutas utilizadas, por lo que el cargador puede "arreglar" las direcciones cuando se carga el archivo (es decir, agregará la dirección de carga base a cada una de esas direcciones). direcciones para que todos se refieran a la dirección de memoria correcta).
Unos cuantos enlazadores modernos también pueden llevar a cabo algunas (en algunos casos muchas ) 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 están juntos, es evidente que nada los llama).
fuente