Introducción: Los arquitectos invisibles del software
En el desarrollo cotidiano ejecutamos a menudo:
| |
Este proceso suele denominarse de forma genérica «compilación». Sin embargo, el compilador solo realiza la mitad de la tarea. La otra mitad—unir decenas de archivos objeto y bibliotecas independientes, resolver sus referencias cruzadas y cargarlos en memoria para que la CPU los ejecute—recae enteramente en el Enlazador (Linker) y el Cargador (Loader).
flowchart TD
subgraph BuildPipeline ["Del código fuente a la ejecución"]
SourceA["main.c"] -->|Compilador| ObjA["main.o (Objeto reubicable)"]
SourceB["utils.c"] -->|Compilador| ObjB["utils.o (Objeto reubicable)"]
Lib["libc.so / libm.a\n(Bibliotecas)"]
ObjA --> Linker["★ Enlazador (ld)\n[Resolución de símbolos & Reubicación]"]
ObjB --> Linker
Lib --> Linker
Linker --> Exec["myprogram (Binario ejecutable ELF)"]
end
Exec -->|execve| Loader["★ Cargador (Kernel & ld-linux.so)\n[Mapeo en memoria & Enlace dinámico]"]
Loader --> CPU["Ejecución en CPU (_start -> main)"]
style BuildPipeline fill:#f9f9f9,stroke:#333,stroke-width:2px
style Linker fill:#ffe6e6,stroke:#d9534f,stroke-width:2px
style Loader fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
Sin enlazadores, la programación modular sería inviable: tendríamos que escribir millones de líneas en un único archivo monstruoso.
1. El formato ELF: Anatomía de un archivo objeto
Los archivos intermedios (.o) son archivos objeto reubicables bajo el formato estándar ELF (Executable and Linkable Format):
.text: Código máquina ejecutable..rodata: Constantes de solo lectura y cadenas de texto..data: Variables globales y estáticas inicializadas..bss: Variables globales no inicializadas. ¡No ocupa espacio físico en disco! Solo se reserva y llena con ceros al cargar el programa..symtab: Tabla de símbolos (registro de funciones y variables)..rela.text: Tabla de reubicación con las direcciones temporales que deben corregirse.
2. Resolución de símbolos (Symbol Resolution)
El enlazador asocia cada llamada externa con su definición concreta.
Símbolos Fuertes y Débiles:
- Fuerte: Definiciones de funciones y variables globales inicializadas (
int x = 1;). - Débil: Variables globales no inicializadas (
int x;).
Reglas:
- Múltiples símbolos fuertes con el mismo nombre generan error (
multiple definition). - Un símbolo fuerte siempre prevalece sobre uno débil.
- Entre múltiples símbolos débiles, el enlazador elige uno al azar (riesgo de corrupción de memoria).
Sensibilidad al orden de bibliotecas estáticas: El enlazador procesa los argumentos de izquierda a derecha en una sola pasada. Por ello, -lm debe situarse siempre después de main.c.
3. Reubicación de direcciones (Relocation)
El compilador escribe ceros como marcador provisional en las llamadas externas:
| |
($S$: dirección del símbolo, $A$: addend, $P$: posición de la instrucción).
4. Enlace dinámico: Código independiente de posición (PIC), GOT y PLT
Para no duplicar bibliotecas en disco y memoria RAM, nacieron las bibliotecas compartidas (.so).
Requieren Código Independiente de Posición (PIC), soportado por dos tablas:
- GOT (Global Offset Table): Contiene las direcciones reales en memoria.
- PLT (Procedure Linkage Table): Tabla de salto en la sección de código.
Gracias al Enlace Perezoso (Lazy Binding), el enlazador dinámico (ld-linux.so) solo calcula la dirección de una función la primera vez que se invoca realmente.
5. El cargador: De _start a main()
Al invocar ./myprogram, la llamada execve mapea los segmentos ELF en la memoria virtual (mmap), inicializa la pila (argc, argv, envp) y salta al punto de entrada: _start.
La biblioteca de C ejecuta los constructores globales de .init_array antes de ceder el control a main().
