Sabe aquela curiosidade de entender como as coisas realmente funcionam por baixo do capô? Pois é. Ultimamente, tenho mergulhado no mundo das MicroVMs e do Firecracker. A promessa é linda: subir máquinas virtuais em milissegundos. Se você já esperou uma VM subir pra testar um script rápido, sabe a dor.
Mas aí eu olhei para as distros “leves” que a gente usa no dia a dia. O Alpine Linux, que é o queridinho do Docker, tem uma imagem base de uns 5MB. O Kernel padrão do Ubuntu? Passa fácil de 12MB só o binário, carregando driver até para joystick de 1990.
Eu pensei: “Dá pra ser menor. Dá pra ser muito menor.”
Foi assim que nasceu o Projeto Osso Linux.
Por que “Osso“? Porque a ideia era deixar o sistema “no osso”. Sem gordura, sem pele, apenas a estrutura para sustentar o sistema a respirar e falar com a rede.
O que você vai ver neste artigo:
- Objetivo: criar um sistema operacional funcional
- Restrição: menos de 5MB no total
- Resultado: 2.7MB de Kernel + 300KB de Userland
O combo foi:
- Kernel: Linux 6.18.1
- Userland: Toybox 0.8.13
Spoiler: eu consegui. Mas o caminho foi cheio de pedras… e a primeira delas foi o meu próprio hardware.
Pré-requisitos
Antes de começar, você vai precisar de:
qemu-system-x86_64(para rodar a MicroVM)musl-gcc(para compilação estática)- Toolchain padrão de compilação (
gcc,make,flex,bison,libelf-dev) - Fonte do Kernel Linux e do Toybox
Organizando o workspace
Antes de meter a mão na massa, vamos criar uma estrutura de diretórios que faça sentido. Isso vai evitar aquela bagunça de arquivos espalhados e facilitar os comandos ao longo do artigo.
mkdir -p ~/osso-linux/{kernel,userland/kernel-headers,rootfs/{bin,dev,proc,sys}}
cd ~/osso-linux
A estrutura final vai ficar assim:
~/osso-linux/
kernel/
linux-6.18.1/ # fonte do kernel (extrair aqui)
userland/
kernel-headers/ # headers gerados pelo kernel
toybox-0.8.13/ # fonte do toybox (extrair aqui)
rootfs/
bin/ # toybox + links simbólicos
dev/ # vazio (mdev popula no boot)
proc/ # mount point
sys/ # mount point
init # script de boot
initramfs.cpio.gz # imagem final (gerada no fim)
Agora baixe e extraia as fontes nos lugares certos:
# Kernel
cd ~/osso-linux/kernel
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.18.1.tar.xz
tar xf linux-6.18.1.tar.xz
cd ..
# Toybox
cd ~/osso-linux/userland
wget http://landley.net/toybox/downloads/toybox-0.8.13.tar.gz
tar xf toybox-0.8.13.tar.gz
cd ..
Pronto. Agora todos os caminhos relativos que vou usar no artigo vão fazer sentido.
O drama do Snapdragon X (ou: onde começa a dor)
Comecei o projeto super empolgado na minha máquina com Snapdragon X, arquitetura ARM64 (aarch64). A ideia era fazer cross-compilation: criar um binário x86_64 dentro do ARM para rodar nas minhas MicroVMs.
Parecia uma boa ideia na teoria. Na prática? O compilador olhou para mim e riu.
Quando tentei forçar a arquitetura alvo no GCC:
make ARCH=x86_64 -j$(nproc) bzImage
tomei este erro na cara:
gcc: error: unrecognized command-line option '-m64'
Problema: O GCC do meu ambiente ARM não sabia o que fazer com a flag -m64 (que instrui a gerar código 64-bit Intel/AMD).
Por que acontece: Eu teria que baixar um toolchain cruzado completo (algo como x86_64-linux-gnu-gcc), configurar sysroots e brigar com bibliotecas dinâmicas.
Como resolvi: Cada tentativa de compilar o Kernel virava uma sessão de debug de 2 horas. Eu estava lutando contra a ferramenta, não contra o problema.
Às vezes, a maturidade na engenharia é saber a hora de parar de dar murro em ponta de faca. Peguei uma máquina x86 nativa e… foi. Tudo o que levei dois dias tentando no ARM fluiu em 15 minutos.
Lição: Se você quer aprender Kernel, elimine as variáveis do ambiente primeiro.
Passo 1: A dieta radical do kernel
Com a máquina certa na mão, entrei no diretório do Kernel 6.18.1.
cd ~/osso-linux/kernel/linux-6.18.1
O segredo para um kernel minúsculo não é o que você coloca, é o que você tira.
# Limpando tudo. E quando digo tudo, é tudo mesmo.
make allnoconfig
Esse comando desliga tudo. Sem suporte a disquete, sem USB, sem som, sem filesystem em disco (só initramfs na RAM). O Kernel vira um peso de papel glorificado.
Para dar vida a ele, ativei cirurgicamente apenas o necessário via make menuconfig:
64-bit kernel(Óbvio)Kernel support for ELF binaries(Para rodar programas)PrintkeSerial drivers(Para ver logs no terminal)PCI supporteVirtio network driver(Para a rede funcionar no QEMU)Networking support->TCP/IP(Porque um PC sem rede é uma calculadora cara)
O .config completo está no repositório do projeto. Use como base.
Compilei e torci pra não explodir:
make -j$(nproc) bzImage
Resultado:
$ ls -alh arch/x86/boot/bzImage
-rw-r--r--. 1 dklima dklima 2.7M Dec 14 08:47 arch/x86/boot/bzImage
2.7 Mega. Começamos bem.
Passo 2: O userland (por que Toybox?)
O Kernel é o cérebro, mas ele precisa de braços. Precisamos de um shell, de um ls, de um ping.
Para o userland, escolhi o Toybox 0.8.13. Vamos voltar para a raiz do projeto e entrar no diretório dele:
cd ~/osso-linux/userland/toybox-0.8.13
“Mas por que não o BusyBox?”
O Toybox é mais moderno, o código é incrivelmente limpo e é a base oficial do Android hoje em dia. Além disso, ele é licenciado como BSD 0-Clause, o que facilita reaproveitar e embutir sem dor de cabeça com GPL.
Para manter o minimalismo, decidi compilar estaticamente usando Musl Libc. A Glibc padrão é “gorda” demais para o Osso Linux.
Foi aqui que o bicho pegou de novo.
Ao tentar compilar com suporte a rede, o compilador explodiu:
toys/pending/route.c:47:10: fatal error: linux/rtnetlink.h: No such file or directory
47 | #include <linux/rtnetlink.h>
| ^~~~~~~~~~~~~~~~~~~
compilation terminated.
make: *** [Makefile:18: toybox] Error 1
Problema: O compilador musl-gcc é isolado. Ele não olha (e nem deve olhar) para os headers (/usr/include) do meu Fedora para não misturar versões.
Por que acontece: Como o comando route precisa falar baixo nível com o kernel, ele não achava o arquivo rtnetlink.h.
A solução na unha:
Tive que gerar os headers “sanitizados” do meu próprio Kernel 6.18 e apontar o compilador para lá na mão.
-
Primeiro, volte ao diretório do Kernel e gere os headers:
cd ~/osso-linux/kernel/linux-6.18.1 make headers_install INSTALL_HDR_PATH=~/osso-linux/userland/kernel-headers -
Agora volte ao diretório do Toybox e compile apontando para esses headers:
cd ~/osso-linux/userland/toybox-0.8.13 LDFLAGS="--static" \ CFLAGS="-I../kernel-headers/include" \ make CC=musl-gcc -j$(nproc) toybox
Compilou! Tamanho final do binário com Shell, Ping, Route e Ifconfig? ~300 KB.
Passo 3: O “glitch” da instalação
Achei que estava pronto, mas quando tentei rodar o comando de instalação automática do Toybox…
$ ./toybox --install
toybox: Unknown command --install
Como usei uma configuração minimalista, esqueci de habilitar o próprio comando de instalação dentro do binário. Ele sabia ser ls, sabia ser sh, mas não sabia se auto-instalar.
A solução foi fazer na unha. Primeiro, copiei o binário para o rootfs/bin e criei os links simbólicos manualmente:
# Copiar o toybox para o rootfs
cp toybox ~/osso-linux/rootfs/bin/
# Entrar no diretório bin do rootfs
cd ~/osso-linux/rootfs/bin
# Criar os links simbólicos
for cmd in cat clear cp echo ls mdev mkdir mount mv ps rm sh toysh umount uname ifconfig route ping; do
ln -sf toybox $cmd
done
Passo 4: O script de boot (Init)
Sem SystemD. Sem Scripts complexos.
Volte para o diretório rootfs e crie o arquivo init. Esse script shell simples monta o sistema virtual, sobe a rede e pinga o Google.
cd ~/osso-linux/rootfs
Crie o arquivo init com o seguinte conteúdo:
#!/bin/sh
/bin/mount -t proc none /proc
/bin/mount -t sysfs none /sys
/bin/mdev -s
/bin/clear
echo ""
echo " OSSO LINUX (Net Edition)"
echo "------------------------------"
echo "Kernel: 6.18.1"
echo "[1/3] Configurando IP..."
/bin/ifconfig eth0 10.0.2.15 netmask 255.255.255.0 up
echo "[2/3] Definindo rota padrão..."
/bin/route add default gw 10.0.2.2
echo "[3/3] Fazendo ping para 8.8.8.8..."
if /bin/ping -c 3 -W 2 8.8.8.8; then
echo "SUCESSO: ONLINE!"
else
echo "FALHA: Sem conexão."
fi
exec /bin/sh
Não esqueça de torná-lo executável:
chmod +x init
Os IPs 10.0.2.15 e 10.0.2.2 são do user-mode networking do QEMU. Se você usar bridge ou tap, vai precisar ajustar.
A hora da verdade
Para que o Kernel consiga carregar nosso sistema de arquivos para a RAM, precisamos transformar o diretório rootfs em um arquivo compactado no formato CPIO (initramfs).
Nesse ponto, seu rootfs deve ter o bin/ populado com o toybox e os links simbólicos, e o init executável na raiz. Hora de empacotar:
# Ainda dentro do diretório rootfs
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
Com o pacote initramfs.cpio.gz gerado na raiz do projeto, volte para lá e rode o QEMU, ativando a virtualização KVM (para velocidade) e a placa de rede Virtio:
cd ..
qemu-system-x86_64 \
-kernel kernel/linux-6.18.1/arch/x86/boot/bzImage \
-initrd initramfs.cpio.gz \
-nographic \
-append "console=ttyS0 quiet" \
-enable-kvm \
-m 512 \
-netdev user,id=n1 \
-device virtio-net-pci,netdev=n1
Se você não tiver KVM disponível (WSL, VM sem nested virtualization, BIOS desabilitada), remova o -enable-kvm. Vai rodar mais lento, mas funciona.
O boot foi instantâneo. E o resultado?
OSSO LINUX (Net Edition)
------------------------------
Kernel: 6.18.1
[1/3] Configurando IP (eth0)...
[2/3] Definindo rota padrão...
[3/3] Fazendo ping para 8.8.8.8...
Ping 8.8.8.8 (8.8.8.8): 56(84) bytes.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=255 time=6 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=255 time=6 ms
SUCESSO: ONLINE!
GitHub
Os arquivos desse projeto estão disponíveis no GitHub.
O que vem a seguir?
Eu não criei o Osso Linux apenas para pingar o Google. O objetivo final é ter um ambiente de execução para microsserviços ultra-rápidos.
Agora que temos um sistema operacional de 3MB que conecta na rede, a pergunta é: será que ele aguenta rodar um banco de dados de verdade?
No próximo artigo, vou tentar o impossível: compilar e rodar o MariaDB dentro desse kernel minimalista.
As métricas que vou pegar:
- Boot time: quanto tempo do QEMU até o MariaDB aceitar conexões
- RSS: consumo de memória residente do processo
- Tamanho final: rootfs completo com o banco
Será que o “Osso” aguenta o tranco ou vai quebrar?
Até a próxima!





Deixe um comentário