GDB Gnu Debugger

Julho 17, 2011

Acho que fui apresentado ao GDB logo que aprendi a programar, mas nunca me acostumei a usá-lo. Talvez por causa da maratona, sempre fui adepto do debug com printf, exceto quando há falha de segmentação. Mas mesmo nesse caso eu acho o valgrind mais eficaz.

Entretanto, dizem que essa é uma má prática e que o GDB pode ser uma ferramenta essencial na hora de encontrar causas de bugs. Pessoalmente, não me lembro de nenhuma vez em que o GDB me ajudou a achar um bug que eu não teria achado de outra forma, tão ou mais rapidamente quanto.

Mas isso deve ser porque eu não aprendi a usá-lo direito :P Em uma nova tentativa de melhorar meu uso com essa ferramenta, decidi organizar informações em um post de forma que eu possa vir a aprender novas funcionalidades.

Exemplo Básico

Para poder rodar código com GDB, devemos passar a flag -g ou -ggdb.

Dado um exemplo teste.c

#include <stdlib.h>
int main (int argc, char **argv){

    int i, v[5], n = atoi(argv[1]);

    for(i=n; i>=0; i++)
        v[i] = i;

    return 0;
}

Podemos compilá-lo com:

$ gcc teste.c -g

Gerando o executável ./a.out. Aí invocamos o gdb com o comando:

$ gdb ./a.out

Sem os possíveis argumentos do programa. Esses argumentos serão passados quando formos rodar o programa dentro do gdb, através do comando run.

(gdb) run 4

O programa acima tem um bug que causará falha de segmentação (ou algum erro relacionado ao acesso indevido de memória). No meu caso a saída foi:

Starting program: /home/kunigami/workspace/c/a.out 

Program received signal SIGSEGV, Segmentation fault.
0x00000000004004d9 in main () at teste.c:6
6	        v[i] = i;

Ele acusa o erro na linha 6 e mostra o trecho de código correspondente. Nesse caso, podemos analisar o código com a função print (ou simplesmente ‘p’) do gdb:

(gdb) p v[i]
Cannot access memory at address 0x7ffffffff000
(gdb) p i
$2 = 832

Vemos, ao tentar imprimir v[i], que tal posição da memória é inválida e isso porque o índice i está além das 5 posições alocadas.

Breakpoints

breakpoint (b) — coloca um marcador em uma linha do código para que o programa pare. A partir daí podemos executar o código passo-a-passo para uma análise mais detalhada.

Há varias maneiras de se colocar esse marcador. Uma é você dizer diretamente o número da linha, como por exemplo:

(gdb) b 123 

Colocará um breakpoint na linha 123. Se há vários arquivos fontes que formam o executável, dá pra especificar em qual arquivo quer se colocar o breakpoint. Se por exemplo quisermos colocar no código foo.c, fazemos:

(gdb) b foo.c:123

Também dá para passar o nome da função.

(gdb) b foo.c:bar

Para listar os breakpoints setados,

info b

Associado a cada breakpoint está um identificador, que pode ser usado se quisermos remover um dado breakpoint através de

delete <id do breakpoint>

Breakpoints condicionais

Se uma dada função é chamada muitas vezes e sabemos em quais condições um erro acontecerá, poderíamos fazer a seguinte gambiarra no código:

...
if(<condição>){
/* linha L do código */
}
...

E colocar o breakpoint na linha L. Ao invés disso, podemos colocar um breakpoint condicional. Dado o id do breakpoint, a sintaxe é a seguinte:

condition <id do breakpoint> <condição>

A condição é exatamente a mesma que você colocaria dentro do if.

Andando pelo código

Uma vez que o breakpoint fez a execução do programa parar, temos vários comandos para analisar detalhadamente as próximas linhas que serão executadas.

* step (s) — vai para a próxima instrução. Se for uma função, entra nela (step into).

* next (n) — vai para a próxima instrução. Se for uma função, não entra nela (step over).

