Entendiendo lo que realmente ocurre cuando corrés un programa en Linux

Cuando ejecutás un programa en Linux con un simple ./app, todo parece inmediato, casi trivial. Escribís el comando, presionás Enter y en cuestión de milisegundos tenés un proceso corriendo. Pero esa aparente simplicidad esconde una secuencia compleja de eventos profundamente coordinados entre el sistema operativo, la memoria, el kernel y el propio binario.

Muchas veces perdemos de vista lo que realmente sucede en el nivel más fundamental. Sin embargo, entender ese “debajo” no solo es interesante: es clave para depurar, optimizar y asegurar sistemas.

Este artículo busca desentrañar ese recorrido: desde que ejecutás un binario hasta que el programa empieza a correr. Vamos a explorar cómo funciona el formato ELF, cómo interviene el kernel, qué rol cumplen las system calls y por qué el dynamic linking es tan importante.

El punto de partida: ejecutar un programa

Supongamos el caso más clásico posible:

#include <stdio.h>
int main() {
printf(“Hola mundo\n”);
return 0;
}

Compilás:
gcc hola.c -o hola

Y ejecutás:
./hola

Resultado:
Hola mundo

Hasta acá, todo parece simple. Pero en realidad, el shell no “ejecuta” directamente ese programa. Lo que hace es invocar una llamada al sistema: execve.

fork y execve: el nacimiento de un proceso

Antes de ejecutar un programa, el shell realiza dos pasos fundamentales:

  1. fork(): crea un nuevo proceso (hijo)
  2. execve(): reemplaza ese proceso con el nuevo programa

Esto implica algo clave: el proceso que ejecuta tu programa no es el shell, sino una copia del mismo que luego se transforma completamente.

Ejemplo práctico

Si corrés:

ps -f

Vas a ver algo como:

bash ── hola

Eso significa que hola es un proceso hijo del shell.

Qué hace fork realmente

fork() no crea un proceso desde cero, sino una copia del proceso actual. Pero no duplica toda la memoria inmediatamente gracias a un mecanismo llamado copy-on-write.

Esto significa que padre e hijo comparten memoria hasta que uno de los dos la modifica.

El rol del kernel

En Linux, todo está dividido en dos espacios:

  • User Space: donde corren tus programas
  • Kernel Space: donde vive el kernel

Los programas no pueden acceder directamente al hardware. Necesitan pedirle al kernel que haga cosas por ellos.

Ahí entran las system calls.

System calls: la puerta al kernel

Una system call es la forma en que un programa le dice al kernel:

  • “abrí este archivo”
  • “escribí en pantalla”
  • “ejecutá este binario”

Por ejemplo, cuando tu programa imprime algo, no escribe directamente en la pantalla. Hace una syscall:

write(1, “Hola mundo\n”, 11);

Donde:

  • 1 es stdout
  • el segundo parámetro es el buffer
  • el tercero es la cantidad de bytes

Ejemplo práctico: evitar printf

#include <unistd.h>

int main() {
write(1, “Hola directo al kernel\n”, 23);
return 0;
}

Este programa no usa printf, sino que habla directamente con el kernel.

ELF: el formato del binario

Cuando compilás un programa, el resultado no es simplemente “código máquina”. Es un archivo estructurado bajo el formato ELF (Executable and Linkable Format).

Este formato define cómo debe cargarse y ejecutarse el programa.

Componentes principales

Un archivo ELF tiene tres partes clave:

  • ELF Header: identifica el archivo
  • Program Headers: indican cómo cargarlo en memoria
  • Section Headers: organizan el contenido lógico

El ELF Header: la identidad del archivo

Los primeros bytes del archivo contienen un identificador único:

7f 45 4c 46

Esto corresponde a:

\x7f ELF

Es lo que le dice al sistema: “esto es un ejecutable ELF”.

Ejemplo práctico

Podés verificarlo con:

file ./hola

Salida típica:

ELF 64-bit LSB executable

Segmentos: cómo se carga en memoria

El kernel no carga todo el archivo en memoria. Solo carga los segmentos necesarios.

Los más importantes son:

  • LOAD: código y datos
  • INTERP: el intérprete (dynamic linker)
  • DYNAMIC: información de librerías

