Header Ads

Depurando com GDB: Indo mais fundo

Shutterstock / Nicescene

O poderoso GNU Debugger GDB retorna ao palco principal. Nós mergulhamos mais fundo em pilhas, backtraces, variáveis, core dumps, frames e depuração do que nunca. Junte-se a nós para uma introdução totalmente nova e mais avançada ao GDB.

O que é GDB?

Se você é novo na depuração em geral, ou no GDB — o GNU Debugger — em particular, você pode querer ler primeiro o nosso artigo Depuração com GDB: Primeiros passos e depois voltar a este. Este artigo continuará a se basear nas informações apresentadas lá.

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

Pilhas, retrocessos e molduras!

Parece maçãs, tortas e panquecas! (E até certo ponto, é.) Assim como maçãs e panquecas nos alimentam, pilhas, retrocessos e quadros são o pão com manteiga de todos os desenvolvedores que depuram no GDB, e as informações apresentadas neles alimentam ricamente um desenvolvedor faminto por descobrir seu ou seu bug no código-fonte.

RELACIONADO Depuração com GDB: primeiros passos

O comando bt GDB irá gerar um backtrace de todas as funções que foram chamadas, uma após a outra, e nos apresentar os frames (as funções) listados, um após o outro. Uma pilha é bastante semelhante a um backtrace no sentido de que uma pilha é uma visão geral ou lista de funções que levaram a um travamento, situação ou problema, enquanto um backtrace é o comando que emitimos para obter uma pilha.

Dito isto, frequentemente, os termos são usados ​​indistintamente e pode-se dizer “ Você pode me dar uma pilha? ” ou “ Vamos ver o histórico, ” que inverte um pouco o significado de ambas as palavras em cada frase, respectivamente.

E como uma atualização do nosso artigo anterior sobre GDB, um quadro é basicamente uma única função listada em um backtrace de todas as chamadas de função aninhadas — por exemplo, o main () função começando primeiro (listada no final de um backtrace), e então main () chamada math_function (), que por sua vez chamada do_the_maths () etc.

Se isso parece um pouco complicado, dê uma olhada em Debugging with GDB: Getting Started first.

Para programas de thread único, o GDB irá, como sempre (se não sempre), descobrir corretamente o thread que está travando (e apenas) quando começarmos nossa aventura de depuração. Isso facilita a execução imediata do comando bt quando entramos em gdb e nos encontramos no prompt (gdb), pois o GDB nos mostrará imediatamente o backtrace relevante para a falha que observamos.

Single-threaded ou Multithreaded?

Uma questão muito importante a observar (e saber) ao depurar core dumps é se o programa que está sendo depurado é (ou talvez mais especificamente, era) de thread único ou multithread?

Em nosso exemplo / artigo anterior, demos uma olhada em um backtrace simples, com um conjunto de frames mostrando a partir de um programa escrito por ele mesmo. O programa era de thread único: nenhum outro thread de execução foi bifurcado no código.

No entanto, assim que tivermos vários encadeamentos, um único comando bt (backtrace) produzirá apenas o backtrace para o encadeamento atualmente selecionado no GDB.

O GDB selecionará automaticamente a falha de travamento, e mesmo para programas multithread, isto é, 99% + das vezes, feito corretamente. Existem apenas instâncias ocasionais em que o GDB confundirá o encadeamento com falha com outro. Por exemplo, isso pode acontecer se o programa travar em dois threads ao mesmo tempo. Nos últimos 10 anos, observei isso apenas menos de um punhado de vezes ao lidar com milhares de despejos de memória.

Para demonstrar a diferença entre o exemplo usado em nosso último artigo e um verdadeiro aplicativo multithread, eu construí o servidor MySQL 8.0.25 em modo de depuração (em outras palavras, com símbolos de depuração / instrumentação adicionados) usando o script de construção no MariaDB -a GitHub repo e executou os dados SQL da estrutura pquery nele por um tempo, o que em breve travou o servidor de depuração do MySQL.

Como você deve se lembrar do nosso artigo anterior, um dump de núcleo é um arquivo produzido pelo sistema operacional ou, em alguns casos, pelo próprio aplicativo (se tiver tratamento de travamento / disposições core-dumping embutidas), que podem então ser analisadas usando GDB. Um arquivo principal é geralmente escrito como um arquivo de privilégios restritos (para proteger as informações confidenciais contidas na memória), e você provavelmente precisará usar sua conta de superusuário (ou seja, root) para acessá-lo.

