wandres.dev
INICIADO · El proceso de compilación

El proceso de compilación en 4 fases

De .c a ejecutable no es un solo paso mágico, son cuatro: preprocesado, compilación, ensamblado y enlazado. Ver cada uno por dentro desmitifica todo lo demás.

⏱ 14 min

Cuando escribes gcc prog.c -o prog, ocurren cuatro transformaciones distintas, en orden. Entenderlas es una de las cosas que más te separa de quien solo “compila y reza”: cuando algo falle —un símbolo no definido, un header que no aparece— sabrás exactamente en qué fase y por qué.

🎯 Al terminar esta lección sabrás
  • Las cuatro fases: preprocesado, compilación, ensamblado, enlazado.
  • Qué produce exactamente cada una.
  • Detener el compilador en cada fase para verlo.
  • Qué es un archivo objeto y un símbolo.

Las cuatro fases

flowchart LR
C[prog.c] -->|preprocesado| I[prog.i]
I -->|compilacion| S[prog.s]
S -->|ensamblado| O[prog.o]
O -->|enlazado| E[ejecutable]
style C fill:#89b4fa,color:#11111b
style I fill:#94e2d5,color:#11111b
style S fill:#f9e2af,color:#11111b
style O fill:#fab387,color:#11111b
style E fill:#a6e3a1,color:#11111b

1. Preprocesado

El preprocesador manipula texto: expande #include (pega el contenido de los headers), sustituye macros (#define), y resuelve la compilación condicional (#ifdef). Produce una unidad de traducción pura, sin directivas.

gcc -std=c23 -E prog.c -o prog.i   # -E: para tras preprocesar

Abre prog.i y verás tu código con todo el stdio.h pegado encima. El preprocesador no entiende C: solo mueve texto.

2. Compilación

Ahora sí: el compilador toma la unidad de traducción, la analiza (¿es C válido?), y la traduce a ensamblador para tu arquitectura.

gcc -std=c23 -S prog.i -o prog.s   # -S: para tras compilar

prog.s es texto en lenguaje ensamblador: las instrucciones de la CPU, aún legibles.

3. Ensamblado

El ensamblador traduce ese texto a código máquina binario, empaquetado en un archivo objeto (.o).

gcc -std=c23 -c prog.s -o prog.o   # -c: para tras ensamblar

prog.o ya es binario: instrucciones de la máquina, más una tabla de símbolos (los nombres de tus funciones y variables) y relocalizaciones pendientes.

4. Enlazado (linking)

El linker toma uno o más .o (y las bibliotecas), resuelve los símbolos —conecta cada llamada con la función real, esté en tu código o en la libc— y produce el ejecutable final.

gcc prog.o -o prog                 # enlaza y produce el ejecutable

Verlo todo de golpe

# el flujo completo, guardando cada intermedio
gcc -std=c23 -save-temps prog.c -o prog
ls prog.*      # prog.i  prog.s  prog.o  prog
Casi todos los errores tienen una fase

Este modelo mental resuelve una confusión enorme. Un error de “header no encontrado” es de preprocesado. Un error de sintaxis o de tipos es de compilación. Y el temido “undefined reference to …” no es un error de compilación: es del linker — tu código compiló bien, pero el linker no encuentra la definición de un símbolo que usaste (te falta un .o o una -l de biblioteca). Quien no distingue las fases pierde horas buscando en el sitio equivocado. Tú, en cuanto leas el error, sabrás en qué fase mirar. Este es de los conocimientos más rentables de todo el track.

Archivos objeto y símbolos

Un .o no es ejecutable por sí solo: le faltan las conexiones. Contiene:

  • Código y datos de tus funciones y variables globales.
  • Una tabla de símbolos: qué define (T en el listado) y qué necesita de fuera (U, undefined).
  • Relocalizaciones: huecos que el linker rellenará con direcciones finales.

Inspecciónalo:

nm prog.o        # lista los símbolos: T=definido, U=indefinido
objdump -d prog.o  # desensambla el código máquina
file prog          # te dice que es un ELF ejecutable
ℹ️
Esto es la semilla de los niveles 20 y 21

Los símbolos, las relocalizaciones y el formato de estos archivos son la base del linker (nivel 20, ELF) y de la ABI (nivel 21). Por ahora quédate con la idea: compilar produce piezas con símbolos, y enlazar es conectar esas piezas. Todo el tooling de build (Meson, Make, bibliotecas) existe para orquestar estas cuatro fases sobre muchos archivos.

⚔️ Disecciona una compilación
  1. Compila un programa con -save-temps y abre el .i, el .s y mira el .o.
  2. Usa gcc -E sobre un archivo con un #define y observa la sustitución.
  3. Llama a una función que no definas (sin implementarla) y provoca a propósito un “undefined reference”: confirma que es un error de linker, no de compilación.
  4. Usa nm sobre un .o para ver sus símbolos definidos (T) e indefinidos (U).