#+title: Curso prático de introdução ao GDB #+author: Blau Araujo #+email: blau@debxp.org * 1. Primeiro contato guiado ** Objetivos - Entender o que é o GDB; - Conhecer o conceito de /depuração/; - Demonstrar o GDB em ação. ** 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. ** O que é depurar (/debugar/) Depuração (ou _/debugging/_) é o processo de identificar, analisar e corrigir erros (/bugs/) em um programa ou sistema. Durante a depuração, o programador examina o comportamento do código para entender onde e por que ele não funciona como esperado. O objetivo é encontrar a origem dos problemas e corrigi-los, até que o software opere corretamente. Sem o auxílio de ferramentas especializadas, a depuração pode ser feita, por exemplo: - Com uma revisão cuidadosa do que está escrito no código-fonte; - Inserindo a exibição de mensagens em pontos suspeitos do código para avaliar variáveis (/"o valor está correto neste ponto?"/) e o próprio fluxo de execução (/"o programa chegou a este ponto?"/); - Com a análise manual de algoritmos, seguindo a execução do programa no código tendo em mente alguns exemplos de entrada. Mas existem ferramentas (como o GDB) que auxiliam o processo de depuração que, sem alterar diretamente o código, oferecem formas de: - Definir pontos de parada (/breakpoints/); - Monitorar a alteração de valores em variáveis (/whatchpoints/); - Executar o programa passo a passo; - Examinar dados em endereços específicos da memória através de /símbolos/; - Examinar o conteúdo da pilha de chamadas de sistema; - Explorar informações sobre o processo do programa na memória... Entre várias outras possibilidades. Também existem ferramentas especializadas em aspectos e métodos específicos da depuração, como... - *Valgrind:* para examinar a memória e detectar vazamentos e acessos inválidos. - *Linters e analisadores:* para analisar como o código-fonte foi escrito e apontar erros de sintaxe, de estilo e violações de especificações e convenções da linguagem em uso sem, sequer, compilar (se for o caso) e executar o código. *** Uma nota sobre depuração preventiva Alguns /ambientes integrados de desenvolvimento/ (IDE) e editores de código oferecem meios para sinalizar erros de sintaxe (a falta de um =;= em C, por exemplo) ou destaques coloridos para diferentes tipos de elementos da linguagem. Muitos deles integram-se com /protocolos de servidores de linguagem/ (LSP) para funcionarem como /linters/ e analisadores de código em tempo real (ou seja, durante a digitação do código), o que antecipa e ajuda a evitar potenciais causas de erros. ** Uma pequena demonstração 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 #+begin_quote Sem os símbolos de depuração, nós teríamos que descobrir quais seriam os endereços de dados em variáveis e de outros elementos do código para inspecionar seus valores e comportamentos. #+end_quote *** Iniciando o GDB para depuração O GDB pode ser iniciado de várias formas para depurar programas. A mais comum, porém, é através da passagem do caminho de um binário executável como argumento na sua invocação na linha de comandos. No nosso exemplo: #+begin_example gdb ./demo #+end_example Sem a opção =-q= (/quiet/) ou sem configurações de início, o GDB exibe informações de versão e /copyright/ antes das mensagens relativas à depuração, propriamente dita, e do prompt de seu shell de comandos... #+begin_example :~$ gdb ./demo GNU gdb (Debian 16.3-1) 16.3 Copyright (C) 2024 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later 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: . Find the GDB manual and other documentation resources online at: . For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./demo... (gdb) #+end_example Apesar da poluição visual, essa mensagem padrão pode ser útil para iniciantes, especialmente este trecho sobre ajuda: #+begin_example For help, type "help". Type "apropos word" to search for commands related to "word"... #+end_example Ou seja... - Com o comando =help= (ou apenas =h=), nós teremos uma lista dos tópicos de ajuda (classes de comandos) e as instruções de uso do próprio comando =help=; - Com o comando =apropos PALAVRA=, nós podemos buscar comandos que casem com =PALAVRA=, que é interpretada como uma expressão regular (REGEX). *** Terminando o GDB Para sair do GDB, nós podemos teclar =Ctrl+D= ou executar =quit= (ou apenas =q=) no prompt do shell de comandos: #+begin_example (gdb) quit #+end_example *** Omitindo a mensagem de início A forma mais simples de omitir a mensagem de início é utilizando a opção =-q= na invocação do GDB: #+begin_example :~$ gdb ./demo Reading symbols from ./demo... (gdb) #+end_example Desta forma, a primeira mensagem relevante para a depuração é a única a ser exibida antes do prompt: #+begin_example Reading symbols from ./demo... #+end_example Ela diz que o nosso binário foi compilado com símbolos para depuração e que esses símbolos foram lidos e registrados. Mas, vamos sair e compilar novamente o programa para ver o que aconteceria... #+begin_example (gdb) q :~$ gcc -o demo demo.c :~$ gdb ./demo Reading symbols from ./demo... (No debugging symbols found in ./demo) (gdb) #+end_example Desta vez, sem os símbolos de depuração, nós tivemos a mensagem: #+begin_example Reading symbols from ./demo... (No debugging symbols found in ./demo) #+end_example *** Uma nota sobre compilação com símbolos de depuração Normalmente, os projetos são compilados para duas finalidades: desenvolvimento e publicação (/release/). É na compilação em desenvolvimento que os símbolos de depuração podem ser úteis. Embora isso não cause prejuízos de desempenho, binários compilados e publicados com símbolos de depuração resultam em arquivos maiores e, dependendo do caso, podem expor informações internas sensíveis. Isso não costuma ser relevante no contexto do Software Livre, já que o acesso aos fontes deve ser garantido, mas existem casos de programas escritos para uso interno em alguma cadeia de produção que vão requerer mais cuidados. #+begin_quote É possível compilar o código com os símbolos de depuração e, depois, removê-los do binário que será distribuído(=strip -g=), ou ainda, gerar e armazenar cópias dos símbolos em arquivos separados (=objcopy --only-keep-debug=), mas isso foge do nosso escopo de interesses. #+end_quote Sendo assim, vamos sair e compilar novamente o nosso programa com os símbolos de depuração. #+begin_example (gdb) q :~$ gcc -g -o demo demo.c :~$ gdb ./demo Reading symbols from ./demo... (gdb) #+end_example *** Listando o código-fonte No GDB, podemos listar o código-fonte com o 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