Emulação e Depuração
Desenvolver diretamente contra hardware físico torna até um erro rotineiro caro: uma falha significa um reboot físico, um disco corrompido significa reimageá-lo, e não há forma embutida de pausar a execução e inspecionar o que a CPU está fazendo em um dado instante. Um emulador remove os três custos de uma vez, e combinar um com um depurador em nível de código-fonte transforma o desenvolvimento de kernel de um ciclo de “iniciar, observar uma tela preta, adivinhar” em algo mais próximo de depurar qualquer outra peça de software.
O QEMU é o emulador mais comumente usado para desenvolvimento de kernel, tanto porque inicializa rapidamente e reseta instantaneamente, quanto porque expõe uma interface de depuração construída especificamente para esse caso de uso. Uma imagem de kernel é tipicamente rodada diretamente, se for compatível com Multiboot e o QEMU for invocado com -kernel, ou via uma ISO inicializável construída com um bootloader real como o GRUB:
qemu-system-x86_64 -kernel mykernel.elf -serial stdio-serial stdio redireciona a saída da porta serial do guest diretamente para o terminal do host, transformando um driver serial funcional em saída visível sem cabeamento físico ou configuração de emulador de terminal alguma: uma das razões pelas quais um driver serial frequentemente está entre as primeiríssimas peças funcionais de código de kernel, bem antes de qualquer saída gráfica existir. -no-reboot e -no-shutdown, comumente adicionadas junto durante o desenvolvimento inicial, impedem o QEMU de resetar silenciosamente a máquina virtual em um triple fault da forma que hardware real faria; sem elas, um bug de kernel grave o suficiente para causar um triple fault produz nada além de uma piscada enquanto o QEMU reinicia e roda de novo o mesmo código quebrado, em vez do estado congelado e inspecionável do qual uma sessão de depuração -s -S funcional depende. -d int,cpu_reset vai um passo além, registrando toda exceção e reset de CPU que o QEMU processa no terminal (ou em um arquivo, via -D logfile), o que frequentemente é a forma mais rápida de identificar por que um triple fault aconteceu: uma falha de página enquanto já se tratava uma falha de proteção geral, por exemplo, escala para um double fault, e uma segunda falha enquanto se trata essa é o que dispara o triple fault e o reset, uma sequência que -d int imprime claramente, em contraste com o próprio reset, que não carrega explicação alguma por conta própria.
O QEMU expõe um console monitor separado, distinto da própria saída serial do guest, para inspecionar e controlar a máquina emulada diretamente (info registers para despejar o arquivo completo de registradores, info mem para percorrer as tabelas de página atuais, x/10i $pc para desmontar instruções no program counter), alcançável via Ctrl-A C ao rodar com -serial stdio e -nographic juntos, ou através de seu próprio socket dedicado quando se passa uma flag -monitor separada. Por padrão, o QEMU roda o guest sob o TCG (Tiny Code Generator), um tradutor binário dinâmico que compila instruções do guest para instruções do host em tempo real inteiramente em software; a flag -enable-kvm em vez disso entrega a execução do guest ao suporte de virtualização de hardware do kernel Linux para velocidade quase nativa, mas a algum custo de previsibilidade de depuração: single-stepping em nível de instrução e parte da própria instrumentação do QEMU se comportam de forma menos consistente sob a execução virtualizada por hardware do KVM do que sob a execução em software do TCG, razão pela qual a maioria dos desenvolvedores de kernel deixa o KVM desabilitado especificamente durante sessões ativas de depuração, e só o habilita depois que um kernel está estável o suficiente para que a velocidade bruta de execução importe mais do que a visibilidade passo a passo.
O Bochs é um emulador x86 mais lento, apenas em software (ele interpreta cada instrução diretamente em vez de traduzir blocos de código do guest para código do host da forma que o TCG do QEMU faz, o que é a principal razão da diferença de velocidade), mas que modela comportamento de hardware, incluindo alguns comportamentos indefinidos ou de caso extremo que silício real exibe, com um nível de fidelidade que a implementação mais rápida e mais abstrata do QEMU nem sempre alcança; a emulação incomumente precisa do Bochs de segmentação em modo real e do A20 gate em particular o tornou um pilar da comunidade hobbyist de desenvolvimento de SO bem antes de as próprias facilidades de depuração do QEMU amadurecerem. Ele é configurado através de um arquivo bochsrc em texto puro em vez de flags de linha de comando (nomeando a imagem de disco, o tamanho de memória, e o dispositivo de boot), e inclui seu próprio depurador embutido de baixo nível, invocado ao construir ou instalar um binário do Bochs habilitado para depuração e alcançado através de sua própria sintaxe de comando, não a do GDB: b para definir um breakpoint, s para dar um passo, c para continuar, e info gdtr/info idtr/sreg/creg para inspecionar tabelas de descritor e registradores de controle diretamente, sem precisar de uma ferramenta separada ou conexão remota alguma. Geralmente é usado como complemento ao QEMU, não um substituto completo: QEMU para a maior parte do desenvolvimento rápido e iterativo, Bochs quando um bug é suspeito de depender de timing em nível de hardware ou comportamento de caso extremo que a emulação do QEMU pode não reproduzir fielmente: peculiaridades de interrupção da BIOS em modo real e casos extremos de setor de boot são as áreas onde essa distinção aparece com mais frequência na prática.
Anexando o GDB
Seção intitulada “Anexando o GDB”A flag -s do QEMU abre um stub de depuração remota compatível com GDB na porta TCP 1234 (abreviação de -gdb tcp::1234), e -S adicionalmente pausa a CPU imediatamente na inicialização, em vez de deixá-la começar a executar antes que um depurador tenha tido a chance de se conectar:
qemu-system-x86_64 -kernel mykernel.elf -s -SEm um terminal separado, o GDB se conecta a esse stub como se estivesse depurando um processo remoto, em vez de um rodando localmente:
(gdb) target remote localhost:1234(gdb) symbol-file mykernel.elf(gdb) break kernel_main(gdb) continueCarregar o próprio arquivo ELF do kernel com symbol-file é o que permite ao GDB exibir informação em nível de código-fonte (nomes de função, números de linha, valores de variáveis locais) em vez de endereços brutos; o stub do QEMU fornece o estado de CPU e memória em execução, mas não tem conhecimento independente algum de quais símbolos correspondem a quais endereços sem ser informado explicitamente. O binário gdb de fábrica de um host frequentemente é construído apenas para a própria arquitetura daquele host e se recusa a carregar um arquivo de símbolos de arquitetura estrangeira de forma alguma; o gdb-multiarch, um pacote separado na maioria das distribuições Linux, ou um gdb construído como parte do mesmo toolchain de cross-compiler, suporta arquiteturas-alvo arbitrárias a partir de um único binário e é a escolha mais confiável para trabalho de kernel, particularmente ao ter como alvo qualquer coisa além da própria arquitetura do host.
Breakpoints, execução instrução a instrução (stepi/nexti para stepping em nível de instrução, step/next para stepping em nível de código-fonte uma vez que símbolos de depuração estejam presentes), e inspeção de memória (x/10xg $rsp, por exemplo, examinando dez valores hexadecimais de 64 bits começando no stack pointer atual) tudo funciona exatamente como funcionaria depurando um programa comum de user space, apesar de o “programa” nesse caso ser um sistema operacional inteiro rodando dentro de uma CPU emulada. Um break comum define um breakpoint por software sobrescrevendo temporariamente a instrução-alvo com um opcode int3, o que exige que a página subjacente seja gravável; hbreak em vez disso usa um breakpoint por hardware apoiado em um dos registradores de depuração da CPU (DR0–DR3), que o QEMU emula fielmente, e é a única opção que funciona contra código mapeado como somente leitura, como uma imagem de ROM ou uma página explicitamente marcada como não gravável pelas próprias tabelas de página do kernel. watch alguma_variavel define um watchpoint de hardware correspondente, interrompendo a execução no momento em que a localização de memória nomeada muda, independentemente de qual instrução em qualquer lugar do kernel por acaso a escreve, consideravelmente mais rápido do que percorrer o código manualmente instrução a instrução caçando a escrita, e frequentemente a única forma prática de rastrear corrupção de memória cujo ponto de escrita não é óbvio a partir do valor corrompido sozinho. O GDB também consegue passar comandos diretamente ao monitor do QEMU descrito acima sem sair do depurador, via monitor <comando>: monitor info registers, por exemplo, alcança o próprio despejo de registradores do QEMU de dentro de uma sessão comum do GDB.
Depurando transições de modo
Seção intitulada “Depurando transições de modo”Um ponto forte particular dessa configuração, sem equivalente fácil algum em hardware real, é percorrer diretamente a transição de real para protected mode, ou de protected para long mode, instrução por instrução, observando o estado de registrador e registrador de controle mudar em cada passo: código que de outra forma falharia sem nada mais informativo do que um reboot silencioso em hardware real, em vez disso pausa exatamente onde a falha ocorre, com a própria saída de diagnóstico do emulador frequentemente identificando precisamente qual passo da sequência de transição deu errado. O próprio desmontador do GDB, no entanto, precisa de ajuda exatamente nessa transição: ele infere a largura de instrução da CPU a partir da arquitetura declarada no arquivo ELF carregado, então desmonta corretamente uma vez que um kernel de 32 ou 64 bits está rodando, mas interpreta mal toda instrução executada antes disso, enquanto a CPU ainda está em modo real de 16 bits: set architecture i8086 antes de percorrer instrução a instrução essa porção do boot corrige isso, e precisa ser revertido com set architecture i386 ou set architecture i386:x86-64 de novo assim que a troca de modo de fato se completa, ou a saída de desmontagem subsequente fica igualmente errada na direção oposta.
Automatizando o fluxo de depuração
Seção intitulada “Automatizando o fluxo de depuração”Digitar o mesmo punhado de comandos do GDB depois de cada reinicialização fica tedioso rapidamente, e a maioria dos projetos de kernel os reúne em um arquivo .gdbinit lido automaticamente na inicialização:
target remote localhost:1234symbol-file mykernel.elfbreak kernel_mainO GDB se recusa a carregar automaticamente um arquivo .gdbinit fora de um pequeno conjunto de diretórios confiáveis por padrão, como medida de segurança contra um arquivo malicioso rodando comandos arbitrários silenciosamente quando o GDB por acaso é iniciado a partir de um diretório não confiável; rodar o GDB com -x .gdbinit explicitamente, ou adicionar o diretório do projeto ao ~/.gdbinit via add-auto-load-safe-path, são as duas formas comuns de contornar essa restrição para o próprio script de um projeto de kernel. Um pequeno script de shell ou alvo de Makefile que inicia o QEMU em segundo plano com -s -S e imediatamente inicia gdb -x .gdbinit em primeiro plano reduz o ciclo inteiro a um único comando, o que importa mais do que possa parecer para um fluxo de trabalho repetido dezenas de vezes ao longo da depuração de um único bug de sequência de boot.
Notas de implementação
Seção intitulada “Notas de implementação”Otimizações que um compilador aplica por padrão podem tornar a depuração em nível de código-fonte consideravelmente mais difícil de acompanhar (variáveis otimizadas para registradores em vez de posições de stack, código reordenado em relação ao código-fonte de onde veio), então código de kernel destinado a ser ativamente depurado é comumente construído com otimizações desabilitadas (-O0) e informação de depuração habilitada (-g), pelo menos para builds de desenvolvimento, mesmo que o build de release real de um kernel possa razoavelmente usar otimização mais agressiva uma vez que esteja funcionando corretamente. Também vale lembrar que o QEMU é uma emulação em software da CPU, não idêntica em todo detalhe de timing e caso extremo a um processador real: código que funciona perfeitamente sob o QEMU não tem garantia automática de se comportar de forma idêntica em hardware real, razão pela qual testar em máquinas físicas eventualmente importa, mesmo para um kernel que já foi extensivamente validado sob emulação primeiro. Por fim, uma sessão de depuração deixada conectada durante um triple fault sem -no-reboot produz um sintoma particularmente confuso: o continue do GDB parece ter sucesso, a execução é retomada, e momentos depois todo breakpoint e watchpoint previamente definido silenciosamente para de disparar, não porque foram removidos, mas porque o QEMU reiniciou silenciosamente a máquina virtual inteira por baixo do depurador ainda conectado, e o código em execução agora é o próprio início do boot de novo, não onde a sessão havia chegado anteriormente.
Referências
Seção intitulada “Referências”- ^ Documentação do QEMU, “GDB usage”: a referência para o stub de depuração remota
-s/-Sdo QEMU usado acima. - ^ Documentação do QEMU, “QEMU Monitor”: documenta o console monitor e os comandos referenciados acima, incluindo
info registerseinfo mem. - ^ Manual do Usuário do Bochs, “The Internal Debugger”: documenta os próprios comandos de breakpoint e inspeção de registrador do Bochs.
Ver também
Seção intitulada “Ver também”- Building a Cross-Compiler: produzindo o binário de kernel que este artigo roda e depura.
- Serial Port: o caminho de saída ao qual
-serial stdioconecta o terminal do host. - A Linha A20: a peculiaridade de endereçamento em modo real pela qual a fidelidade do Bochs é particularmente conhecida.
- Registradores de Depuração e Breakpoints de Hardware: os mesmos registradores DR0-DR3 que
hbreak/watchusam, do lado da programação pelo próprio kernel. - Panics de Kernel e Stack Backtraces: o que as ferramentas deste artigo comumente são usadas para inspecionar depois que um ocorre.
- Reiniciando e Desligando: o triple fault que
-no-reboot, mencionada acima, existe especificamente para congelar em vez de resetar silenciosamente.