Freestanding: C sin libc
El C que corre sin sistema operativo debajo: -ffreestanding, -nostdlib, escribir tu propio _start y runtime. La base de kernels, firmware y bare metal.
Todo lo que has escrito hasta ahora asumía un sistema operativo y una libc debajo. Pero C puede correr sin nada de eso: en el arranque de un kernel, en un microcontrolador, en firmware. Ese es el entorno freestanding, y entenderlo te lleva al metal desnudo.
- Hosted vs freestanding.
- Qué headers sobreviven sin libc.
-nostdliby el punto de entrada_start.- Cuándo y por qué escribir código sin libc.
Hosted vs freestanding
El estándar C define dos entornos:
Hosted (alojado)
Hay un sistema operativo y la libc completa. Tu programa empieza en main, con printf, malloc, archivos… Es lo que has usado.
Freestanding (autónomo)
No hay SO ni libc completa. Solo unos pocos headers. El programa arranca donde tú digas. Es el mundo de kernels, bootloaders y firmware.
gcc -ffreestanding -nostdlib ... # dile al compilador que no hay libc hosted
Los headers que sobreviven
En freestanding, casi toda la librería estándar desaparece. Solo quedan los headers que no dependen del sistema — los que definen tipos y límites, no funciones:
#include <stddef.h> // size_t, nullptr_t, offsetof
#include <stdint.h> // enteros de ancho fijo
#include <stdbool.h> // bool (aunque en C23 es palabra clave)
#include <stdarg.h> // argumentos variádicos
#include <limits.h> // límites de los tipos
#include <stdalign.h> // alineación
Nada de stdio.h, stdlib.h ni string.h: si quieres printf o malloc, los implementas tú (o hablas directo con el hardware).
Sin libc: _start y tu runtime
En un programa normal, antes de main corre código de arranque de la libc (el “crt”: prepara la pila, los argumentos, las variables globales, y al final llama a main). Con -nostdlib ese andamiaje desaparece: el punto de entrada real es _start, y lo escribes tú.
// -nostdlib: sin arranque de la libc; el kernel salta a _start
void _start(void) {
// aquí no hay printf ni exit: solo syscalls directas
const char msg[] = "hola sin libc\n";
// write(1, msg, 14) y exit(0) mediante ensamblador de syscall
__asm__ volatile ("syscall" :: "a"(1), "D"(1), "S"(msg), "d"(14));
__asm__ volatile ("syscall" :: "a"(60), "D"(0)); // exit
__builtin_unreachable();
}
gcc -ffreestanding -nostdlib -static -o hola hola.c
./hola # "hola sin libc" — sin depender de NADA
Este es uno de los momentos más reveladores del track. Sin libc, ves lo que de verdad hay: un punto de entrada, una pila, y syscalls crudas al kernel. printf era una comodidad de decenas de miles de líneas; write es una instrucción syscall con el número correcto en un registro. Este es el nivel en el que trabaja el arranque de un kernel (antes de que exista una libc), el firmware de un dispositivo (donde no hay SO), y el “no_std” de Rust. Entender freestanding desmitifica por completo qué es un programa: no necesita un SO ni una librería para existir — solo código, memoria y una forma de hablar con el hardware o el kernel. Es la base sobre la que se construye el track del kernel de Linux que viene después.
No escribirás _start a mano a diario, pero saber que puedes cambia tu comprensión. Es la base de: kernels (arrancan freestanding y luego montan su propio entorno), firmware y embebido (microcontroladores sin SO), bootloaders, y binarios ultraminimalistas. Cuando en el track de Rust veas #![no_std], o en el del kernel veas que no hay libc, recordarás este nivel: es el mismo mundo, el del código que se sostiene solo.
- Compila un programa con
-ffreestanding -nostdlibque escriba un mensaje con una syscall directa y salga. - Comprueba con
lddque no depende de ninguna librería. - Lista los headers freestanding e identifica por qué esos sí sobreviven (definen tipos, no funciones).
- Reflexiona: ¿qué tendrías que implementar tú para tener un
mallocen este entorno?