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 (ou r): executa o programa.
  • break (ou b): define um breakpoint.
  • next (ou n): executa a próxima linha, sem entrar em funções.
  • step (ou s): executa a próxima linha, entrando em funções.
  • print (ou p): exibe o valor de uma variável.
  • continue (ou c): continua a execução até o próximo breakpoint.
  • quit (ou q): 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 – como break 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

Exercícios

  1. Explique a diferença entre os comandos next e step no gdb e dê um exemplo de quando cada um é útil.
  2. ✓ Resposta: 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.
  3. Considere o código abaixo. Usando printf debugging, adicione instruções para rastrear o valor de produto a 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;
    }
    
  4. ✓ Resposta: Adicione 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
    
  5. Escreva um programa em C que tenha uma função soma(a,b). Compile com -g e mostre como definir um breakpoint na função soma no gdb e como inspecionar os parâmetros.
  6. ✓ Resposta: Programa:
    #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.
  7. Dado o array int arr[5] = {1,2,3,4,5};, mostre como usar o comando x para exibir os 5 inteiros em hexadecimal.
  8. ✓ Resposta: No gdb, após parar no programa: x/5wx arr exibirá os 5 inteiros em hexadecimal (w = word, x = hexadecimal).
  9. Explique como breakpoints condicionais podem economizar tempo em loops longos e dê um exemplo de sintaxe.
  10. ✓ Resposta: Breakpoints condicionais interrompem apenas quando uma condição é verdadeira, evitando paradas a cada iteração. Exemplo: break loop.c:5 if i == 1000.