Depurando ARM com GDB no arm-box: código instrução a instrução
No artigo anterior a gente somou um vetor no arm-box e conferiu o resultado pelo exit code. Funcionou — mas foi um pouco "caixa preta": rodou, deu 24, e pronto. E se o laço estivesse errado? Como a gente vê o que o processador faz a cada instrução? Resposta: o arm-box agora fala GDB. Neste texto você vai dar um step em código ARM de verdade e assistir os registradores mudarem na sua frente.
O que é "o arm-box falar GDB"
Quando você passa a flag --gdb=PORT, o arm-box não executa o binário sozinho. Ele abre um stub GDB remote serial e trava esperando um cliente se conectar. A partir daí, quem manda é o seu gdb: ele pede registradores, lê e escreve memória, coloca breakpoints no PC, watchpoints de escrita, e manda step ou continue. Por baixo, o stub é o GdbServer/Gdb64Server do arm-jitter — o mesmo que o emulador usa para depuração interna.
O detalhe que faz isso ser ótimo para aprender: cada passo de depuração usa o interpretador puro do arm-jitter (ArmCore#step() / Ir64BlockExecutor#step()), nunca o JIT. Ou seja, o que você vê no GDB é a semântica da instrução instrução a instrução, idêntica ao hardware — não uma otimização do compilador JIT. É a ferramenta perfeita para "explicar o código": você não só descobre que está errado, mas onde e por quê.
Mão na massa: subindo o stub
O arm-box é um projeto Java (Maven). Build e execução com a porta de depuração (exemplo com o hello.elf do primeiro artigo):
mvn package
java -jar target/armbox-*.jar --gdb=3333 testdata/hello.elf Ele imprime algo e para, esperando o GDB. Em outro terminal, conectamos o cliente (aqui usando a toolchain arm-none-eabi, da mesma forma que compilamos os exemplos):
arm-none-eabi-gdb testdata/hello.elf -ex "target remote :3333"Funciona tanto em 32-bit quanto em AArch64 — basta trocar a toolchain e a flag de arquitetura:
java -jar target/armbox-*.jar --arch=aarch64 --gdb=3333 testdata/hello-aarch64.elf
aarch64-none-elf-gdb testdata/hello-aarch64.elf -ex "target remote :3333"Comandos básicos de GDB (cola rápida)
Antes de mergulhar no exemplo, uma tabela de consulta. O comando de inspeção de memória merece atenção: x/<N><formato><tamanho> <endereço> — por exemplo, x/4xw $r0 lê 4 palavras (w, 32 bits) em hex (x) a partir do endereço em r0. Troque o 4 pela quantidade e o w por b (byte), h (halfword) ou g (8 bytes) conforme o dado.
| Comando | O que faz |
|---|---|
break loop / b loop | Define um breakpoint no rótulo (ou em *0x...). |
continue / c | Roda até o próximo breakpoint ou o fim do programa. |
stepi / si | Executa uma instrução de máquina (o nosso "microscópio"). |
info registers / i r | Mostra os registradores (i r r0 r1 filtra para os que interessam). |
info registers cpsr / p $cpsr | Mostra o CPSR (registrador de status), com as flags N, Z, C, V que o bge/blt consultam. p/t $cpsr mostra em binário, bit a bit. |
x/4xw $r0 | Inspeciona 4 palavras em hex a partir de r0 (ver sintaxe acima). |
watch *endereco | Watchpoint de escrita: para quando aquela memória muda. |
print $r1 / p $r1 | Imprime o valor de r1 (ou de qualquer expressão). |
disassemble / disas | Mostra o código desmontado ao redor do PC. |
Exemplo: depurando o laço de soma
Vamos usar o sum.elf do artigo de carga e armazenamento. A gente sabe que ele soma 3,7,1,9,4 e termina com r1 = 24. Mas vamos ver acontecer. Depois de conectar, posicione um breakpoint no laço e mande rodar até lá:
(gdb) break loop
(gdb) continue
Continuing.
Breakpoint 1, loop () at sum.s:84
(gdb) info registers r0 r1 r2 r3
r0 0x... nums @ ponteiro para o vetor
r1 0x0 @ soma acumulada = 0
r2 0x0 @ i = 0
r3 0x5 @ len = 5 Agora o pulo do gato: stepi (ou si) avança uma instrução de máquina. Repare como o ldr r4, [r0], #4 carrega o primeiro elemento e empurra o ponteiro, e como o add r1, r1, r4 acumula:
(gdb) si @ ldr r4, [r0], #4 -> r4 = 3, r0 avança 4 bytes
(gdb) si @ add r1, r1, r4 -> r1 = 3
(gdb) si @ add r2, r2, #1 -> r2 = 1
(gdb) si @ b loop
(gdb) si @ cmp r2, r3
(gdb) si @ ldr r4, [r0], #4 -> r4 = 7
(gdb) si @ add r1, r1, r4 -> r1 = 10
(gdb) info registers r1 r2
r1 0xa @ 3 + 7 = 10
r2 0x2 @ i = 2 Repita e você verá r1 virar 0xb (11), 0x14 (20) e finalmente 0x18 (24) antes do bge done sair do laço. É exatamente a álgebra do while ganhando vida — e se você tivesse escrito r1, r4, r1 por engano, veria a soma diminuir e pegaria o bug na hora, sem precisar adivinhar.
Watchpoints: o "porquê" sem vasculhar memória
Além de breakpoints, o stub aceita watchpoints de escrita. Quer saber exatamente quando a soma é jogada de volta para a memória (nosso str r1, [r5])? Coloque um watch no endereço de result:
(gdb) watch *result
(gdb) continue
Continuing.
Hardware watchpoint 2: *result
Old value: 0
New value: 24
done () at sum.s:93 Outro detalhe de robustez: se você pedir para ler/escrever um endereço fora da faixa mapeada do guest (por exemplo, um x/10xw — "examine 10 palavras em hex" — apontando para fora da região mapeada), o stub responde com erro ao GDB em vez de derrubar o processo — então você pode continuar depurando em vez de ter que recomeçar do zero.
Por que isso ajuda a entender código ARM
Ler assembly é uma coisa; ver a máquina executar é outra. Com o GDB no arm-box você:
- Confere o writeback do
[r0], #4acontecendo de verdade, não só na teoria. - Vê as flags (N, Z, C, V) mudando no
cpsre entende por que obgedesvia (ou não) — useinfo registers cpsr(oup/t $cpsrpara ver em binário) logo após ocmpe confira o bit Z virar 1 quandor2 == r3, fazendo o laço terminar. - Usa
x/4xw $r0para inspecionar a memória que o ponteiro está apontando, ligando endereço a dado. - Compara o caminho do laço com o que você desenhou no papel — ótimo para validar o que foi dito nos fundamentos da arquitetura.
Onde ainda não funciona
A depuração remota está disponível em --machine=linux-user, tanto 32-bit quanto --arch=aarch64. Ainda não está habilitada no modo --machine=cortex-m (bare-metal) — lá o foco hoje é rodar o binário, não depurá-lo passo a passo. Para o curso, isso é o que importa: quase todo exemplo que escrevemos aqui é user-mode ARMv5–ARMv7.
Próximo passo
Agora que você tem um "microscópio" para código ARM, o exercício natural é quebrar de propósito: troque um add por sub, esqueça o #4 do writeback, ou passe len errado e veja o que o GDB revela. No próximo texto abrimos o objdump do sum.elf e decodificamos as instruções ARM à mão, bit a bit — exatamente a leitura que o decodificador do arm-jitter faz. Até lá, divirta-se depurando!