Programação Por Equipe DevBras

Capa do artigo: Quer aprender baixo nível? Não comece escrevendo um sistema operacional

Quer aprender baixo nível? Não comece escrevendo um sistema operacional

Um caminho em quatro etapas, de ponteiros em C ao setor de boot no QEMU com depurador, para aprender como o computador funciona sem travar na tela preta.

Domingo, 23h. O repositório "meu-so" tem três arquivos copiados de um tutorial, o QEMU mostra uma tela preta e não há mensagem de erro, porque ainda não existe nada capaz de imprimir uma mensagem de erro. Essa cena é o fim de quase todo projeto de "criar um sistema operacional no fim de semana". A vontade de entender o que acontece antes do seu programa é legítima; o atalho é que está errado.

A resposta curta: aprenda em camadas, cada uma com uma ferramenta que mostra o que deu errado. Primeiro ponteiros e memória em C com depurador, depois o sistema visto de fora do kernel com strace e /proc, depois um setor de boot de 512 bytes numa máquina virtual com GDB conectado, e só então um curso estruturado de sistemas operacionais. Nunca faça experimento de boot no disco da máquina em que você trabalha.

Por que "criar um SO" trava quem está começando

Escrever um kernel mínimo junta, numa semana, assuntos que normalmente ocupam disciplinas inteiras: linguagem de montagem, o ligador e o formato do executável, o modo real e o modo protegido do processador, o formato do disco, interrupções e acesso ao vídeo. Cada um tem sua forma particular de falhar. Juntos, falham ao mesmo tempo e em silêncio.

No desenvolvimento comum, quando algo quebra, você tem uma pilha de chamadas, um log ou ao menos um printf. No boot, antes de você escrever o código que imprime, não há nada disso. Uma instrução errada reinicia a máquina virtual em loop ou congela a tela. Sem método de diagnóstico, cada tentativa vira palpite, e palpite em sequência é a receita da desistência.

O caminho abaixo resolve isso dando a cada etapa um instrumento de observação. Vamos acompanhar o Rafael, que trabalha com PHP e quer entender o que existe embaixo do servidor.

Etapa 1: memória e ponteiros em C, com depurador

Todo o resto depende de você enxergar a memória como uma sequência de endereços. O primeiro programa do Rafael imprime o endereço e o valor de cada posição de um vetor:

#include <stdio.h>

int main(void) {
    int numeros[4] = {10, 20, 30, 40};
    int *p = numeros;

    for (int i = 0; i < 4; i++) {
        printf("%p -> %d\n", (void *)(p + i), *(p + i));
    }
    return 0;
}
gcc -g -O0 -Wall ponteiros.c -o ponteiros
./ponteiros
gdb ./ponteiros

A saída mostra endereços que avançam de 4 em 4 bytes, o tamanho de um int na maioria das plataformas atuais. No GDB, use break main, run, next e print *p para andar linha a linha. A flag -g inclui as informações de depuração e -O0 evita que o compilador reorganize o código e confunda a leitura.

O objetivo da etapa é sair confortável com três coisas: ler um endereço, entender o que é a pilha e reconhecer um acesso fora do vetor. Quando um Segmentation fault aparecer, você já sabe abrir o GDB e pedir backtrace.

Etapa 2: o sistema operacional visto de fora

Antes de escrever um kernel, observe um funcionando. Em Linux, o seu programa conversa com o kernel por chamadas de sistema, e você pode ver cada uma:

strace -e trace=openat,read,write ./ponteiros
cat /proc/self/maps

O strace mostra o processo pedindo ao kernel para abrir bibliotecas e escrever na saída padrão. O /proc/self/maps mostra como a memória de um processo é dividida: código, heap, pilha, bibliotecas. É o mapa que o kernel monta para cada programa, e vê-lo pronto ajuda muito a entender, mais tarde, por que você precisa montá-lo à mão.

Etapa 3: um setor de boot de 512 bytes no QEMU

Agora sim, código que roda sem sistema operacional. Na inicialização por BIOS legado, o firmware carrega os primeiros 512 bytes do disco no endereço 0x7C00 e começa a executar ali, desde que os dois últimos bytes sejam a assinatura 0x55 0xAA. O programa abaixo, em NASM, imprime uma frase usando o serviço de vídeo da BIOS:

