18 KiB
Curso prático de introdução ao GDB
- Parte 1: Conceitos Básicos
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]