Depuração com GDB: Primeiros passos

Shutterstock / Nicescene
O travamento do aplicativo não precisa ser o fim da jornada! Aprenda os fundamentos do uso do GDB, o poderoso GNU Debugger e saiba como depurar core dumps no Linux. Ideal para usuários finais e também para recém-chegados.
O que é GDB?
A ferramenta GDB é um utilitário de depuração antigo e altamente respeitado no Linux GNU Toolset. Ele fornece sua própria linha de comando, uma ampla gama de comandos e funções e execução de programa passo a passo (código de computador) e até mesmo funcionalidade de modificação.
O desenvolvimento no GDB começou em 1986-1988 e, em 1988, a ferramenta tornou-se parte da Free Software Foundation. É totalmente gratuito e pode ser instalado facilmente em todas as principais distribuições do Linux.
Se você gosta do Windows, talvez goste de ler os despejos de memória do Windows: para que servem exatamente? em vez disso!
Para usuários Linux, é importante entender onde o GDB se encaixa no fluxo do processo ao considerar bugs e erros do computador. Existem três cenários possíveis. Em primeiro lugar, pode haver um usuário final enfrentando uma falha de aplicativo que gostaria de aprender um pouco mais sobre o que aconteceu e descobrir se o bug já é conhecido pela comunidade, etc.
Esta é uma situação comum, e a maioria dos usuários avançados, em um ponto ou outro, se verá depurando uma falha do aplicativo. Conhecer o GDB ajuda tremendamente nessa tarefa. Mais sobre isso abaixo.
O segundo cenário é um profissional (por exemplo, um consultor de TI ou engenheiro de teste) travando com um aplicativo que ele também oferece suporte ou mantém. Nesses casos, o engenheiro provavelmente desejará depurar o travamento visto, especialmente se for um engenheiro de teste, por exemplo, obter um backtrace (uma visão geral) de quais funções estavam em execução no momento do travamento, etc. Isso pode ajudar a definir o bug melhor e pode ajudar a restringir um caso de teste.
O terceiro cenário é o de um desenvolvedor, que desejará usar GDB em um nível mais profissional para, por exemplo, definir pontos de interrupção, depurar relógios de variáveis, fazer análises de despejo de núcleo, etc. Embora o escopo deste artigo seja um pouco leve para esses profissionais, haverá um artigo GDB mais aprofundado a seguir. E, se você é um desenvolvedor e nunca trabalhou com GDB, continue lendo.
O que é um Core Dump?
Se você já assistiu Star Trek e ouviu o Capitão Picard (ou Janeway!) dar instruções para “ despejar o núcleo do warp ” você terá uma imagem bastante boa de como pode ser um dump de memória. Era basicamente o componente principal (o núcleo de dobra) sendo ejetado como resultado de alguma falha ou problema percebido. Provavelmente foi um trocadilho com o Linux Core Dumping.
Com toda a diversão à parte, um despejo de memória é simplesmente um arquivo gerado com o estado (total ou parcial) (e memória) de um aplicativo, no momento em que ele travou.
Um core dump é um arquivo binário, que só pode ser lido por um depurador. O GDB é um depurador e um dos melhores. O dump principal pode ser escrito pelo próprio aplicativo com falha (não comumente empregado, embora seja possível e algum software de escala de servidor maior possa usar isso), mas é mais frequentemente escrito pelo próprio sistema operacional.
Alguns sistemas operacionais têm core dumping desabilitado por padrão ou minimizam os core dumps para um minidump, o que pode ajudar na depuração do aplicativo, mas provavelmente será muito mais limitado do que um despejo de núcleo completo. Então, novamente, um dump de núcleo completo pode ser problemático; por exemplo, se você tiver um sistema com 128 GB de memória e seu aplicativo estiver usando a maior parte dessa memória, um dump de memória (o arquivo no disco) pode ter aproximadamente o mesmo tamanho.
A configuração de core dumps em seu sistema operacional específico está fora do escopo deste artigo, mas essas informações são relativamente fáceis de encontrar online. Basta pesquisar em seu mecanismo de pesquisa favorito por uma frase como & # 8216; Configurar core dumps no Linux Mint ’, substituindo & # 8216; Mint &’ pelo nome do seu sistema operacional Linux.
Dependendo do seu sistema operacional e da configuração atual, pode ser necessário alguns ajustes e edição nos arquivos de configuração, algumas reinicializações e, às vezes, algumas soluções leves de problemas, mas uma vez definido, você &’ ser capaz de usar o GDB contra os dumps centrais escritos sempre que um aplicativo trava.
Observe que habilitar a gravação de core dumps em seu sistema operacional estará sempre limitado a alterar as definições de configuração em arquivos de configuração existentes (ou novos). Nenhum outro aplicativo precisa ser instalado para que os core dumps sejam ativados e gravados sempre que um aplicativo trava. A ferramenta GDB, entretanto, precisa ser instalada, mas estará disponível no repositório de aplicativos principal da sua distribuição Linux &’ por padrão.
Instalando GDB
Para instalar o GDB em sua distribuição Linux baseada em Debian / Apt (como Ubuntu e Mint), execute o seguinte comando em seu terminal:
sudo apt install gdb
Para instalar o GDB em sua distribuição Linux baseada em RedHat / Yum (como RHEL, Centos e Fedora), execute o seguinte comando em seu terminal:
sudo yum install gdb
Locais de despejo principal e reprodutibilidade do problema
Depois de habilitar o core dumps e instalar o GDB, é hora de encontrar e ler o core dump (o arquivo gerado pelo sistema operacional quando o aplicativo trava) com o GDB.
Se você configurou seu sistema para core dumps depois que seu aplicativo travou, é um pouco provável que nenhum core dump esteja disponível para o travamento anterior. Tente e execute as mesmas etapas em seu aplicativo para reproduzir a falha / problemas.
Depois que isso acontecer, verifique o local padrão para core dumps em sua distribuição Linux (como / var / crash ou / var / lib / systemd / coredump / no Ubuntu / Mint e Centos ou / var / spool / abrt no RedHat). Regularmente, e às vezes dependendo das configurações feitas, um dump de memória também pode ter sido gravado no diretório onde o binário (o aplicativo) reside (provavelmente) ou no diretório de trabalho principal (um pouco menos provável).
Pela ambigüidade da verborragia, você pode ter a impressão de que perseguir core dumps pode ser um tanto ilusório. Essa seria uma avaliação precisa. Considerando que a ferramenta GDB em si é muito estável, resiliente e madura, escrever core dumps é um esforço muito mais casual. Existem várias razões para isso, a principal é que a maioria das principais distribuições do Linux tem diferentes implementações de comportamento de dumping de núcleo e uma variedade de definições de configuração correspondentes.
A escrita de core dumps também afeta, de forma bastante significativa, a segurança; depois que toda a memória principal do computador, total ou parcialmente, é gravada no dump, permitindo que os usuários do GDB leiam informações potencialmente confidenciais. Além disso, conforme explicado anteriormente, às vezes a gravação do core dump pode ser limitada pelos recursos do sistema.
Finalmente, estamos lidando com um aplicativo travando em um estado desconhecido e nem sempre é possível gravar tal estado no disco. É justo dizer que se pode esperar gastar algum tempo para fazer com que o core dumping funcione de forma confiável e persistente em um determinado sistema. Isso nos leva ao tópico da reprodutibilidade do problema.
Se você tiver um problema que é reproduzível de forma consistente, como seu computador sempre travando quando você reproduz um arquivo de música, então a configuração de core dumps e depuração do mesmo usando GDB faz muito sentido. Certa vez, descobri um bug em um driver de áudio dessa forma. Se você é um engenheiro de teste e está testando repetidamente um determinado programa, faz sentido configurar despejos de memória e usar GDB ainda mais.
No entanto, se houver uma única instância de falha do aplicativo e o problema não puder ser reproduzido imediatamente, você terá que fazer uma escolha. Se você deseja estar preparado para o próximo travamento do aplicativo, e / ou se o aplicativo for muito importante para você (por exemplo, para a continuidade dos negócios), você deve configurar pelo menos core dumps para que na próxima vez que o aplicativo travar, um o despejo de núcleo é gerado. O GDB pode ser instalado mesmo após a geração de um dump de memória.
Lendo o Core Dump com GDB
Agora que você tem um dump principal disponível para um determinado aplicativo com falha e instalou o GDB, pode invocar facilmente o GDB:

gdb ./myapp ./core
Aqui, temos um aplicativo chamado myapp que falha assim que é iniciado. Neste sistema Ubuntu, os core dumps foram ativados simplesmente definindo ulimit -c para um número maior como root e, em seguida, iniciando o aplicativo. O resultado é um dump de núcleo gerado como ./core.
Invocamos o gdb com duas opções. A primeira opção é o aplicativo / binário / executável que gerou um travamento. O segundo é o core dump gerado pelo sistema operacional como resultado da falha do aplicativo.
Análise inicial do Core Dump
Quando o GDB é iniciado, como pode ser visto na saída do GDB na imagem acima, ele fornece uma infinidade de informações. A revisão cuidadosa pode nos fornecer muitas informações sobre o problema que seu sistema enfrentou resultando na falha do aplicativo.
Por exemplo, notamos imediatamente que o programa foi encerrado devido a um erro SIGFPE, uma exceção aritmética. Também confirmamos que o GDB identificou corretamente que o executável pelo Core foi gerado pela linha `./myapp '.
Também vemos a linha / noção interessante Nenhum símbolo de depuração encontrado em ./myapp, indicando que os símbolos de depuração não puderam ser lidos do binário do aplicativo. Símbolos de depuração é onde as coisas podem ficar confusas rapidamente. Os binários mais otimizados / de nível de lançamento (que seriam a maioria dos aplicativos que você executa no dia a dia) terão as informações do símbolo de depuração retiradas do binário resultante para economizar espaço e aumentar os tempos de execução / eficiência de trabalho do aplicativo.
Dependendo de quanto foi retirado do binário otimizado resultante, mesmo nomes de funções simples podem não estar disponíveis. Em nosso caso, os nomes das funções ainda estão visíveis, como pode ser observado na referência do nome da função do_the_maths (). No entanto, nenhuma variável está visível e é a isso que o GDB se referia com os símbolos No debugging encontrados na nota ./myapp. Quando os nomes das funções não estão disponíveis, as referências aos nomes das funções serão processadas como ?? em vez do nome da função.
Podemos ver, no entanto, qual é o nome do quadro / função com falha: # 0 do_the_maths (). Você pode estar se perguntando o que é um quadro. A melhor maneira de descrever e pensar sobre um quadro é pensar sobre funções, por exemplo do_the_maths (), em um programa de computador. Um único quadro é uma única função.
Assim, se um programa passa por várias funções, por exemplo, a função main () em um programa C ou C ++ que, por sua vez, pode chamar uma função chamada math_function () que finalmente chama do_the_maths () levaria a três quadros, com o quadro # 0 (o quadro resultante e com falha) sendo a função do_the_maths (), o quadro # 1 sendo math_function () e o quadro # 2 (o primeiro quadro chamado, com o número mais alto ) sendo a função principal ().
Não é incomum ver uma pilha de 10-20 frames em alguns programas de computador, e uma pilha ocasional de 40 ou 50 frames é possível, por exemplo, em um software de banco de dados. Observe que a ordem dos quadros é inversa; travando o quadro com o quadro número 0 primeiro, depois subindo de volta ao primeiro quadro. Isso faz sentido quando você pensa da perspectiva do depurador / core dump; começou no ponto em que travou, depois voltou aos frames até a função main ().
O termo pilha de quadros agora deve ser mais autoexplicativo; uma pilha de quadros, do mais específico (ou seja, do_the_maths) ao menos específico (ou seja, main ()) que nos guiará na avaliação do que aconteceu. Então, podemos ver essa pilha / pilha de quadros no GDB para nosso problema atual? Claro que podemos.
Hora do Backtrace!
Assim que chegarmos ao prompt do gdb, podemos emitir um comando backtrace bt. Este comando irá – apenas para o thread atual (que geralmente é o thread com falha; o GDB detecta automaticamente os threads com falha e nos coloca automaticamente nesse thread, embora nem sempre esteja correto) – despeja um backtrace da pilha de quadros, que discutimos acima. Em outras palavras, poderemos ver o fluxo das funções pelas quais o programa passou até o momento em que travou.

Aqui nós executamos o comando bt, que nos dá uma boa pilha de chamadas, ou pilha de quadros, ou pilha, ou backtrace – qualquer palavra que você preferir, todas significam exatamente a mesma coisa. Podemos ver que main () chamou math_function que por sua vez chamou a função do_the_maths ().
Como podemos ver, os nomes das variáveis não são exibidos nesta saída em particular e o GDB nos notificou que os símbolos de depuração não estavam disponíveis. Embora o binário resultante ainda tivesse algum nível de informações de depuração disponíveis, já que todos os nomes de quadros / funções eram exibidos corretamente e nenhum quadro era renderizado como ??.
Portanto, recompilei o aplicativo do código-fonte usando a opção -ggdb para gcc (como em gcc -ggdb ...) e observe como há muito mais informações disponíveis:

Desta vez, podemos ver claramente que o GDB está lendo símbolos (conforme indicado por Lendo símbolos de ./myapp ...) e que podemos ver nomes de variáveis como x e y. Desde que o código-fonte esteja disponível, podemos até ver a linha do código-fonte exata onde o problema aconteceu (conforme indicado por 3 o = x / y;). Também podemos ver as linhas do código-fonte para todos os frames.
Estudando um pouco a saída, percebemos imediatamente o que está errado. A variável o estava sendo definida para a variável x sendo dividida por y. No entanto, olhando um pouco mais de perto a entrada da função &’ (mostrada por (x = 1, y = 0)), vemos que o aplicativo tentou dividir um número por zero, o que é uma impossibilidade matemática, levando ao Sinal SIGFPE (exceção aritmética).
Nosso problema, portanto, não é diferente de digitar 1 dividido por 0 em sua calculadora; ele também reclamará, mas não travará 😉
Nesse caso, uma solução alternativa pode ser concebida fornecendo uma entrada diferente para o aplicativo (onde você está tentando contornar uma determinada falha e não tem o código-fonte disponível) – por exemplo, você pode tentar inserir 0,000000001 em vez de 0 e aceitar um pequeno ajuste de arredondamento ou – desde que o código-fonte esteja disponível e você esteja melhorando o código-fonte como desenvolvedor – você poderia adicionar algum código adicional em seu programa para fornecer entradas erradas para a variável y.
Como você pode ver, o GDB é uma ferramenta muito versátil, na variedade de funções e situações em que uma pessoa pode se encontrar. Além disso, nós apenas arranhamos a superfície do que o legal pode fazer. Também depende exatamente da situação e das informações ao redor (é um dump de núcleo disponível, temos símbolos de depuração, etc.) até onde se pode levar a jornada do GDB em cada instância. Mas uma coisa é clara; é muito mais provável depurar uma determinada situação de forma mais completa se, quando e como core dumps e GDB são usados.
Concluindo
Neste artigo, apresentamos o GDB, o depurador GNU que pode ser facilmente instalado e usado em qualquer distribuição Linux importante. Discutimos a necessidade de configurar dumps de núcleo primeiro no sistema de destino e suas complexidades. Vimos como instalar o GDB, permitindo-nos ler e processar os core dumps gerados.
A seguir, revisamos a interface básica entre o dump principal e o usuário ou desenvolvedor, e fornecemos um exemplo prático de uma análise real, observando o comando bt backtrace. Também discutimos compilações de aplicativos instrumentados otimizados versus depuração [símbolo] e como isso afeta o nível de visibilidade das informações dentro do GDB.
Em um artigo futuro, iremos nos aprofundar no GDB e explorar o uso mais avançado do GDB.
Aproveite a depuração!
Nenhum comentário