Vamos mergulhar direto no dump principal produzido com gdb bin / mysqld $ (ls data / * core *):

E alguns segundos depois, o GDB termina de carregar e nos leva ao prompt do GDB:

As várias novas mensagens LWP (que eram ainda mais numerosas na saída completa) dão uma boa dica de que este programa era multithread. O termo LWP significa Processo de Peso Leve. Você pode pensar nisso como sendo equivalente a um único thread cada, juntos fazendo uma lista de todos os threads que o GDB descobriu durante a análise do núcleo. Observe que o GDB deve fazer isso antecipadamente para que possa encontrar o thread de travamento conforme descrito anteriormente.

Além disso, como podemos ler na última linha da primeira imagem de inicialização do GDB acima, o GDB iniciou uma leitura de símbolos a partir da ação bin / mysqld. Sem os símbolos de depuração construídos / compilados no binário, teríamos visto alguns ou a maioria dos quadros marcados com um nome de função ??. Além disso, nenhuma leitura de variável seria apresentada para esses nomes de função.

Este problema (frames não resolvidos vistos durante a depuração de binários otimizados / despojados específicos que tiveram seu símbolo de depuração despojado / removido) não é facilmente resolvido. Por exemplo, se você ver isso em um binário de servidor de banco de dados de nível de produção (que tem os símbolos de depuração retirados / removidos para otimizar o tempo de execução, etc.), você terá que seguir procedimentos geralmente mais complexos, como, por exemplo, How to Produza um rastreamento de pilha completo para o mysqld.

Backtraces!

Como compilamos o servidor MySQL com os símbolos de depuração incluídos, um backtrace exibirá corretamente todos os nomes de função em nosso caso. Emitimos um comando bt no prompt (gdb) e nossa saída de backtrace é a seguinte:

Então, como vemos um backtrace para todos os tópicos ou um tópico diferente? Isso pode ser obtido usando os comandos thread apply all bt ou thread 2; bt, respectivamente. Podemos trocar o 2 no último comando para acessar outro thread, etc. Embora o thread apply all bt output seja um pouco prolixo para inserir aqui, aqui está a saída ao trocar para outro thread e obter um backtrace para esse thread:

A leitura cuidadosa de qualquer log ou rastreamento de erro do computador irá, como sempre, revelar mais detalhes que podem ser facilmente perdidos quando apenas olhamos as informações. Esta é uma habilidade real. Um de meus gerentes de TI anteriores me alertou sobre a grande necessidade de fazê-lo e, por meio deste, passo a mesma informação a todos os leitores ávidos deste artigo. Para apoiar essa afirmação com alguma prova, dê uma olhada de perto no backtrace produzido, e você notará os termos listen_for_connection_event, poll, Mysqld_socket_listener e connection_event_loop para Mysqld_socket_listener. É bastante claro: este tópico está aguardando entrada.

Este é apenas um thread inativo que provavelmente estava esperando um cliente MySQL se conectar ou inserir um novo comando ou algo semelhante. Em outras palavras, haveria um valor igual a zero em continuar a depurar este tópico.

Isso também nos traz de volta a como é útil ter o GDB automaticamente apresentando o thread de travamento para nós na inicialização. Tudo o que precisamos fazer para iniciar nossa aventura de depuração é obter um backtrace. Então, ao analisar vários threads e sua interação, faz sentido pular entre os threads com o comando thread. Observe que isso pode ser abreviado para t:

Curiosamente, aqui, temos o thread 3, que também está em algum loop de votação e se parece com (LinuxAIOHandler :: poll), embora, neste caso, esteja no nível do sistema operacional / disco (conforme indicado por os termos Linux, AIO e Handler) e uma análise mais detalhada revela que ele está aguardando, ao que parece, o AIO ser concluído: fil_aio_wait.

Como você pode ver, há muitas informações sobre o estado de um programa no momento em que ele trava, o que pode ser visto nos registros se olharmos de perto o suficiente .

Aqui está uma dica: você pode usar o comando set log on no GDB se quiser salvar todas as informações no disco para que possa pesquisar facilmente a saída mais tarde, e você pode usar set log off para encerrar o rastreamento de saída. As informações são armazenadas em gdb. txt por padrão.

Saltando nos quadros

