Featured image of post Cómo se unen programas escritos por separado: El funcionamiento interno de Linkers y Loaders

Cómo se unen programas escritos por separado: El funcionamiento interno de Linkers y Loaders

Introducción: Los arquitectos invisibles del software

En el desarrollo cotidiano ejecutamos a menudo:

1
gcc -O2 main.c utils.c math.c -o myprogram

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:

  1. Múltiples símbolos fuertes con el mismo nombre generan error (multiple definition).
  2. Un símbolo fuerte siempre prevalece sobre uno débil.
  3. 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:

1
callq 0x00000000  # Desplazamiento ficticio
$$\text{Valor} = S + A - P$$

($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().

comments powered by Disqus