Debugging em C
LidoNesta aula, você aprenderá técnicas essenciais de debugging em C, desde o uso do depurador gdb até a prática de printf debugging, breakpoints e inspeção de memória. Serão apresentados exemplos práticos e exercícios para consolidar o conhecimento.
Debugging é uma habilidade fundamental para qualquer programador, especialmente em C, onde o controle de memória e a ausência de abstrações podem gerar erros sutis. Nesta aula, vamos explorar as principais ferramentas e técnicas para depurar programas em C, focando no depurador GNU (gdb), na abordagem clássica de printf debugging, no uso de breakpoints e na inspeção de memória. Ao final, você estará apto a identificar e corrigir bugs de forma mais eficiente.
Vamos começar com uma visão geral do gdb, o depurador mais utilizado em sistemas Unix-like, e depois avançar para técnicas práticas que podem ser aplicadas em qualquer ambiente de desenvolvimento.
gdb (visão geral)
O GNU Debugger (gdb) é uma ferramenta poderosa que permite executar seu programa passo a passo, examinar variáveis, memória, registradores e muito mais. Ele é essencial para depurar programas em C, pois oferece um controle granular sobre a execução.
Para usar o gdb, você precisa compilar seu programa com a opção -g (ou -ggdb para incluir informações específicas do gdb). Isso adiciona informações de depuração ao executável, permitindo que o gdb associe endereços de memória a linhas de código e nomes de variáveis.
Comandos básicos do gdb:
run(our): executa o programa.break(oub): define um breakpoint.next(oun): executa a próxima linha, sem entrar em funções.step(ous): executa a próxima linha, entrando em funções.print(oup): exibe o valor de uma variável.continue(ouc): continua a execução até o próximo breakpoint.quit(ouq): sai do gdb.
Exemplo de uma sessão típica:
// arquivo: exemplo.c
#include <stdio.h>
int main() {
int x = 10;
int y = 20;
int soma = x + y;
printf("Soma: %d\n", soma);
return 0;
}
Compilando com informações de debug e iniciando o gdb:
gcc -g -o exemplo exemplo.c
gdb ./exemplo
Dentro do gdb, você pode definir um breakpoint na linha 5 (onde soma é calculada):
(gdb) break exemplo.c:5
(gdb) run
(gdb) print x
$1 = 10
(gdb) print y
$2 = 20
(gdb) next
(gdb) print soma
$3 = 30
(gdb) continue
O gdb também permite inspecionar chamadas de função, pilha de execução e até mesmo alterar valores em tempo de execução.
printf debugging
Antes de depuradores sofisticados, a técnica mais comum era inserir instruções printf no código para rastrear o fluxo e os valores das variáveis. Apesar de simples, essa técnica ainda é muito útil, especialmente em ambientes onde o gdb não está disponível (ex.: sistemas embarcados) ou quando se deseja um log persistente.
Exemplo de printf debugging:
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int soma = 0;
for (int i = 0; i < 5; i++) {
soma += arr[i];
printf("i=%d, arr[i]=%d, soma parcial=%d\n", i, arr[i], soma);
}
printf("Soma total: %d\n", soma);
return 0;
}
As mensagens de log ajudam a entender a evolução das variáveis e identificar onde o comportamento diverge do esperado. No entanto, essa técnica tem desvantagens: é necessário recompilar o código a cada alteração, pode poluir a saída e, se esquecida, pode impactar o desempenho. Por isso, muitos programadores preferem usar um depurador interativo.
Breakpoints
Breakpoints são pontos de parada no código onde a execução é interrompida, permitindo inspecionar o estado do programa. Eles são fundamentais para análise detalhada sem precisar executar o programa inteiro.
No gdb, você pode definir breakpoints de várias formas:
break linha– para em uma linha específica do arquivo atual.break função– para quando uma função é chamada.break arquivo:linha– para uma linha em um arquivo específico.break condicional– comobreak linha if condição, para parar apenas se a condição for verdadeira.
Exemplo de breakpoint condicional:
// arquivo: loop.c
#include <stdio.h>
int main() {
for (int i = 0; i < 100; i++) {
printf("i = %d\n", i);
}
return 0;
}
No gdb:
(gdb) break loop.c:5 if i == 50
(gdb) run
O programa será interrompido quando i atingir 50, permitindo inspecionar o estado naquele momento. Breakpoints também podem ser gerenciados com comandos como info breakpoints, delete e disable.
Inspecionando memória
Em C, é crucial entender como os dados são armazenados na memória. O gdb oferece comandos para examinar memória em detalhes, como x (examinar) e ptype (tipo).
O comando x tem a sintaxe x/unidade formato endereço. Por exemplo, x/4dw &var exibe 4 palavras (4 bytes cada) em formato decimal a partir do endereço de var.
Exemplo de inspeção de memória:
#include <stdio.h>
int main() {
int numbers[4] = {10, 20, 30, 40};
printf("Endereço de numbers: %p\n", (void*)numbers);
return 0;
}
Compilando e executando no gdb:
gcc -g -o mem mem.c
gdb ./mem
(gdb) break mem.c:5
(gdb) run
(gdb) print numbers
$1 = {10, 20, 30, 40}
(gdb) x/4dw numbers
0x7fffffffe2c0: 10 20 30 40
(gdb) x/4dh numbers
0x7fffffffe2c0: 10 0 20 0 30 0 40 0 (dependendo do endianness)
O comando ptype mostra o tipo de uma variável ou expressão, útil para entender estruturas complexas. Além disso, é possível inspecionar o heap e a pilha, rastreando alocações dinâmicas e possíveis vazamentos.
Boas práticas e observações finais
Para um debugging eficaz, combine várias técnicas: use breakpoints para análise interativa, printf para logs de longo prazo e inspeção de memória para problemas de ponteiros. Sempre compile com -g em desenvolvimento, e remova prints de debug em produção. Aprenda atalhos do gdb para agilizar o processo.
Referências
- Documentação oficial do GDB
- Manual do GDB (online)
- GDB: A Beginner's Guide (Brendan Gregg)
- GDB Tutorial (Carnegie Mellon)
- GDB Documentation (Delorie)
- IBM: printf debugging
Exercícios
- Explique a diferença entre os comandos
nextestepno gdb e dê um exemplo de quando cada um é útil. - Considere o código abaixo. Usando printf debugging, adicione instruções para rastrear o valor de
produtoa cada iteração. Qual a saída esperada?#include <stdio.h> int main() { int produto = 1; for (int i = 1; i <= 5; i++) { produto *= i; } printf("%d\n", produto); return 0; } - Escreva um programa em C que tenha uma função
soma(a,b). Compile com-ge mostre como definir um breakpoint na funçãosomano gdb e como inspecionar os parâmetros. - Dado o array
int arr[5] = {1,2,3,4,5};, mostre como usar o comandoxpara exibir os 5 inteiros em hexadecimal. - Explique como breakpoints condicionais podem economizar tempo em loops longos e dê um exemplo de sintaxe.
next executa a próxima linha sem entrar em funções chamadas, enquanto step entra na função. Use next quando quiser pular a implementação de uma função e step quando precisar depurar dentro dela.printf("i=%d, produto=%d\n", i, produto); dentro do loop. A saída será:i=1, produto=1
i=2, produto=2
i=3, produto=6
i=4, produto=24
i=5, produto=120
#include <stdio.h>
int soma(int a, int b) { return a + b; }
int main() { printf("%d\n", soma(2, 3)); return 0; }
No gdb: break soma, run, print a, print b.x/5wx arr exibirá os 5 inteiros em hexadecimal (w = word, x = hexadecimal).break loop.c:5 if i == 1000.