Dynamic Linking: el binario no está solo

La mayoría de los programas en Linux no son autosuficientes. Dependen de librerías externas como libc.

Esto se llama linking dinámico.

En lugar de incluir todo el código en el binario, se carga en tiempo de ejecución.

Ejemplo práctico

ldd ./hola

Salida:

libc.so.6 => /lib/libc.so.6

Esto significa que tu programa necesita libc para funcionar.

El dynamic linker

Cuando el kernel detecta que el binario necesita librerías, no ejecuta directamente el programa.

Primero ejecuta un intermediario: el dynamic linker.

Este se encarga de:

  • cargar librerías
  • resolver símbolos
  • preparar el entorno

Recién después de eso se ejecuta tu main().

Secciones: la organización interna

Dentro del ELF, el contenido se organiza en secciones:

  • .text: código
  • .data: variables inicializadas
  • .bss: variables no inicializadas
  • .rodata: constantes

Ejemplo práctico

readelf -S ./hola

Esto te muestra todas las secciones.

Memoria virtual vs memoria real

Cada proceso tiene su propio espacio de memoria virtual.

Pero no toda esa memoria está realmente en RAM.

El uso real se mide con:

  • RSS (Resident Set Size)

Ejemplo práctico

ps -o pid,rss,vsz,cmd
  • VSZ: memoria virtual
  • RSS: memoria real

/proc: ver procesos como archivos

En Linux, todo es un archivo. Incluso los procesos.

Cada proceso tiene un directorio:

/proc/<PID>

Ahí podés ver:

  • memoria
  • file descriptors
  • estado del proceso

Ejemplo práctico

ls /proc/$$/fd

Esto muestra los descriptores abiertos.

File descriptors: entradas y salidas

Todo proceso tiene tres descriptores por defecto:

  • 0: stdin
  • 1: stdout
  • 2: stderr

Ejemplo práctico

Redirigir salida:

./hola > salida.txt

Acá stdout deja de ser la terminal y pasa a ser un archivo.

strace: viendo la “conversación” con el kernel

strace permite ver las system calls en tiempo real.

Ejemplo práctico

strace ./hola

Vas a ver cosas como:

  • execve
  • mmap
  • openat
  • write

Esto muestra todo lo que ocurre antes de que veas el output.

Flujo completo de ejecución

Resumiendo todo el proceso:

  1. Escribís ./hola
  2. El shell hace fork()
  3. El hijo ejecuta execve()
  4. El kernel lee el ELF
  5. Verifica el header
  6. Carga los segmentos en memoria
  7. Invoca el dynamic linker
  8. Se cargan las librerías
  9. Se resuelven símbolos
  10. Se ejecuta el programa

Todo esto ocurre en milisegundos.

Ejemplo práctico completo

Imaginá un servidor que ejecuta múltiples procesos.

Cada vez que hacés:

curl http://localhost

Pasa exactamente este flujo:

  • se crea un proceso
  • se carga un binario ELF
  • se resuelven librerías
  • se ejecuta código
  • se hacen syscalls de red

Entender esto te permite:

  • debuggear fallas
  • analizar performance
  • detectar comportamientos sospechosos

Detrás de cada ejecución en Linux hay una coreografía precisa entre el usuario, el kernel y el binario. Lo que parece inmediato es en realidad el resultado de décadas de diseño en sistemas operativos.

Entender ELF, las system calls, el rol del kernel y el dynamic linking no es solo conocimiento teórico. Es una herramienta poderosa para cualquier persona que trabaje con sistemas, seguridad o desarrollo.

Cuando algo falla, cuando un programa no arranca o cuando el rendimiento no es el esperado, muchas veces la respuesta está en estos detalles que normalmente ignoramos.

Y ahí es donde deja de ser “magia” y pasa a ser ingeniería.

Créditos a Facu de la Cruz (tty0 en telegram) que nos basamos en un PDF de su autoría.

Donaciones
STREAMER

Segui Nuestras Redes
  • LinkedIn17.3k+
  • Whatsapp1.7k+
  • TelegramNuevo

Advertisement

Cargando Siguiente Publicación...
Encontranos
Buscar Tendencia
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...

Todos los campos son obligatorios.