1.1 Tutorial: gdb
Usando o gdb
para resolver um problema de segfault
Neste exemplo, você irá aprender a usar o gdb para descobrir por que o programa abaixo, quando executado, causa um erro de segmentation fault.
O programa deveria ler uma linha de texto do usuário e imprimi-la. No entanto, veremos que, do jeito como está, o resultado não é exatamente esse...
Versão inicial (com bug) do programa:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
printf("Digite um texto qualquer e pressione Enter ao final:\n");
char *buf;
buf = malloc(1<<31);
fgets(buf, 1024, stdin);
printf("Texto digitado foi:\n");
printf("%s\n", buf);
return 1;
}
Crie um arquivo chamado segfault-example.c
, com o conteúdo acima. Compile o arquivo usando o gcc e o execute, para verificarmos se realmente existe um problema:
gcc -o segfault-example segfault-example.c
./segfault-example
Você deve ter observado que, realmente, o programa termina com erro antes de imprimir a linha digitada pelo usuário. Vamos usar o gdb, então, para descobrir o motivo.
O primeiro passo é recompilar o arquivo usando a flag -g
, que faz com que o gcc compile o programa de uma forma ligeiramente diferente, incluindo símbolos que serão usados pelo gdb.
gcc -g -o segfault-example segfault-example.c
Agora, execute o programa novamente, dessa vez pelo gdb:
$ gdb segfault-example
[várias linhas na saída, omitidas]
Reading symbols from segfault-example...done.
(gdb)
Nesse ponto, o gdb está pronto e aguardando instruções. Para começar, vamos simplesmente executar o programa usando o comando run
e ver o que acontece:
(gdb) run
Starting program: /home/deborasetton/Documents/Mestrado/Monitoria/PCS3616-Systems-Programming/aula2/segfault-example
Digite um texto qualquer e pressione Enter ao final:
QUALQUER COISA AQUI
Program received signal SIGSEGV, Segmentation fault.
__GI__IO_getline_info (fp=fp@entry=0x7ffff7dd3980 <_IO_2_1_stdin_>, buf=buf@entry=0x0, n=1022, delim=delim@entry=10, extract_delim=extract_delim@entry=1, eof=eof@entry=0x0) at iogetline.c:86
86 iogetline.c: No such file or directory.
(gdb)
O gdb executa o programa até onde consegue e para novamente, informando que recebeu o sinal SIGSEGV
do sistema operacional. Isso significa que o programa tentou acessar uma região de memória inválida.
Vamos usar o comando backtrace
para descobrir onde exatamente o programa travou:
(gdb) backtrace
#0 __GI__IO_getline_info (fp=fp@entry=0x7ffff7dd3980 <_IO_2_1_stdin_>, buf=buf@entry=0x0, n=1022, delim=delim@entry=10, extract_delim=extract_delim@entry=1, eof=eof@entry=0x0) at iogetline.c:86
#1 0x00007ffff7a7f188 in __GI__IO_getline (fp=fp@entry=0x7ffff7dd3980 <_IO_2_1_stdin_>, buf=buf@entry=0x0, n=<optimized out>, delim=delim@entry=10, extract_delim=extract_delim@entry=1) at iogetline.c:38
#2 0x00007ffff7a7dfc4 in _IO_fgets (buf=0x0, n=<optimized out>, fp=0x7ffff7dd3980 <_IO_2_1_stdin_>) at iofgets.c:56
#3 0x0000000000400647 in main (argc=1, argv=0x7fffffffd5c8) at segfault-example.c:10
(gdb)
Repare que a saída do comando faz referência a alguns arquivos que o programa usa, mas que não fomos nós que escrevemos, como iogetline.c
e iofgets.c
.
Como estamos interessados no nosso próprio código, vamos usar o comando frame
para ir até o frame 3, que é o frame que fala sobre o nosso arquivo, segfault-example.c
:
(gdb) frame 3
#3 0x0000000000400647 in main (argc=1, argv=0x7fffffffd5c8) at segfault-example.c:10
10 fgets(buf, 1024, stdin);
(gdb)
Ok, então o programa travou na chamada à função fgets
. De maneira geral, sempre podemos assumir que funções da biblioteca padrão, como esta, estão funcionando -- se este não for o caso, o problema é muito maior.
Portanto, o problema deve estar em um dos 3 argumentos que passamos para a função. Talvez você não saiba, mas stdin
é uma variável global que é criada pela biblioteca stdio
, então este argumento podemos assumir que está ok. Resta o argumento buf
.
Vamos usar o comando print
para inspecionar o valor desta variável:
(gdb) print buf
$1 = 0x0
(gdb)
O valor da variável é 0x0
, que é o ponteiro nulo. Isso não é o que queremos -- buf
deveria apontar para uma área de memória que foi alocada na chamada ao malloc
(veja o código).
Portanto, vamos ter que descobirir o que aconteceu aqui. Mas antes, podemos encerrar a instância atual do programa (que já nos deu informações suficientes e não tem mais o que executar) usando o comando kill
:
(gdb) kill
Kill the program being debugged? (y or n) y
(gdb)
Após este comando, estamos novamente no início do gdb. Desta vez, vamos colocar um breakpoint
na linha do código que chama o malloc
:
(gdb) break segfault-example.c:9
Breakpoint 1 at 0x40062f: file segfault-example.c, line 9.
(gdb)
Agora, vamos rodar o programa novamente:
(gdb) run
Starting program: /home/deborasetton/Documents/Mestrado/Monitoria/PCS3616-Systems-Programming/aula2/segfault-example
Digite um texto qualquer e pressione Enter ao final:
Breakpoint 1, main (argc=1, argv=0x7fffffffd5c8) at segfault-example.c:9
9 buf = malloc(1<<31);
(gdb)
Primeiro, vamos ver qual é o valor de buf
antes da chamada ao malloc
, usando o comando print
. Uma vez que essa variável ainda não foi inicializada, esperamos que o valor seja inválido, e realmente é:
(gdb) print buf
$1 = 0x0
(gdb)
Agora, vamos usar o comando next
para executar apenas esta linha de código e parar novamente, para podermos ver o que aconteceu com a variável:
(gdb) next
10 fgets(buf, 1024, stdin);
(gdb) print buf
$2 = 0x0
(gdb)
Após a chamada, verificamos que buf
continua inválido, apontando para NULL. Por quê? Se você consultar a documentação do malloc
, descobrirá que essa função retorna NULL quando não consegue alocar a quantidade de memória solicitada. Portanto, a chamada ao malloc
feita pelo nosso programa deve ter falhado. Vamos olhar esssa chamada novamente:
buf = malloc(1<<31);
Bom... O valor da expressão 1 << 31
(o inteiro 1
deslocado 31 bits à esquerda) é 429497295
ou 4GB. Poucos sistemas operacionais alocariam esta quantidade de memória para um único programa, a não ser com configurações especiais, então é claro que o malloc falhou. Além disso, nós só estamos lendo 1024 bytes com o fgets
, então para que alocar tanta memória?
Mude o valor 1<<31
no código-fonte para 1024
, compile e execute o programa novamente:
$ gcc -o segfault-example segfault-example.c
$ ./segfault-example
Digite um texto qualquer e pressione Enter ao final:
QUALQUER COISA AQUI
Texto digitado foi:
QUALQUER COISA AQUI
Problema resolvido! \o/
E agora você sabe como depurar segfaults usando o gdb, o que é extremamente útil. Finalmente, este exemplo também ilustra um outro ponto muito importante: sempre verifique o valor retornado pelo malloc!
Versão original: http://www.unknownroad.com/rtfm/gdbtut/gdbsegfault.html