* continue (c) — continua a execução normal do código (irá para a próxima vez que encontrar um breakpoint)

* finish (sem atalho) — sai da função atual (step out)

GDB e multi-threading

Quando lidamos com código multi-thread, o GDB começa a fazer mais falta. Vamos analisar o seguinte exemplo, adaptado daqui, usando pthreads:

threads.c

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *foo(void *id)
{
    int i, s = 0;
    for(i = 0; i < 10000000; i++)
        s = (s + i) % 1234567;
    printf("[%d] s = %d\n", *((int*)id), s);
}

main()
{
     pthread_t t1, t2;
     int  id1 = 1, id2 = 2;

     /* Create independent threads each of which
        will execute function */
     (void) pthread_create(&t1, NULL, foo, (void*) &id1);
     (void) pthread_create(&t2, NULL, foo, (void*) &id2);

     /* Wait till threads are complete before main
        continues. Unless we wait we run the risk
        of executing an exit which will terminate   
        the process and all threads before the 
        threads have completed. */
     pthread_join(t1, NULL);
     pthread_join(t2, NULL); 

     exit(0);
}

Nesse código a thread inicial cria duas novas threads e bloqueia. Essas duas threads chamam a função foo, onde ficam em um loop. Quando ambas saem da função, a thread inicial desbloqueia e termina o código. Para compilá-lo,

gcc threads.c -lpthread -g

Vamos rodar o gdb e colocar um breakpoint na função foo, mas apenas para uma das threads. Podemos usar o breakpoint condicional.

(gdb) b foo
Breakpoint 1 at 0x400680: file teste.c, line 7.
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000400680 in foo at teste.c:7
(gdb) condition 1 (*(int*)id) == 1

Aí rodamos o código

(gdb) run
Starting program: /home/kunigami/workspace/c/threads/a.out 
[Thread debugging using libthread_db enabled]
[New Thread 0x7ffff783c700 (LWP 30676)]
[Switching to Thread 0x7ffff783c700 (LWP 30676)]

Breakpoint 1, foo (id=0x7fffffffe2fc) at teste.c:7
7	    int i, s = 0;

Aqui ele avisa que trocou para outra thread (“Switching to Thread….“). Podemos visualizar as threads através do comando info threads.

(gdb) info threads
[New Thread 0x7ffff703b700 (LWP 30677)]
  3 Thread 0x7ffff703b700 (LWP 30677)  clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:84
* 2 Thread 0x7ffff783c700 (LWP 30676)  foo (id=0x7fffffffe2fc) at teste.c:7
  1 Thread 0x7ffff7fcf700 (LWP 30673)  clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:84

O que a tabela acima mostra é que a thread com id 1 é a thread inicial que está sendo clonada para criar a thread 3. A thread 2 está na função foo, onde ativou o breakpoint e é ela quem o GDB está analisando, indicado pelo asterisco na frente do id. Note que essa tabela provavelmente vai ser diferente a cada execução. Depende de como o sistema operacional está escalonando a execução das tarefas, por exemplo.

Vamos dar uns steps até que a variável i do loop valha, por exemplo, 3.

(gdb) p i
$2 = 3

Agora, vamos trocar para a thread 3 através do comando thread 3 e imprimir o valor de i.

(gdb) thread 3 
[Switching to thread 3 (Thread 0x7ffff703b700 (LWP 30779))]
#0  foo (id=0x7fffffffe2f8) at teste.c:8
8	    for(i = 0; i < 100000000; i++)
(gdb) p i
$7 = 2

Novamente, note que o estágio em que a thread 3 se encontra dependerá muito. Se ela ainda não tiver entrado no loop, dê alguns steps.

Conclusão

A possibilidade de se analisar várias threads ao mesmo tempo torna o GDB uma ferramenta muito mais atraente para se debugar código multi-thread do que o uso de printf‘s.

Referências

[1] http://www.delorie.com/gnu/docs/gdb/gdb_25.html