Assim como vimos, é possível alternar entre as conversas e até mesmo obter um backtrace para todos os logs de uma vez, e é possível pular em quadros individuais! Podemos até — desde que o código-fonte esteja disponível no disco e armazenado no local original do disco (ou seja, o mesmo diretório do código-fonte que foi usado ao construir o produto) — ver o código-fonte para um quadro específico em que estamos.

Algum cuidado deve ser tomado aqui. É muito fácil não combinar binários, código e core dumps. Por exemplo, tentar analisar um core dump criado com a versão v1.0 de um determinado programa provavelmente não será compatível com o binário da versão v1.01 compilado um pouco mais tarde com o código v1.01. Além disso, não se poderia &’ t [sempre] usar o código-fonte v1.01 para depurar um dump de memória escrito com a versão v1.0 de um programa, mesmo se o binário v1.0 também estiver disponível.

A palavra sempre foi colocada como opcional, pois às vezes — se o código dessa seção do código e do programa que está sendo depurado não mudou desde a última versão — pode ser possível usar mais antigos código-fonte.

Esta prática talvez seja desaprovada, pois algumas mudanças simples no código podem fazer com que as linhas de código não correspondam mais ao binário e / ou dump principal. É melhor nunca misturar versões diferentes do código-fonte, os binários e os dumps principais, ou contar apenas com o dump principal e o binário, ambos da mesma versão, sem o código-fonte ou com o código-fonte referido apenas manualmente.

Ainda assim, se você estiver analisando muitos núcleos que os clientes enviaram, geralmente com informações limitadas, às vezes, pode-se usar uma versão ligeiramente diferente do código-fonte e, talvez, até mesmo um binário ligeiramente diferente ( menos provável), embora, sempre perceba que a informação apresentada pelo GDB muito provavelmente será inválida em parte, ou mais provavelmente, na íntegra. O GDB também irá avisá-lo na inicialização se for capaz de detectar uma incompatibilidade entre o núcleo e o binário.

Para o nosso exemplo, o código-fonte, o binário e o dump principal são todos feitos com a mesma versão do código-fonte e uns com os outros, e podemos, portanto, confiar no GDB quando ele produz uma saída como backtraces.

Há outra pequena exceção aqui, que é o esmagamento de pilha. Nesse caso, você observará mensagens de erro no GDB, consulte ?? nomes de quadros — semelhantes à situação descrita acima (mas desta vez, como resultado da ilegibilidade de um dump principal em combinação com o binário) — ou a pilha parecerá realmente estranha e incorreta. Na maioria das vezes, será bastante claro. Às vezes, um bug muito ruim pode causar o esmagamento da pilha.

Vamos agora pular para um quadro e ver como são algumas de nossas variáveis ​​e código:

Variáveis ​​

 t 1 bt f 7 quadro 8 p * thd p thd 

Aqui, inserimos vários comandos para navegar até o thread correto e executar um backtrace (t 1 nos levou ao primeiro thread, o thread com falha em nosso exemplo, seguido pelo comando backtrace bt) e, subsequentemente, saltou para o quadro 7 e, em seguida, o quadro 8 usando os comandos f7 e o quadro 8, respectivamente. Você pode ver como, semelhante ao comando thread, pode-se abreviar o comando frame para sua primeira letra, f.

Finalmente, tentamos acessar a variável thd, embora ela tenha sido otimizada do trace / core dump para este quadro em particular. A informação está disponível, no entanto, se simplesmente pularmos para o quadro certo, que tem a variável disponível e não foi otimizado (um pouco de tentativa e erro pode ser necessário):

Nas duas últimas capturas de tela acima, mostrei duas maneiras diferentes de digitar o comando print (novamente, abreviado de forma semelhante a apenas p), a primeira com um * à esquerda para o nome da variável, a segunda sem).

O interessante aqui é que o segundo é usado com mais frequência, mas geralmente fornecerá apenas um endereço de memória para a variável em questão, o que não é muito útil. A * versão do comando (p * thd) resolverá a variável em seu conteúdo completo. Além disso, o GDB conhece o tipo de variável, portanto, não há necessidade de fazer o typecast (converter o valor para um tipo de variável diferente).

Concluindo

Neste guia GDB mais aprofundado, examinamos pilhas, backtraces, variáveis, core dumps, frames e depuração. Estudamos alguns exemplos de GBD e demos algumas dicas importantes para o leitor ávido sobre como depurar bem e com sucesso. Se você gostou de ler este artigo, dê uma olhada em nosso artigo Como funcionam os sinais do Linux: SIGINT, SIGTERM e SIGKILL.

Nenhum comentário