Depurador Rust: como olhar por baixo dos panos step by step
\n\nVocê já se perguntou: «Step by step interactive debugger for Rust: como funciona por baixo dos panos?» Se você escreveu em Rust e tentou capturar um bug complicado, certamente já se deparou com GDB, LLDB ou até mesmo ferramentas como rust-gdb. Mas como exatamente eles conseguem parar a execução do programa, mostrar os valores das variáveis e percorrer o código-fonte?
Neste artigo, não vamos apenas contar como usar o depurador. Vamos olhar para dentro: desde o código de máquina até os pontos de parada, dos quadros de pilha aos símbolos de depuração. Se você tem interesse não apenas no que fazer, mas também no porquê funciona — está no lugar certo.
\n\nPor que Rust é um caso especial para depuração?
\n\nRust é compilado para código nativo através do LLVM. Isso significa que para depuração usamos as mesmas ferramentas que para C/C++: GDB e LLDB. Mas Rust adiciona suas próprias complexidades:
- \n
- Propriedade e empréstimo — variáveis podem ser movidas, e o depurador precisa rastrear isso. \n
- Monomorfização de genéricos — cada
Tse transforma em um tipo concreto, e os nomes das funções ficam «criptografados». \n - Otimizações — o compilador pode reorganizar o código, e o depurador precisa «desfazer» isso. \n
É por isso que o step by step interactive debugger for Rust não é apenas executar o programa sob GDB. É todo um sistema que traduz endereços de máquina de volta para linhas do código-fonte.
\n\nComo funciona um ponto de parada (breakpoint) por baixo dos panos?
\n\nImagine: você coloca um ponto de parada na linha println!("Hello");. O que acontece no processador?
Quando você executa o programa sob o depurador, ele faz o seguinte:
\n\n- \n
- Lê as informações de depuração (formato DWARF) do arquivo executável. Lá está armazenada a correspondência: endereço na memória → linha no código. \n
- Substitui a instrução naquele endereço por um comando especial
INT3(em x86 —0xCC). Isso é uma «armadilha» para o processador. \n - Quando a execução chega a este ponto, o processador gera um sinal SIGTRAP. \n
- O depurador intercepta o sinal, salva o estado dos registradores e mostra a linha atual. \n
Aqui está um exemplo de um programa simples em Rust:
\n\nfn main() {\n let x = 42;\n let y = x + 10; // <-- ponto de parada\n println!("y = {}", y);\n}\n\nQuando você coloca um breakpoint em let y = x + 10;, o depurador encontra o endereço desta instrução no binário e substitui o primeiro byte por 0xCC. Após a parada, ele restaura o byte original, executa um passo e coloca a armadilha novamente — se você quiser continuar.
Step over, step into, step out — como o depurador sabe onde está a «próxima linha»?
\n\nEsta é, talvez, a parte mais mágica. Como o depurador sabe que depois de let y = x + 10; vem println!?
Tudo se resume às tabelas de depuração. O compilador Rust (através do LLVM) gera a seção .debug_line no formato DWARF. Esta é uma tabela compactada que mapeia endereços de instruções para números de linha no código-fonte.
Por exemplo, para o nosso código, a tabela pode ser assim:
\n\nEndereço 0x1000 → linha 2 (let x = 42;)\nEndereço 0x1010 → linha 3 (let y = x + 10;)\nEndereço 0x1020 → linha 4 (println!)\n\nQuando você pressiona step over, o depurador calcula o endereço da próxima instrução que não pertence à linha atual e coloca um ponto de parada temporário lá. A execução continua até este endereço — e você avança para a próxima linha.
\n\nPara step into — o depurador verifica se no endereço atual há uma chamada de função. Se sim, ele procura a primeira linha dessa função