bits 16
org 0x7c00

inicio:
    xor ax, ax
    mov ds, ax
    cld
    mov si, mensagem
.proximo:
    lodsb
    or al, al
    jz .parar
    mov ah, 0x0e
    int 0x10
    jmp .proximo
.parar:
    cli
    hlt
    jmp .parar

mensagem db 'Ola do setor de boot', 0

times 510-($-$$) db 0
dw 0xaa55

Siga estes passos, sempre dentro da máquina virtual:

  1. Monte o binário com nasm -f bin boot.asm -o boot.bin.
  2. Confira o tamanho com wc -c boot.bin. Tem de dar exatamente 512.
  3. Rode com qemu-system-x86_64 -drive format=raw,file=boot.bin. A frase deve aparecer na janela do QEMU.
  4. Se a tela ficar preta, rode de novo com -s -S no fim do comando. O -S congela o processador no início e o -s abre o servidor do GDB na porta 1234.
  5. Em outro terminal, abra gdb e digite target remote :1234, set architecture i8086, break *0x7c00 e continue. Depois, stepi avança uma instrução por vez e info registers mostra o estado.

Esse é o passo que falta nos tutoriais de fim de semana: um jeito de ver o que o processador está fazendo quando a tela não mostra nada. Com o GDB conectado, a tela preta deixa de ser mistério e vira "parou na instrução tal com o registrador tal".

Repare que o arquivo é carregado como disco virtual. Nunca grave esse binário com dd num pendrive ou disco sem conferir três vezes o dispositivo de destino. Gravar no disco errado apaga a tabela de partição da sua máquina de trabalho, e aí o problema vira o do guia de kernel panic que não monta a raiz.

Etapa 4: um curso com começo, meio e fim

Com as três etapas feitas, um curso estruturado passa a render. Dois caminhos conhecidos e gratuitos: o curso de sistemas operacionais do MIT, que usa o xv6, um sistema pequeno no estilo Unix feito para ser lido, e o wiki do OSDev, cujos tutoriais introdutórios mostram como chegar a um kernel mínimo carregado por um bootloader como o GRUB. A vantagem do xv6 é que você estuda um sistema que já funciona e altera partes dele, em vez de partir do zero.

Ao longo do curso, mantenha a regra das etapas anteriores: cada mudança é testada no QEMU, com o GDB à mão. Se em algum momento você for mexer no bootloader da sua máquina real, leia antes o guia de personalizar o GRUB sem quebrar o boot.

Erros comuns

  • Começar pelo kernel antes de dominar ponteiros. Metade dos travamentos de boot é endereço errado, e quem não lê endereço não diagnostica.
  • Copiar tutorial sem entender o porquê de cada linha. Quando algo muda (outro montador, outra versão do QEMU), você não sabe o que adaptar.
  • Testar em hardware real cedo demais. Sem depurador, cada falha é uma reinicialização e nenhuma pista.
  • Usar dd com o dispositivo errado. O comando não pergunta nada e apaga o disco que você indicou.
  • Ignorar o tamanho do setor. Um boot.bin com 511 ou 513 bytes não tem a assinatura no lugar e o firmware simplesmente não o executa.
  • Querer UEFI, modo protegido e sistema de arquivos no mesmo fim de semana. Cada um é um projeto; junte só depois que o anterior funciona.

Checklist

  • Você consegue explicar, olhando a saída do primeiro programa, por que os endereços avançam de 4 em 4.
  • Você já usou backtrace no GDB para achar a origem de um Segmentation fault.
  • Você leu a saída do strace de um programa seu e reconheceu as chamadas.
  • O boot.bin tem 512 bytes e termina em 55 AA.
  • O setor de boot imprime a frase no QEMU.
  • Você parou o QEMU em 0x7c00 com o GDB e avançou instrução por instrução.
  • Nenhum experimento foi gravado no disco da sua máquina de trabalho.

Rafael levou duas semanas para chegar à frase na tela, e dessa vez sabia explicar cada byte. Se a sua máquina virtual mostrar tela preta hoje, qual comando você usaria para descobrir em que instrução o processador parou?

Avalie este artigo

Referência: http://www.vivaolinux.com.br/artigo/Criando-um-sistema-operacional-com-ASM-e-C