Pular para o conteúdo
,

Do Zero ao Ping: O nascimento do “Osso Linux” de 3MB (e por que quase desisti)

Criei o Osso Linux: um OS de 3MB feito do zero (Kernel 6.18 + Toybox). Do erro no ARM ao ping de 6ms. Veja como compilei tudo na unha.

Avatar de DK
DKTrabalha com Linux e Unix a mais de 23 anos e possui as certificações LPI 3, RHCE, AIX e VIO.

14 dez, 2025
9 min de leitura

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:

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)
  • Printk e Serial drivers (Para ver logs no terminal)
  • PCI support e Virtio 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.

  1. 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
  2. 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!

Avatar de DK

Comentários

Deixe um comentário

Seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Ir para