Artigo :: Começando com GDB

Introdução

Este guia completo cobre tudo o que você precisa para começar a depurar programas C com GDB, desde compilar com símbolos de depuração até inspecionar memória e controlar a execução do programa.

Visão Geral

GDB (GNU Debugger) é o depurador padrão para programas compilados com GCC. Ao contrário de ferramentas como strace que mostram chamadas de sistema, o GDB permite percorrer seu código-fonte linha por linha, inspecionar variáveis, definir breakpoints condicionais e até modificar o comportamento do programa durante a execução.

Seja rastreando um segfault, entendendo como o código executa, ou fazendo engenharia reversa de um binário, o GDB é uma ferramenta indispensável para qualquer desenvolvedor C/C++ ou pesquisador de segurança.

Pré-requisitos

Antes de começar, você deve:

Compilando com Símbolos de Depuração

Para depurar efetivamente com GDB, você precisa de símbolos de depuração no seu binário. Esses símbolos mapeiam código de máquina de volta para seu código-fonte (funções, variáveis, números de linha).

1
gcc -ggdb source.c -o prog_with_symbols

A flag -ggdb produz informações de depuração específicas para GDB com mais detalhes que a flag padrão -g.

Gerenciando Símbolos de Depuração

Removendo Símbolos de Binários

Para deploys em produção, remova símbolos de depuração para reduzir o tamanho do binário:

Usando strip (remove todos os símbolos):

1
strip --strip-debug --strip-unneeded prog_not_stripped -o prog_stripped

Usando objcopy (cria arquivo de debug separado):

1
objcopy --only-keep-debug binary_file debug_file

Esta abordagem mantém os símbolos em um arquivo separado, permitindo depurar problemas de produção sem distribuir binários grandes.

Adicionando Símbolos de Volta

Opção 1: Vincular arquivo de debug ao binário

1
objcopy --add-gnu-debuglink=debug_file binary_file

O GDB automaticamente encontrará e carregará debug_file ao depurar binary_file.

Opção 2: Carregar arquivo de símbolos manualmente no GDB

1
(gdb) symbol-file debug_file

Analisando Símbolos com nm

O comando nm lista símbolos em arquivos objeto e binários, útil para entender quais funções e variáveis existem em um programa.

Convenções de capitalização de símbolos:

Tipos Comuns de Símbolos

SímboloSignificado
TNa Seção de Texto (código)
DNa Seção de Dados Inicializados
BNa Seção de Dados Não-Inicializados (BSS)
UIndefinido (referência externa)
ASímbolo absoluto
NSímbolo de depuração

Comandos Úteis do nm

Procurar por um símbolo específico:

1
nm -A Binary_File | grep function_name

Exibir símbolos ordenados por endereço:

1
nm -n Binary_File

Mostrar apenas símbolos externos (globais):

1
nm -g Binary_File

Mostrar todos os símbolos incluindo símbolos de depuração:

1
nm -a Binary_File

Listar apenas símbolos na seção TEXT (código executável):

1
nm -a Binary_File | grep ' T '  # Espaços ao redor do T são importantes

Desmontando com objdump

O comando objdump desmonta binários, mostrando o código assembly real:

1
objdump -M intel -D a.out | grep -A20 main.:

Isso mostra 20 linhas de assembly começando da função main usando sintaxe Intel.


Referência de Comandos do GDB

Iniciando o GDB

1
2
gdb ./program           # Carregar programa
gdb --args ./program arg1 arg2   # Carregar com argumentos

Alternando Sintaxe (AT&T vs Intel)

Por padrão, o GDB usa sintaxe assembly AT&T. A sintaxe Intel é frequentemente mais legível:

1
(gdb) set disassembly-flavor intel

Adicione isso ao ~/.gdbinit para tornar permanente.

Desmontando Código

1
2
3
4
(gdb) disassemble main              # Desmontar função main
(gdb) disassemble /r main           # Mostrar opcodes também
(gdb) disassemble _start            # Desmontar _start
(gdb) disassemble 0x80484b0         # Desmontar endereço específico

Desmontar um intervalo:

1
(gdb) disassemble main,+30          # 30 bytes a partir de main

Dois argumentos (separados por vírgula) especificam um intervalo: início,fim ou início,+tamanho.

Executando o Programa

