gdb-pratico/gdb-01.org
2025-04-22 13:12:36 -03:00

18 KiB

Curso prático de introdução ao GDB

Parte 1: Conceitos Básicos

O que é o GDB

O GDB (GNU Debugger) é uma ferramenta de depuração de programas escritos em diversas linguagens. Ele possibilita a inspeção da execução de um programa em tempo real ou após uma falha, fornecendo recursos como:

  • Pontos de parada (breakpoints);
  • Execução passo a passo (step-by-step);
  • Visualização e modificação de variáveis;
  • Análise de memória e registradores;
  • Inspeção da pilha de chamadas (backtrace);
  • Avaliação de expressões em tempo de execução;
  • Depuração de múltiplas threads;
  • Depuração remota (via gdbserver).

Por operar em baixo nível, o GDB oferece uma visão detalhada do comportamento interno do programa, sendo essencial tanto para o desenvolvimento quanto para o diagnóstico de erros complexos.

Primeiro contato

Considere o seguinte programa em C (demo.c):

#include <stdio.h>

int soma(int a, int b) {
    int resultado = a + b;
    return resultado;
}

int main() {
    int x = 10;
    int y = 20;
    int z = soma(x, y);
    printf("Resultado: %d\n", z);
    return 0;
}

Compilação com símbolos de depuração:

gcc -g -o demo demo.c

Carregando o binário no GDB:

gdb ./demo

Listando o código-fonte

No GDB, podemos listar o código-fonte (comando list):

(gdb) list
3	int soma(int a, int b) {
4	    int resultado = a + b;
5	    return resultado;
6	}
7	
8	int main() {
9	    int x = 10;
10	    int y = 20;
11	    int z = soma(x, y);
12	    printf("Resultado: %d\n", z);

Por padrão, somente 10 linhas são exibidas, mas podemos teclar Enter algumas vezes até que todo o código seja listado.

(gdb)
13	    return 0;
14	}

Chagando ao final, podemos reiniciar a listagem com list .:

(gdb) list .
3	int soma(int a, int b) {
4	    int resultado = a + b;
5	    return resultado;
6	}
7	
8	int main() {
9	    int x = 10;
10	    int y = 20;
11	    int z = soma(x, y);
12	    printf("Resultado: %d\n", z);

Ponto de parada e execução

Nós podemos definir pontos de parada com o comando break:

(gdb) break main
Breakpoint 1 at 0x115b: file demo.c, line 9.

Assim, quando o programa for executado (com o comando run), a execução será pausada nos símbolos definidos como pontos de parada:

(gdb) run
Starting program: /home/blau/tmp/gdb/demo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main () at demo.c:9
9	    int x = 10;

Neste exemplo, o ponto de parada é a função main, e nós vemos a próxima linha a ser executada (linha 9). Para avançar para as próximas linhas, nós podemos executar o comando next:

(gdb) next
10	    int y = 20;

A linha 9 foi executada e a próxima será a linha 10. Se quisermos continuar executando o comando next, basta teclar Enter imediatamente em seguida:

(gdb)
11	    int z = soma(x, y);

Neste ponto, as variáveis x e y já foram carregadas e nós podemos conferir seus valores com o comando print:

(gdb) print x
$1 = 10
(gdb) print y
$2 = 20

O resultado de cada avaliação é armazenado no GDB em uma variável especial numerada ($n) de acordo com a ordem da avaliação.

A próxima linha a ser executada será a linha 11, onde temos a chamada da função soma. Se executarmos next novamente, a função será executada e nós iremos para a linha seguinte na função main. Mas nós também podemos entrar na função soma e acompanhar a sua execução passo a passo com o comando step:

(gdb) step
soma (a=10, b=20) at demo.c:4
4	    int resultado = a + b;

Aqui, nós podemos inspecionar os valores de a, b e do valor inicial de resultado, antes da linha 4 ser executada:

(gdb) print a
$3 = 10
(gdb) print b
$4 = 20
(gdb) print resultado
$5 = 0

Com o comando next, nós avançamos na função e já podemos exibir o novo valor de resultado:

(gdb) next
5	    return resultado;
(gdb) print resultado
$6 = 30

Se, a partir daqui, nós quisermos executar todo o restante do programa, basta executar comando continue:

(gdb) continue
Continuing.
Resultado: 30
[Inferior 1 (process 214693) exited normally]

Para sair do GDB…

(gdb) quit
:~$

Instalação

Debian e derivados

sudo apt update
sudo apt install gdb

O pacote build-essential instala outras ferramentas úteis para desenvolvimento, incluindo o gcc (GNU Compiler Collection), o make e as dependências mais comuns.

sudo apt install build-essential

Fedora e derivados

sudo dnf install gdb

Para um ambiente completo de desenvolvimento:

sudo dnf groupinstall "Development Tools"

Arch Linux e derivados

sudo pacman -S gdb

O grupo base-devel contém ferramentas úteis para compilação e depuração:

sudo pacman -S base-devel

Verificação da instalação

Versão:

gdb --version

Ajuda:

gdb --help

Personalização e configurações de início

O GDB pode ser configurado por meio de arquivos de inicialização lidos automaticamente ao iniciar. Esses arquivos permitem predefinir opções úteis, automatizar tarefas e estender o ambiente de depuração.

Configuração por usuário

O arquivo ~/.gdbinit:

O GDB executa esse arquivo sempre que for iniciado, a menos que seja desativado com a opção -nh.

Configuração de exemplo:

set pagination off            # Não pausa a saída do GDB
set confirm off               # Não pede confirmação para alguns comandos
set print pretty on           # Formata a apresentação de structs e arrays
set history save on           # Salva histórico entre seções
set history size 1000         # Tamanho máximo do histórico
set disassembly-flavor intel  # Define a sintaxe Intel na desmontagem de binários

Também pode ser interessante omitir as mensagens de versão no início do GDB, o que é feito no arquivo ~/.config/gdb/gdbearlyinit:

set startup-quietly on

O arquivo gdbearlyinit é carregado antes do GDB executar outros arquivos de início e só recebe definições que afetam o seu próprio comportamento.

Configuração por projeto

Arquivo .gdbinit no diretório do projeto (configuração local):

Se existir um arquivo .gdbinit no diretório corrente, o GDB pode executá-lo, mas isso é bloqueado por padrão:

:~/projeto$ gdb 
Warning: File ".gdbinit" auto-loading has been declined by your `auto-load safe-path'...

Para permitir o carregamento, temos que adicionar o caminho do projeto ao arquivo do usuário:

echo "add-auto-load-safe-path $(pwd)" >> ~/.gdbinit

Isso é muito utilizado para definir breakpoints automáticos, carregar símbolos extras, configurar scripts, etc.

Configuração global

O arquivo de configuração do sistema, /etc/gdb/gdbinit, é lido antes de ~/.gdbinit e pode ser usado para definir opções globais em ambientes compartilhados (ex: laboratórios ou servidores educacionais).

Comandos customizados

Nos arquivos .gdbinit, também é possível definir comandos customizados com a sintaxe:

define COMANDO
    LISTA DE COMANDOS
end
document COMANDO
    DESCRIÇÃO
end

Scripts em Python

É possível estender o GDB usando scripts em Python no .gdbinit, mas isso foge do escopo deste curso.

Binários ELF, símbolos e códigos-fonte

Todo arquivo executável pelo sistema é construído em um binário segundo um formato padrão. No GNU/Linux, esse formato é o ELF (Executable and Linkable Format), que inclui no binário:

  • O código de máquina do programa;
  • Dados de variáveis globais e estáticas;
  • Dados constantes;
  • Tabelas de símbolos;
  • Informações de depuração (se incluídas).

Os primeiros bytes de um arquivo ELF contém um cabeçalho com diversas informações sobre o binário, o que nós podemos listar com o utilitário readelf:

:~$ readelf -h demo
Cabeçalho ELF:
  Magia:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Classe:                            ELF64
  Dados:                             complemento 2, little endian
  Versão:                            1 (actual)
  OS/ABI:                            UNIX - System V
  Versão ABI:                        0
  Tipo:                              DYN (Position-Independent Executable file)
  Máquina:                           Advanced Micro Devices X86-64
  Versão:                            0x1
  Endereço do ponto de entrada:               0x1050
  Início dos cabeçalhos do programa:          64 (bytes no ficheiro)
  Start of section headers:          14944 (bytes no ficheiro)
  Bandeiras:                         0x0
  Tamanho deste cabeçalho:           64 (bytes)
  Tamanho dos cabeçalhos do programa:56 (bytes)
  Nº de cabeçalhos do programa:      14
  Tamanho dos cabeçalhos de secção:  64 (bytes)
  Nº dos cabeçalhos de secção:       37
  Índice de tabela de cadeias da secção: 36

Símbolos e o código-fonte

Símbolos são nomes associados a elementos do programa, como funções e variáveis. Quando os símbolos de depuração são incluídos no binário (compilando com -g, por exemplo), o GDB é capaz de:

  • Exibir nomes legíveis, em vez de endereços de memória;
  • Acompanhar o fluxo do programa com o código-fonte;
  • Receber definições de breakpoints por nomes de funções;
  • Fazer a associação de códigos binários com as linhas do código-fonte;
  • Acessar nomes de variáveis, tipos, estruturas, etc.

Nós podemos verificar se o programa foi compilado com os símbolos de depuração com o utilitário file

Sem a opção -g:

:~$ gcc -o demo demo.c
:~$ file demo
demo: ELF 64-bit LSB pie executable [...] not stripped

Com a opção -g:

:~$ gcc -g -o demo demo.c
:~$ file demo
demo: ELF 64-bit LSB pie executable [...] with debug_info, not stripped

Repare que, desta vez, nós temos a informação with debug_info no final da linha.

Nós também podemos verificar se há símbolos de depuração a partir das informações no formato ELF:

:~$ readelf -S demo | grep debug
  [28] .debug_aranges    PROGBITS         0000000000000000  00003037
  [29] .debug_info       PROGBITS         0000000000000000  00003067
  [30] .debug_abbrev     PROGBITS         0000000000000000  00003180
  [31] .debug_line       PROGBITS         0000000000000000  0000324a
  [32] .debug_str        PROGBITS         0000000000000000  000032ba
  [33] .debug_line_str   PROGBITS         0000000000000000  00003367

Mas o próprio GDB informa se há símbolos de depuração ao iniciar…

Sem a opção -g:

:~$ gcc -o demo demo.c
:~$ gdb demo
Reading symbols from demo...
(No debugging symbols found in demo)

Com a opção -g:

:~$ gcc -g -o demo demo.c
:~$ gdb demo
Reading symbols from demo...

O GDB e a programação em baixo nível

O GDB permite observar e controlar a execução de programas no nível mais próximo da máquina, o que inclui:

  • Acompanhamento de registradores da CPU;
  • Inspeção de endereços de memória brutos;
  • Execução instrução por instrução em assembly (stepi, nexti);
  • Observação do stack frame e chamadas (backtrace, info frame);
  • Controle direto de flags, pilha, heap e segmentos do processo.

Isso o torna útil para:

  • Diagnóstico de segmentation faults;
  • Estudo de chamadas de sistema;
  • Reversão e engenharia de baixo nível;
  • Entendimento de como o compilador transforma código em instruções.

Exemplo com um programa em assembly

Código-fonte (soma.asm):

section .data
    a dq 10
    b dq 20
    resultado dq 0

section .text
    global _start

_start:
    mov rax, [a]
    add rax, [b]
    mov [resultado], rax

    ; saída limpa com código 0
    mov rax, 60         ; syscall: exit
    xor rdi, rdi        ; status 0
    syscall

Montagem e link-edição:

nasm -f elf64 -g soma.asm -o soma.o
ld soma.o -o soma

Abertura com o GDB:

gdb ./soma
Reading symbols from soma...

Definindo _start como ponto de parada:

(gdb) b _start
Breakpoint 1 at 0x401000: file soma.asm, line 10.

Listando o fonte:

(gdb) l
1	section .data
2	    a dq 10
3	    b dq 20
4	    resultado dq 0
5	
6	section .text
7	    global _start
8	
9	_start:
10	    mov rax, [a]
(gdb)
11	    add rax, [b]
12	    mov [resultado], rax
13	
14	    ; saída limpa com código 0
15	    mov rax, 60         ; syscall: exit
16	    xor rdi, rdi        ; status 0
17	    syscall

Executando:

(gdb) r
Starting program: /home/blau/tmp/gdb/soma

Breakpoint 1, _start () at soma.asm:10
10	    mov rax, [a]

Observando os dados rotulados como a, b e resultado (assembly não tem variáveis):

(gdb) x/1gx &a
0x402000 <a>:	0x000000000000000a
(gdb) x/1gx &b
0x402008 <b>:	0x0000000000000014
(gdb) x/1gx &resultado
0x402010 <resultado>:	0x0000000000000000

O comando x exibe um conteúdo na memória e, com as opções /1gx, eu estou pedindo para mostrar uma giant word (1g), que é uma palavra de 8bytes, em base hexadecimal (x).

Executando as três instruções seguintes:

(gdb) n
11	    add rax, [b]
(gdb)
12	    mov [resultado], rax
(gdb)
15	    mov rax, 60         ; syscall: exit

Observando o no valor em resultado:

(gdb) x/1gx &resultado
0x402010 <resultado>:	0x000000000000001e

Exibindo o estado atual de todos os registradores da CPU:

(gdb) i registers
rax            0x1e                30
rbx            0x0                 0
rcx            0x0                 0
rdx            0x0                 0
rsi            0x0                 0
rdi            0x0                 0
rbp            0x0                 0x0
rsp            0x7fffffffe020      0x7fffffffe020
r8             0x0                 0
r9             0x0                 0
r10            0x0                 0
r11            0x0                 0
r12            0x0                 0
r13            0x0                 0
r14            0x0                 0
r15            0x0                 0
rip            0x401018            0x401018 <_start+24>
eflags         0x206               [ PF IF ]
cs             0x33                51
ss             0x2b                43
ds             0x0                 0
es             0x0                 0
fs             0x0                 0
gs             0x0                 0
fs_base        0x0                 0
gs_base        0x0                 0

Continuando a execução até o término:

(gdb) c
Continuing.
[Inferior 1 (process 222188) exited normally]

Notações de tamanhos de palavra

Tamanho Arquitetura x86-64 NASM / Assembly GDB (comando x)
1 byte byte byte b
2 bytes word word h (halfword)
4 bytes doubleword dword w (word)
8 bytes quadword qword g (giant word)

O GDB e a programação em C/C++

O GDB tem suporte avançado para C e C++, oferecendo:

  • Identificação de tipos, nomes, escopos e estruturas;
  • Acesso a variáveis locais, globais e parâmetros;
  • Depuração de ponteiros e aritmética de ponteiros;
  • Interação com structs, typedef, enum, union, etc.;
  • Acompanhamento de chamadas recursivas e stack frames;
  • Em C++: suporte a classes, herança, namespaces e templates.

Exemplo com um programa em C

Código-fonte (somac.c):

#include <stdio.h>

int soma(int a, int b) {
    int resultado = a + b;
    return resultado;
}

int main() {
    int x = 10, y = 20;
    int z = soma(x, y);
    printf("Resultado: %d\n", z);
    return 0;
}

Compilação:

gcc -g -o somac somac.c

Abrindo com o GDB e definindo o ponto de parada na função soma:

:~$ gdb ./somac
Reading symbols from ./somac...
(gdb) b soma
Breakpoint 1 at 0x1143: file somac.c, line 4.

Iniciando a execução do programa:

(gdb) r
Starting program: /home/blau/tmp/gdb/somac
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, soma (a=10, b=20) at somac.c:4
4	    int resultado = a + b;

Exibindo chamadas de funções na pilha:

(gdb) backtrace
#0  soma (a=10, b=20) at somac.c:4
#1  0x0000555555555178 in main () at somac.c:10

Executando passo a passo até a chamada da função printf:

(gdb) n
5	    return resultado;
(gdb) n
6	}
(gdb) n
main () at somac.c:11
11	    printf("Resultado: %d\n", z);
(gdb) backtrace
#0  main () at somac.c:11

Entrando na função printf (glibc):

(gdb) s
__printf (format=0x555555556004 "Resultado: %d\n") at ./stdio-common/printf.c:28
warning: 28	./stdio-common/printf.c: Arquivo ou diretório inexistente

Observando as chamadas de funções na pilha:

(gdb) backtrace
#0  __printf (format=0x555555556004 "Resultado: %d\n") at ./stdio-common/printf.c:28
#1  0x0000555555555194 in main () at somac.c:11

Continuando a execução até o término:

(gdb) c
Continuing.
Resultado: 30
[Inferior 1 (process 223980) exited normally]

Interface TUI