Debugger GDB

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:

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:

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