1
2
3
(gdb) run                    # Executar sem argumentos
(gdb) run arg1 arg2          # Executar com argumentos
(gdb) start                  # Executar e parar no main (aprimoramento do GEF)

Listando Código-Fonte

Requer arquivo-fonte disponível no mesmo diretório:

1
2
3
(gdb) list                   # Mostrar linhas próximas à posição atual
(gdb) list 1                 # Listar da linha 1
(gdb) list main              # Listar a função main

Inspecionando Estado do Programa

Informações de registradores:

1
2
(gdb) info registers         # Mostrar todos os registradores
(gdb) info registers eax     # Mostrar registrador específico

Listagem de funções:

1
(gdb) info functions         # Listar todas as funções

Arquivos-fonte:

1
2
(gdb) info sources           # Listar todos os arquivos-fonte
(gdb) info source            # Mostrar arquivo-fonte atual

Informações de variáveis:

1
2
(gdb) info variables         # Listar variáveis globais/estáticas
(gdb) info scope Function_Name   # Listar variáveis locais na função

Dump da tabela de símbolos:

1
2
(gdb) maintenance print symbols              # Imprimir no console
(gdb) maintenance print symbols output.txt   # Imprimir em arquivo

Trabalhando com Breakpoints

Definir breakpoints:

1
2
3
(gdb) break main             # Break na função
(gdb) break 42               # Break na linha 42
(gdb) break *0x080484b0      # Break no endereço

Listar breakpoints:

1
(gdb) info breakpoints       # Mostrar todos os breakpoints

Habilitar/desabilitar breakpoints:

1
2
(gdb) disable 1              # Desabilitar breakpoint #1
(gdb) enable 1               # Habilitar breakpoint #1

Deletar breakpoints:

1
2
(gdb) delete 1               # Deletar breakpoint #1
(gdb) delete                 # Deletar todos os breakpoints

Modificando Memória e Registradores

O GDB permite modificar o estado do programa durante a execução:

1
2
3
4
(gdb) set {char} 0xbffff7e6 = 'B'              # Escrever byte na memória
(gdb) set {int} (0xbffff7e6 + 1) = 66          # Escrever int na memória
(gdb) set var1 = 100                            # Modificar variável
(gdb) set $eax = 10                             # Modificar registrador

Opcodes também podem ser modificados:

1
(gdb) set {char} 0x080484b0 = 0xb8              # Escrever opcode

Definindo Macros

Crie comandos personalizados que executam automaticamente:

1
2
3
4
5
(gdb) define hook-stop
    info registers
    x/8xw $esp
    disassemble $eip,+10
end

Isso executa após cada passo, mostrando registradores, pilha e próximas instruções. Salve em ~/.gdbinit para usar em todas as sessões.

Trabalhando com Dados

Imprimir valores de registradores em diferentes formatos:

1
2
3
(gdb) print /d $eax          # Decimal
(gdb) print /t $eax          # Binário
(gdb) print /x $eax          # Hexadecimal

Exemplo de saída:

(gdb) print /d $eax
$17 = 13
(gdb) print /t $eax
$18 = 1101
(gdb) print /x $eax
$19 = 0xd

Examinar memória (x/nyz):

Formato: x/[quantidade][formato][tamanho] endereço

Exemplos:

1
2
3
(gdb) x/8xw $esp             # Mostrar 8 words em hex do stack pointer
(gdb) x/s 0x08048000         # Mostrar string no endereço
(gdb) x/20i $eip             # Mostrar 20 instruções no instruction pointer

Variáveis de Conveniência

Crie variáveis temporárias para depuração:

1
2
3
4
(gdb) set $i = 10
(gdb) set $dyn = (char *)malloc(10)
(gdb) set $demo = "psylinux"
(gdb) set argv[1] = $demo                      # Modificar argumentos do programa

Essas variáveis existem apenas na sessão do GDB.

Chamando Funções

Você pode chamar funções no programa sendo depurado:

1
2
3
4
(gdb) info functions                            # Listar funções disponíveis
(gdb) call Function_1(args_list)               # Chamar função personalizada
(gdb) call strlen("psylinux")                  # Chamar biblioteca padrão
(gdb) call strcpy($dyn, argv[1])              # Usar variáveis de conveniência

Pontos-Chave

Exercícios Práticos

Leitura Adicional