Cómo se compila un binario, de código a proceso
Publicado el 2026-09-12 por frfrand
Las cuatro etapas que convierten texto en un programa, qué hace el enlazador y por qué falla tanto, la diferencia entre enlazado estático y dinámico, y qué pasa al ejecutarlo.

Escribís código, corrés un comando, aparece un ejecutable. En el medio hay cuatro etapas, y saber cuál es cuál cambia por completo la interpretación de los errores.
1. Preprocesado
Se resuelven las directivas: se pegan los archivos incluidos, se expanden las macros, se eliminan los bloques condicionales que no aplican.
El resultado sigue siendo texto, normalmente mucho más largo que el original.
2. Compilación
El compilador traduce ese texto a instrucciones de máquina. Verifica la sintaxis, aplica optimizaciones y genera un archivo objeto por cada archivo fuente.
Un archivo objeto tiene código máquina, pero no es ejecutable todavía: contiene referencias a cosas que están en otro lado. Si tu código llama a una función definida en otro archivo, el objeto guarda el nombre y un espacio a completar.
Los errores de esta etapa son los familiares: un punto y coma, un tipo que no coincide, una variable no declarada. Te dicen archivo y línea.
3. Enlazado
Acá es donde aparecen los errores que más desconciertan.
El enlazador junta todos los objetos y las bibliotecas, y resuelve cada referencia pendiente: para cada nombre usado, busca dónde está definido y anota la dirección.
Si un nombre no aparece en ningún lado, falla con el clásico:
undefined reference to 'mi_funcion'
Este error confunde porque parece un error de compilación y no lo es. El compilador estuvo perfectamente feliz: sabía que esa función existía en algún lado. El que no la encuentra es el enlazador.
Las causas habituales: falta agregar un archivo a la compilación, falta enlazar una biblioteca (el clásico -lm para matemáticas), o el orden de las bibliotecas está mal (el enlazador las procesa en orden y algunos casos son sensibles a eso).
Estático o dinámico
Una decisión con consecuencias muy prácticas.
Enlazado estático: el código de las bibliotecas se copia adentro de tu ejecutable. El binario es más grande y no depende de nada externo: lo copiás a otra máquina y funciona.
Enlazado dinámico: el ejecutable guarda sólo el nombre de la biblioteca, y se carga al ejecutar. El binario es chico, y varios programas comparten la misma biblioteca en memoria. Además, una actualización de seguridad de la biblioteca beneficia a todos los programas sin recompilar.
El costo del dinámico es el error del otro extremo:
error while loading shared libraries: libfoo.so.3: cannot open shared object file
Eso es tiempo de ejecución: el programa compiló y enlazó bien, pero al arrancar no encuentra la biblioteca. Para ver de qué depende un binario:
ldd /usr/bin/miprograma
Este es el motivo por el que un binario compilado en una distribución puede no correr en otra, y es también una de las razones por las que existen los contenedores: encapsular el conjunto exacto de bibliotecas.
4. Cargar y ejecutar
Cuando ejecutás el programa, el kernel lee la cabecera del archivo (en Linux, formato ELF), reserva memoria, mapea las secciones de código y datos, y le pasa el control al cargador dinámico, que resuelve las bibliotecas que faltan y finalmente salta al punto de entrada.
Curiosidad: el segmento de código se mapea en modo sólo lectura y compartido. Si ejecutás el mismo programa veinte veces, el código está una sola vez en memoria física.
Y los lenguajes que no compilan así
Python, JavaScript o Ruby no producen un binario nativo. Se interpretan, o se compilan a un formato intermedio que ejecuta una máquina virtual.
Java y C# están en el medio: compilan a bytecode y lo traducen a código nativo mientras corren.
Go y Rust hacen lo contrario y por eso se hicieron populares en infraestructura: producen un binario estático que no depende de nada. Copiás el archivo al servidor y anda. Sin intérprete, sin bibliotecas del sistema, sin entorno virtual.