Você já escreveu bastante código C — e, se foi honesto nos exercícios, já caçou muitos bugs. Como? Provavelmente espalhando printf pelo código para ver o valor das variáveis. Essa técnica funciona, mas é primitiva: você adiciona prints, recompila, roda, remove os prints, e repete. Existe uma forma infinitamente melhor. O GDB (GNU Debugger) permite pausar seu programa em qualquer ponto, inspecionar qualquer variável, executar linha a linha e ver exatamente onde e por que algo dá errado. Abrimos a Fase 6 aprendendo a depurar como um profissional — trocando a adivinhação pela investigação.
Preparando o programa para depuração
Para que o GDB possa relacionar o código de máquina com o seu código-fonte (mostrando nomes de variáveis e números de linha), você precisa compilar com a flag -g, que inclui informações de depuração:
gcc -g programa.c -o programa
Essa flag -g já apareceu quando usamos o Valgrind, e por boa razão: ela é o que permite às ferramentas falarem a sua linguagem, apontando linhas e variáveis reais em vez de endereços crus. Sem -g, o GDB ainda funciona, mas mostra apenas endereços de memória, o que é quase inútil. Torne -g um hábito ao compilar durante o desenvolvimento.
Iniciando o GDB e os primeiros comandos
Você inicia o GDB passando o programa como argumento, e ele abre um prompt interativo onde você digita comandos:
gdb ./programa
Dentro dele, os comandos essenciais para começar. run (ou r) inicia a execução do programa. break (ou b) coloca um ponto de parada numa linha ou função, fazendo o programa pausar ali. quit (ou q) sai do GDB. Veja um exemplo de sessão. Suponha este programa com um bug:
#include <stdio.h>
int dividir(int a, int b) {
return a / b; // bug em potencial: e se b for 0?
}
int main(void) {
int x = 10;
int y = 0;
int resultado = dividir(x, y); // vai causar divisão por zero
printf("Resultado: %d\n", resultado);
return 0;
}
Compilado com -g e executado no GDB, se você apenas digitar run, o programa vai travar (divisão inteira por zero), e o GDB mostrará exatamente a linha onde isso ocorreu — dentro de dividir. Só isso já é mais informativo que qualquer printf: o GDB aponta o local do crash de imediato.
Breakpoints: pausando onde você quiser
O verdadeiro poder vem dos breakpoints — pontos onde o programa pausa para você investigar. Você os coloca antes de rodar:
(gdb) break main # pausa ao entrar em main
(gdb) break dividir # pausa ao entrar na função dividir
(gdb) break programa.c:10 # pausa na linha 10 do arquivo
(gdb) run # inicia; vai parar no primeiro breakpoint
Quando o programa atinge um breakpoint, ele congela naquele ponto, e você ganha controle. É como apertar "pausa" num filme: agora você pode examinar o estado do programa com calma — quais valores as variáveis têm, onde exatamente você está no código. Colocar um breakpoint na função ou linha suspeita é o primeiro passo de qualquer investigação de bug.
Inspecionando o estado: print e outros comandos
Com o programa pausado, você examina tudo. O comando print (ou p) mostra o valor de qualquer variável ou expressão:
(gdb) print x # mostra o valor de x
$1 = 10
(gdb) print y # mostra o valor de y
$2 = 0
(gdb) print x + y # avalia uma expressão
$3 = 10
(gdb) print x / y # o GDB até detecta a divisão por zero aqui!
Division by zero
Você pode inspecionar variáveis simples, expressões, elementos de vetores (print v[2]), campos de structs (print pessoa.idade) e até seguir ponteiros (print *ptr). Esse é o coração da depuração investigativa: em vez de adivinhar o que as variáveis contêm, você pergunta e o GDB responde. Outros comandos úteis de inspeção: info locals mostra todas as variáveis locais de uma vez, e backtrace (ou bt) mostra a pilha de chamadas — a sequência de funções que levou até o ponto atual, aquela pilha que estudamos nas aulas A Pilha e o Heap: Onde Cada Coisa Vive e Recursão: Quando uma Função Chama a Si Mesma, agora visível diante de você.
Executando passo a passo
Uma vez pausado, você pode avançar a execução de forma controlada, linha a linha, observando o efeito de cada uma. Três comandos governam esse avanço. next (ou n) executa a linha atual e para na próxima, sem entrar em funções chamadas (trata a chamada como um passo único). step (ou s) também avança uma linha, mas entra nas funções chamadas, permitindo depurá-las por dentro. E continue (ou c) retoma a execução normal até o próximo breakpoint (ou o fim).
(gdb) break main
(gdb) run
(gdb) next # executa uma linha, sem entrar em funções
(gdb) next
(gdb) step # desta vez, entra na função chamada para investigá-la
(gdb) print b # já dentro de 'dividir', inspeciona o parâmetro b -> 0!
A distinção entre next e step é fundamental: use next quando confia na função chamada e só quer passar por ela; use step quando suspeita que o bug está dentro dela e quer investigá-la linha a linha. Avançar passo a passo, inspecionando variáveis a cada parada, é como você reconstrói a sequência exata de eventos que levou ao bug — vendo o momento preciso em que um valor se torna errado.
Um fluxo de depuração típico
Vale descrever como esses comandos se combinam numa investigação real. Você tem um programa que trava ou produz resultado errado. Primeiro, compila com -g e abre no GDB. Coloca um breakpoint na função suspeita (break funcao_suspeita) e roda (run). Quando para lá, inspeciona os argumentos e variáveis (print, info locals) para ver se já chegam errados. Se estão certos, avança passo a passo (next, step), inspecionando a cada linha, até flagrar o exato momento em que um valor se torna incorreto. Se o programa travou, um backtrace mostra o caminho de chamadas que levou ao crash. Esse ciclo — parar, inspecionar, avançar, inspecionar — transforma a depuração de tentativa-e-erro numa investigação metódica, onde você observa o programa por dentro em vez de adivinhar de fora.
Por que isso vale o esforço de aprender
Aprender o GDB tem uma pequena curva — são comandos novos a memorizar —, e é tentador continuar com os printf familiares. Mas o investimento se paga rápido. Com printf, cada nova hipótese exige editar o código, recompilar e rodar de novo; com o GDB, você investiga um programa já em execução, testando hipóteses instantaneamente com print e avançando à vontade. Para bugs difíceis — um valor que se corrompe em algum ponto incerto, um crash em código que parece correto — o GDB é insubstituível. Ele também é a base de ferramentas gráficas de depuração (nas IDEs) que usam o GDB por baixo. Dominar o depurador é uma das habilidades que mais separam o programador iniciante do experiente.
O que vem a seguir
Hoje trocamos a depuração por adivinhação pela investigação metódica: compilar com -g, pausar o programa com breakpoints, inspecionar variáveis e a pilha de chamadas com print e backtrace, e avançar de forma controlada com next e step. O GDB nos deixa observar o programa por dentro. Na próxima aula, combinamos o GDB com uma ferramenta que já conhecemos de longe — o Valgrind, junto dos sanitizers — para atacar a categoria mais traiçoeira de bugs em C: os erros de memória, que muitas vezes não travam onde acontecem, mas muito depois. Juntas, essas ferramentas formam o arsenal de depuração completo do programador C.
Fontes e leituras recomendadas
- GDB Documentation — o manual oficial completo — https://www.gnu.org/software/gdb/documentation/
- The Art of Debugging with GDB, DDD, and Eclipse, Matloff & Salzman — livro dedicado ao tema
- GDB Cheat Sheet — referência rápida dos comandos essenciais — https://darkdust.net/files/GDB%20Cheat%20Sheet.pdf
- The C Programming Language (K&R) — embora anterior ao GDB moderno, fundamenta o modelo de execução
- Beej's Guide — seção sobre depuração — https://beej.us/guide/bgc/
Exercícios
Exercício 1
Compile um programa simples com -g, abra-o no GDB, coloque um breakpoint em main com break main, rode com run, e use next para avançar algumas linhas. Descreva o que você observa a cada passo.
Ver resposta
✓ Resposta: Ao colocar break main e rodar, o programa para na primeira linha executável de main. Cada next executa uma linha e avança para a seguinte, e o GDB mostra a próxima linha a ser executada. Observando, você vê o programa progredir linha a linha; a cada parada, poderia usar print para ver como as variáveis vão sendo inicializadas e modificadas. O objetivo do exercício é sentir o controle do fluxo — o programa avança apenas quando você manda, em vez de rodar tudo de uma vez.
Exercício 2
No programa dividir do artigo (com a divisão por zero), use o GDB para descobrir o valor de b no momento do erro. Coloque um breakpoint em dividir, rode, e use print b. Confirme que b é 0.
Ver resposta
✓ Resposta:
(gdb) break dividir
(gdb) run
(gdb) print b
$1 = 0
Ao parar dentro de dividir (antes de executar a divisão), print b revela que b vale 0 — a causa da divisão por zero. Isso demonstra como o GDB localiza a origem de um crash: em vez de só saber que "houve divisão por zero", você vê exatamente qual valor causou o problema.
Exercício 3
Escreva um programa com um vetor de 5 inteiros e um laço que os soma. Coloque um breakpoint dentro do laço e use print para observar o valor do acumulador e do índice a cada iteração (avançando com continue ou next).
Ver resposta
✓ Resposta:
#include <stdio.h>
int main(void) {
int v[5] = {10, 20, 30, 40, 50};
int soma = 0;
for (int i = 0; i < 5; i++) {
soma += v[i]; // coloque o breakpoint nesta linha
}
printf("%d\n", soma);
return 0;
}
No GDB: break na linha do soma += v[i], depois run. A cada vez que o programa para (use continue para ir à próxima iteração), rode print i, print soma e print v[i]. Você verá soma crescer (0→10→30→60→100→150) e i avançar (0→1→2→3→4), observando o acumulador sendo construído passo a passo. É uma forma de "ver" o laço funcionando por dentro.
Exercício 4
Explique a diferença entre os comandos next e step do GDB. Dê um exemplo de situação em que você usaria cada um.
Ver resposta
✓ Resposta: next executa a linha atual e para na próxima linha da mesma função; se a linha atual contém uma chamada de função, next executa essa função inteira de uma vez (como um único passo), sem entrar nela. step também avança uma linha, mas se a linha contém uma chamada de função, step entra nessa função, parando na sua primeira linha, para que você possa depurá-la por dentro. Exemplo de uso: se você está em main e chega numa linha resultado = calcular(x, y), use next se confia em calcular e só quer o resultado dela (passando por cima); use step se suspeita que o bug está dentro de calcular e quer investigar sua execução linha a linha. next passa por cima das chamadas, step mergulha nelas.
Exercício 5
Explique o que o comando backtrace mostra e por que ele é especialmente útil quando um programa trava dentro de uma função que é chamada por várias outras. Como ele se relaciona com a pilha de chamadas das aulas A Pilha e o Heap: Onde Cada Coisa Vive e Recursão: Quando uma Função Chama a Si Mesma?
Ver resposta
✓ Resposta: O comando backtrace mostra a pilha de chamadas atual — a sequência de funções que estão em execução no momento, da mais recente (onde o programa está parado) até main, na ordem em que foram chamadas. Ele é especialmente útil quando um programa trava dentro de uma função chamada por várias outras porque revela qual caminho levou até o crash: se processar foi chamada por main, ou por validar, ou por salvar, o backtrace mostra exatamente a cadeia, com os argumentos de cada nível. Sem ele, você saberia onde travou, mas não como chegou lá. Isso se relaciona diretamente com a pilha de chamadas das aulas A Pilha e o Heap: Onde Cada Coisa Vive e Recursão: Quando uma Função Chama a Si Mesma: cada função em execução tem seu stack frame empilhado, e o backtrace é precisamente uma leitura dessa pilha — do frame do topo (a função atual) descendo até o frame de main na base. O GDB torna visível a estrutura que, naquelas aulas, discutimos de forma abstrata: você vê os frames empilhados, um chamando o outro.