Contribuíram neste tutorial Giovanni Bertão, Guilherme Tiaki Sassai Sato
Depuração
Debugar ou depurar um programa é a maneira mais efetiva de se encontrar erros e avaliar o fluxo de execução do programa. Em MC202, os laboratórios são feitos em C. Em C, temos diversas rotinas de manipulação de memória, ponteiros e chamadas de funções. Em todos esses casos realizar a depuração do programa irá permitir identificar erros com facilidade.
Para debugar um programa iremos utilizar a ferramenta GDB. Com ela é possível:
- executar instrução por instrução de um programa
- manipular valores de variáveis
- acompanhar chamadas de funções;
- inspecionar a memória do programa
- e várias outras funcionalidades.
Nesse tutorial, você irá aprender a instalar o GDB e debugar um programa bem simples.
GNU/Linux
NO Ubuntu ou Debian, utilize os comandos para instalar o GDB:
user@desktop:~$ sudo apt-get update && sudo apt-get install gdb -y
Para verificar a instalação digite gdb --version
. Você deve ver uma
como a seguinte:
user@desktop:~$ gdb --version
GNU gdb (Ubuntu 9.1-0ubuntu1) 9.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Mac e Windows
Você pode instalar o GDB com passos parecidos aos utilizados para instalar o compilador GCC.
Debugando um programa
Para facilitar o processo de depuração, é importante compilar o
programa passando a flag -g
para o GCC. Essa flag instrui o
compilador a adicionar dados no binário executável contendo
informações úteis e outros metadados. Por exemplo, ele insere uma
tabela mapeando cada linha do código-fonte às instruções de máquina
binárias correspondentes no arquivo executável. Assim, compilamos um
arquivo main.c
como:
user@desktop:~$ gcc -std=c99 -Wall -Werror -g main.c -o main -lm
Carregando um programa
Uma vez compilado o programa, é necessário carregá-lo no GDB utilizando o comando:
user@desktop:~$ gdb ./main
Isso abrirá uma sessão interativa do GDB e já irá carregar todos os metadados do programa na memória, mas seu programa ainda não terá começado a executar. Uma mensagem como a seguinte irá aparecer:
user@desktop:~$ gdb ./main
GNU gdb (Ubuntu 9.1-0ubuntu1) 9.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./main...
(gdb)
Breakpoints
Os breakpoints são utilizados para parar a execução do programa
imediatamente antes da execução de um determinado ponto. O comando
utilizado é o break <argumento>
ou b <argumento>
. Um argumento
pode ser o número da linha como argumento (b 10
) ou o nome de uma
função (b main
).
Outros comandos
Além do comando de breakpoint outros comandos importantes que são comumente utilizados são:
run
our
: inicia a execução do programa.continue
ouc
: continua a execução do programa até a próxima parada.step
ous
: executa a próxima linha do programa.next
oun
: igual aos
, porém, não entra em funções.print
oup
: imprime na tela o valor de uma variável.display
: imprime na tela o conteúdo cada vez que o programa para.delete
oud
: remove todos os breakpoints.
Debugando um primeiro programa
Vamos utilizar o seguinte programa main.c
nesse tutorial:
#include <stdio.h>
void escreva(int x) {
printf("Escrevendo: %d\n", x);
}
int main(void) {
int i = 10;
while (i >= 0) {
escreva(i);
i--;
}
return 0;
}
Compile o programa main.c com a flag de depuração(-g
):
user@desktop:~$ gcc -std=c99 -Wall -Werror -g main.c -o main -lm
Carregue o programa com o gdb:
user@desktop:~$ gdb ./main
Crie um breakpoint na função main:
(gdb) b main
Inicie a execução do programa:
(gdb) r
A execução irá parar com a seguinte mensagem:
Breakpoint 1, main () at main.c:8
8 int i = 10;
Utilize o comando n
para executar uma linha. A seguinte mensagem irá aparecer:
(gdb) n
10 while(i >= 0) {
(gdb)
Visualize o valor da variável i
:
(gdb) p i
$1 = 10
(gdb)
Imprima o valor de i
em hexadecimal cada vez que o programa parar:
(gdb) display/x i
Coloque um breakpoint na linha 11:
(gdb) b 11
Continue a execução até o próximo breakpoint c
:
(gdb) c
Continuando.
Breakpoint 2, main () at main.c:11
11 escreva(i);
1: /x i = 0xa
Repita o comando anterior:
(gdb) c
Continuando.
Breakpoint 2, main () at main.c:11
11 escreva(i);
1: /x i = 0x9
Remova todos os breakpoints e continue até o programa terminar:
(gdb) d
Delete all breakpoints? (y or n) y
(gdb) c
Continuing.
Escrevendo: 9
Escrevendo: 8
Escrevendo: 7
Escrevendo: 6
Escrevendo: 5
Escrevendo: 4
Escrevendo: 3
Escrevendo: 2
Escrevendo: 1
Escrevendo: 0
[Inferior 1 (process 7492) exited normally]
Finalize o gdb com o comando quit
(gdb) quit
Resolvendo uma falha de segmentação
Segmentation fault (SIGSEGV, falha de segmentação) é um erro muito comum que acontece quando trabalhamos com ponteiros e alocação de memória. Por mais monstruosos que esse problema aparente, podemos analisá-lo e resolve-lo usando o GDB.
Vamos analisar e consertar um programa que apresenta o erro SIGSEGV.
O seguinte código com erro será usado nesse tutorial.
#include<stdio.h>
#include<stdlib.h>
typedef struct node{
int valor;
struct node *prox;
} Node;
void free_lista(Node *no) {
if (no != NULL) {
free_lista(no->prox);
no->prox = NULL;
free(no);
}
}
void remover(Node **lista, int valor) {
Node *tgt, *par;
tgt = *lista;
par = NULL;
while (tgt->valor != valor) {
par = tgt;
tgt = tgt->prox;
}
tgt->prox = NULL;
free(tgt);
tgt = NULL;
if (par != NULL) {
par->prox = tgt->prox;
} else {
*lista = tgt->prox;
}
}
void inserir(Node **lista, int valor) {
Node *new, *tgt;
new = malloc(sizeof(Node));
new->valor = valor;
new->prox = NULL;
if (*lista == NULL) {
*lista = new;
} else {
tgt = *lista;
while (tgt->prox != NULL)
tgt = tgt->prox;
tgt->prox = new;
}
}
int main (void) {
Node *lista, *tgt;
lista = NULL;
for (int i = 0; i < 10; i++)
inserir(&lista, i);
remover(&lista, 5);
tgt = lista;
while (tgt != NULL) {
printf("%d\n", tgt->valor);
tgt = tgt->prox;
}
free_lista(lista);
lista = NULL;
return 0;
}
Compile e execute o código.
user@desktop:~$ gcc segf.c -o segf -g
user@desktop:~$ ./segf
Segmentation fault (core dumped)
Como esperado, estamos caindo em um segmentation fault. Execute o programa no gdb.
user@desktop:~$ gdb ./segf
.
.
.
(gdb) run
Starting program: /tmp/segf
Program received signal SIGSEGV, Segmentation fault.
0x000055555555523d in remover (lista=0x7fffffffdd00, valor=5) at segf.c:32
32 par->prox = tgt->prox;
Ao inspecionar a variável tgt
, obtemos que a mesma é um ponteiro
para a posição 0x0 (NULL). Logo, o acesso tgt->prox
é indevido e
resultou no SIGSEGV.
(gdb) p tgt
$1 = (Node *) 0x0
Analisando o código fonte, é possível identificar que o free(tgt)
e
tgt = NULL
são feitos antes da atribuição par->prox = tgt->prox
.
Assim, para consertar o problema, basta mover as linhas 27, 28 e 29
para depois do bloco do if-else
.
Execute o programa e verifique a saída.
user@desktop:~$ ./segf
0
1
2
3
4
6
7
8
9