← Voltar aos artigosDepurando ARM com GDB no arm-box: código instrução a instrução

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 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 $r04 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.

Principais comandos GDB usados neste artigo
ComandoO que faz
break loop / b loopDefine um breakpoint no rótulo (ou em *0x...).
continue / cRoda até o próximo breakpoint ou o fim do programa.
stepi / siExecuta uma instrução de máquina (o nosso "microscópio").
info registers / i rMostra os registradores (i r r0 r1 filtra para os que interessam).
info registers cpsr / p $cpsrMostra 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 $r0Inspeciona 4 palavras em hex a partir de r0 (ver sintaxe acima).
watch *enderecoWatchpoint de escrita: para quando aquela memória muda.
print $r1 / p $r1Imprime o valor de r1 (ou de qualquer expressão).
disassemble / disasMostra 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], #4 acontecendo de verdade, não só na teoria.
  • Vê as flags (N, Z, C, V) mudando no cpsr e entende por que o bge desvia (ou não) — use info registers cpsr (ou p/t $cpsr para ver em binário) logo após o cmp e confira o bit Z virar 1 quando r2 == r3, fazendo o laço terminar.
  • Usa x/4xw $r0 para 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!