4. Depuração
4.1. Compilando para Depuração
Durante o desenvolvimento de software, a depuração ou debugging desempenha um papel fundamental na identificação e correção eficiente de erros no código. Ao compilar programas em C, existe uma prática recomendada para facilitar a depuração: a utilização da opção -g
ao chamar o GCC. Essa opção instrui o compilador a incluir informações de depuração no executável gerado. Essas informações, como símbolos de função, variáveis locais e localizações de linha, permitem uma análise detalhada do código durante a depuração. Por exemplo, ao executar o programa em um depurador, é possível definir pontos de interrupção, inspecionar valores de variáveis e rastrear a execução do código passo a passo.
Abaixo está um exemplo de código que podemos usar para ilustrar como compilar programas em C para depuração:
exemplo.c
int foo (int *p);
int main (void)
{
int *p = 0;
/* ponteiro nulo */
return foo (p);
}
int foo (int *p)
{
int y = *p;
return y;
}
Nesse código, temos duas funções: int main()
e int foo()
. A função main
inicializa um ponteiro p
com o valor nulo (0) e, em seguida, chama a função foo
passando esse ponteiro como argumento. A função foo
recebe um ponteiro como parâmetro e tenta acessar o valor apontado por ele. No entanto, como p
é nulo, essa operação resulta em um erro. Para compilar esse código com a opção de depuração, você pode usar o seguinte comando:
Ao executá-lo, receberemos uma mensagem de erro, indicando que houve uma violação de segmentação, segmentation fault
. Essa mensagem é exibida quando ocorre uma tentativa de acessar uma área da memória que não é permitida, como no caso em que o ponteiro p
aponta para o valor nulo.
Vamos falar sobre o arquivo core mencionado na mensagem de erro. Ele é um arquivo de despejo de memória que pode ser gerado quando ocorre uma falha grave em um programa e contém informações sobre o estado da memória no momento da falha, sendo útil para analisar e depurar o problema. Nem todos os sistemas geram automaticamente o core por padrão. Se ele não for gerado, essa funcionalidade pode ser habilitada executando o comando abaixo, que define temporariamente o tamanho máximo do arquivo core como ilimitado, permitindo a geração do core em caso de falha no programa:
É possível que, ainda assim, o arquivo core não seja gerado. Caso você utilize o Ubuntu ou algum de seus derivados, é possível que ao desativar o Apport, o programa que reporta erros no Ubuntu, resolva o problema. Isso pode ser feito com o seguinte comando:
É importante ressaltar que, se o core não tiver sido gerado durante a primeira execução do programa, o core terá que ser gerado novamente.
4.2. Depurando Programas
Vamos agora usar o gdb para depurar o nosso executável exemplo. Para isso, escrevemos o seguinte comando:
$ gdb exemplo core
(...)
Core was generated by './exemplo'.
Program terminated with signal SIGSEGV, Segmentation fault.
warning: Section '.reg-xstate/17012' in core file too small.
#0 0x00005636c35b715b in foo (p=0x0) at exemplo.c:12
11 int y = *p;
(gdb)
O GDB aponta a linha do código onde ocorreu a falha do programa. Nesse caso, o erro ocorreu quando o programa tentou desreferenciar o ponteiro p
. Para entender melhor o motivo da falha, podemos examinar o valor de p
com o comando print
:
Como visto acima, o ponteiro p
é nulo, explicando a falha ao tentarmos desreferenciá-lo. Para investigar a sequência de chamadas de funções que conduziu ao estado atual do programa, pode-se exibir um rastreamento de pilha com o comando backtrace
:
(gdb) backtrace
#0 0x000055661c11315b in foo (p=0x0) at exemplo.c:12
#1 0x000055661c113149 in main () at exemplo.c:7
Outra funcionalidade importante do GDB é a capacidade de definir pontos de interrupção usando o comando break
, permitindo a parada da execução do programa em pontos específicos. Tais pontos podem ser determinados para funções específicas, linhas ou locais na memória. Vamos definir um ponto de interrupção no início da função main
:
Agora, quando executarmos o programa no GDB, ele irá parar assim que a função main
for executada. Poderemos, então, avançar pela execução do programa passo a passo com o comando step
, aprimorando a observação do comportamento do programa:
(gdb) run
Starting program: exemplo
Breakpoint 1, main () at exemplo.c:5
5 int *p = 0;
(gdb) step
7 return foo(p);
(gdb) print p
$2 = (int *) 0x0 /* ponteiro nulo */
(gdb)
As variáveis podem ser modificadas durante a depuração com o comando set var
, o que é útil para testar diferentes cenários. Vamos continuar com o nosso exemplo e modificar os valores de p
e *p
. Por fim, após modificarmos as variáveis, retomaremos a execução do programa com o comando continue
:
(gdb) set variable p = malloc(sizeof(int))
(gdb) print p
$3 = (int *) 0x5555555592a0
(gdb) set variable *p = 255
(gdb) print *p
$4 = 255
(gdb) continue
Continuing.
[Inferior 1 (process 17673) exited with code 0377] /* 0377 base 8 = 255 base 10 */
(gdb)
Em resumo, introduzimos o processo de depuração de programas em C usando o depurador GNU, GDB. Abrangemos os comandos essenciais, incluindo como iniciar o GDB, definir pontos de interrupção, inspecionar variáveis e alterar seus valores, bem como rastrear a sequência de chamadas de funções, técnicas vitais para entender e resolver os problemas que ocorrem durante a execução do seu código.