Como funciona o depurador Rust por baixo dos panos: análise passo a passo

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

Depurador Rust: como olhar por baixo dos panos step by step

\n\n

Você 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?

\n\n

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\n

Por que Rust é um caso especial para depuração?

\n\n

Rust é 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\n
    \n
  • Propriedade e empréstimo — variáveis podem ser movidas, e o depurador precisa rastrear isso.
  • \n
  • Monomorfização de genéricos — cada T se 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
\n\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\n

Como funciona um ponto de parada (breakpoint) por baixo dos panos?

\n\n

Imagine: você coloca um ponto de parada na linha println!("Hello");. O que acontece no processador?

\n\n

Quando você executa o programa sob o depurador, ele faz o seguinte:

\n\n
    \n
  1. 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.
  2. \n
  3. Substitui a instrução naquele endereço por um comando especial INT3 (em x86 — 0xCC). Isso é uma «armadilha» para o processador.
  4. \n
  5. Quando a execução chega a este ponto, o processador gera um sinal SIGTRAP.
  6. \n
  7. O depurador intercepta o sinal, salva o estado dos registradores e mostra a linha atual.
  8. \n
\n\n

Aqui está um exemplo de um programa simples em Rust:

\n\n
fn main() {\n    let x = 42;\n    let y = x + 10; // <-- ponto de parada\n    println!("y = {}", y);\n}
\n\n

Quando 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.

\n\n

Step over, step into, step out — como o depurador sabe onde está a «próxima linha»?

\n\n

Esta é, talvez, a parte mais mágica. Como o depurador sabe que depois de let y = x + 10; vem println!?

\n\n

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.

\n\n

Por exemplo, para o nosso código, a tabela pode ser assim:

\n\n
Endereço 0x1000 → linha 2 (let x = 42;)\nEndereço 0x1010 → linha 3 (let y = x + 10;)\nEndereço 0x1020 → linha 4 (println!)
\n\n

Quando 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\n

Para 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

Blogs

Book Recommendations