#+title: Curso prático de introdução ao GDB #+author: Blau Araujo #+email: blau@debxp.org * 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=): #+begin_src c #include 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; } #+end_src Compilação com símbolos de depuração: #+begin_example gcc -g -o demo demo.c #+end_example Carregando o binário no GDB: #+begin_example gdb ./demo #+end_example *** Listando o código-fonte No GDB, podemos listar o código-fonte (comando =list=): #+begin_example (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); #+end_example Por padrão, somente 10 linhas são exibidas, mas podemos teclar =Enter= algumas vezes até que todo o código seja listado. #+begin_example (gdb) 13 return 0; 14 } #+end_example Chagando ao final, podemos reiniciar a listagem com =list .=: #+begin_example (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); #+end_example *** Ponto de parada e execução Nós podemos definir pontos de parada com o comando =break=: #+begin_example (gdb) break main Breakpoint 1 at 0x115b: file demo.c, line 9. #+end_example Assim, quando o programa for executado (com o comando =run=), a execução será pausada nos símbolos definidos como pontos de parada: #+begin_example (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; #+end_example 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=: #+begin_example (gdb) next 10 int y = 20; #+end_example 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: #+begin_example (gdb) 11 int z = soma(x, y); #+end_example Neste ponto, as variáveis =x= e =y= já foram carregadas e nós podemos conferir seus valores com o comando =print=: #+begin_example (gdb) print x $1 = 10 (gdb) print y $2 = 20 #+end_example #+begin_quote O resultado de cada avaliação é armazenado no GDB em uma variável especial numerada (=$n=) de acordo com a ordem da avaliação. #+end_quote 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=: #+begin_example (gdb) step soma (a=10, b=20) at demo.c:4 4 int resultado = a + b; #+end_example Aqui, nós podemos inspecionar os valores de =a=, =b= e do valor inicial de =resultado=, antes da linha =4= ser executada: #+begin_example (gdb) print a $3 = 10 (gdb) print b $4 = 20 (gdb) print resultado $5 = 0 #+end_example Com o comando =next=, nós avançamos na função e já podemos exibir o novo valor de =resultado=: #+begin_example (gdb) next 5 return resultado; (gdb) print resultado $6 = 30 #+end_example Se, a partir daqui, nós quisermos executar todo o restante do programa, basta executar comando =continue=: #+begin_example (gdb) continue Continuing. Resultado: 30 [Inferior 1 (process 214693) exited normally] #+end_example Para sair do GDB... #+begin_example (gdb) quit :~$ #+end_example ** Instalação *** Debian e derivados #+begin_example sudo apt update sudo apt install gdb #+end_example O pacote =build-essential= instala outras ferramentas úteis para desenvolvimento, incluindo o =gcc= (/GNU Compiler Collection/), o =make= e as dependências mais comuns. #+begin_example sudo apt install build-essential #+end_example *** Fedora e derivados #+begin_example sudo dnf install gdb #+end_example Para um ambiente completo de desenvolvimento: #+begin_example sudo dnf groupinstall "Development Tools" #+end_example *** Arch Linux e derivados #+begin_example sudo pacman -S gdb #+end_example O grupo =base-devel= contém ferramentas úteis para compilação e depuração: #+begin_example sudo pacman -S base-devel #+end_example *** Verificação da instalação Versão: #+begin_example gdb --version #+end_example Ajuda: #+begin_example gdb --help #+end_example ** 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: #+begin_src gdb 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 #+end_src Também pode ser interessante omitir as mensagens de versão no início do GDB, o que é feito no arquivo =~/.config/gdb/gdbearlyinit=: #+begin_example set startup-quietly on #+end_example #+begin_quote 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. #+end_quote *** 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: #+begin_example :~/projeto$ gdb Warning: File ".gdbinit" auto-loading has been declined by your `auto-load safe-path'... #+end_example Para permitir o carregamento, temos que adicionar o caminho do projeto ao arquivo do usuário: #+begin_src sh echo "add-auto-load-safe-path $(pwd)" >> ~/.gdbinit #+end_src #+begin_quote Isso é muito utilizado para definir /breakpoints/ automáticos, carregar símbolos extras, configurar scripts, etc. #+end_quote *** 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: #+begin_example define COMANDO LISTA DE COMANDOS end document COMANDO DESCRIÇÃO end #+end_example *** 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=: #+begin_example :~$ 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 #+end_example *** 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=: #+begin_example :~$ gcc -o demo demo.c :~$ file demo demo: ELF 64-bit LSB pie executable [...] not stripped #+end_example Com a opção =-g=: #+begin_example :~$ gcc -g -o demo demo.c :~$ file demo demo: ELF 64-bit LSB pie executable [...] with debug_info, not stripped #+end_example 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: #+begin_example :~$ 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 #+end_example Mas o próprio GDB informa se há símbolos de depuração ao iniciar... Sem a opção =-g=: #+begin_example :~$ gcc -o demo demo.c :~$ gdb demo Reading symbols from demo... (No debugging symbols found in demo) #+end_example Com a opção =-g=: #+begin_example :~$ gcc -g -o demo demo.c :~$ gdb demo Reading symbols from demo... #+end_example ** 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=): #+begin_src 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 #+end_src Montagem e link-edição: #+begin_example nasm -f elf64 -g soma.asm -o soma.o ld soma.o -o soma #+end_example Abertura com o GDB: #+begin_example gdb ./soma Reading symbols from soma... #+end_example Definindo =_start= como ponto de parada: #+begin_example (gdb) b _start Breakpoint 1 at 0x401000: file soma.asm, line 10. #+end_example Listando o fonte: #+begin_example (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 #+end_example Executando: #+begin_example (gdb) r Starting program: /home/blau/tmp/gdb/soma Breakpoint 1, _start () at soma.asm:10 10 mov rax, [a] #+end_example Observando os dados rotulados como =a=, =b= e =resultado= (assembly não tem variáveis): #+begin_example (gdb) x/1gx &a 0x402000 : 0x000000000000000a (gdb) x/1gx &b 0x402008 : 0x0000000000000014 (gdb) x/1gx &resultado 0x402010 : 0x0000000000000000 #+end_example #+begin_quote 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=). #+end_quote Executando as três instruções seguintes: #+begin_example (gdb) n 11 add rax, [b] (gdb) 12 mov [resultado], rax (gdb) 15 mov rax, 60 ; syscall: exit #+end_example Observando o no valor em =resultado=: #+begin_example (gdb) x/1gx &resultado 0x402010 : 0x000000000000001e #+end_example Exibindo o estado atual de todos os registradores da CPU: #+begin_example (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 #+end_example Continuando a execução até o término: #+begin_example (gdb) c Continuing. [Inferior 1 (process 222188) exited normally] #+end_example *** 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=): #+begin_src c #include 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; } #+end_src Compilação: #+begin_example gcc -g -o somac somac.c #+end_example Abrindo com o GDB e definindo o ponto de parada na função =soma=: #+begin_example :~$ gdb ./somac Reading symbols from ./somac... (gdb) b soma Breakpoint 1 at 0x1143: file somac.c, line 4. #+end_example Iniciando a execução do programa: #+begin_example (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; #+end_example Exibindo chamadas de funções na pilha: #+begin_example (gdb) backtrace #0 soma (a=10, b=20) at somac.c:4 #1 0x0000555555555178 in main () at somac.c:10 #+end_example Executando passo a passo até a chamada da função =printf=: #+begin_example (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 #+end_example Entrando na função =printf= (=glibc=): #+begin_example (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 #+end_example Observando as chamadas de funções na pilha: #+begin_example (gdb) backtrace #0 __printf (format=0x555555556004 "Resultado: %d\n") at ./stdio-common/printf.c:28 #1 0x0000555555555194 in main () at somac.c:11 #+end_example Continuando a execução até o término: #+begin_example (gdb) c Continuing. Resultado: 30 [Inferior 1 (process 223980) exited normally] #+end_example ** Interface TUI