O que é um depurador e como isso pode me ajudar a diagnosticar problemas?

Esta é uma questão de propósito geral para ajudar novos programadores que têm problemas com um programa, mas não sabem como usar um depurador para diagnosticar a causa do problema.

Esta questão abrange duas classs de questões mais específicas:

  • Quando eu executo meu programa, ele não produz a saída que eu esperava para a input que eu forneci
  • Quando eu executo meu programa, ele trava e me dá um rastreamento de pilha. Eu examinei o rastreamento de pilha , mas ainda não sei a causa do problema porque o rastreamento de pilha não me fornece informações suficientes.

Um depurador é um programa que pode examinar o estado do seu programa enquanto o programa está sendo executado. Os meios técnicos usados ​​para fazer isso não são importantes para entender os fundamentos de como usar um depurador. Você pode usar um depurador para interromper a execução de seu programa quando ele atinge um determinado local em seu código e, em seguida, examine os valores das variables ​​no programa. Você pode usar um depurador para executar seu programa muito lentamente, uma linha de código de cada vez (chamada de stepping único ), enquanto examina os valores de suas variables.

Ao fazer isso, você pode descobrir se uma variável tem o valor errado e onde, no seu programa, seu valor foi alterado para o valor errado.

Usando uma única etapa, você também pode descobrir se o stream de controle é o esperado. Por exemplo, se um ramo if executado quando você espera que deveria ser.

As especificidades de usar um depurador dependem do depurador e, em menor grau, da linguagem de programação que você está usando.

  • Você pode append um depurador a um processo que já está executando seu programa. Você pode fazer se o seu programa estiver emperrado.

  • Na prática, é mais fácil executar seu programa sob o controle de um depurador desde o início.

  • Você indica onde seu programa deve parar de executar indicando o arquivo de código-fonte eo número da linha na qual a execução deve parar, ou indicando o nome do método / function na qual o programa deve parar (se você quiser parar como assim que a execução entrar no método). Os meios técnicos que o depurador usa para fazer com que seu programa pare é chamado de ponto de interrupção e esse processo é chamado de configuração de um ponto de interrupção .

  • A maioria dos depuradores modernos faz parte de um IDE e fornece uma GUI conveniente para examinar o código-fonte e as variables ​​do seu programa, com uma interface de apontar e clicar para definir pontos de interrupção, executar seu programa e fazer um único passo.

  • Usar um depurador pode ser muito difícil, a menos que seus arquivos executáveis ​​ou bytecode do programa incluam informações de símbolos de debugging. Você pode ter que compilar (ou recompilar) seu programa de forma ligeiramente diferente para garantir que as informações estejam presentes.

Eu quero acrescentar que um depurador nem sempre é a solução perfeita, e nem sempre deve ser a solução para a debugging. Aqui estão alguns casos em que um depurador pode não funcionar para você:

  • A parte do seu programa que falha é realmente grande (pouca modularização, talvez?) E você não sabe exatamente onde começar a percorrer o código. Percorrendo tudo isso pode ser muito demorado.
  • Seu programa usa muitos callbacks e outros methods de controle de stream não lineares, o que torna o depurador confuso quando você o percorre.
  • Seu programa é multi-threaded. Ou pior ainda, seu problema é causado por uma condição de corrida.
  • O código que contém o bug é executado muitas vezes antes de ser corrigido. Isso pode ser particularmente problemático em loops principais, ou pior ainda, em mecanismos de física, onde o problema pode ser numérico. Mesmo definindo um ponto de interrupção, neste caso, você simplesmente teria que bater nele muitas vezes, com o bug não aparecendo.
  • Seu programa deve ser executado em tempo real. Esse é um grande problema para programas que se conectam à rede. Se você configurar um ponto de interrupção em seu código de rede, a outra extremidade não esperará por você, simplesmente vai expirar. Programas que dependem do relógio do sistema, por exemplo, jogos com salto de quadro, também não são muito melhores.
  • Seu programa realiza alguma forma de ações destrutivas, como escrever em arquivos ou enviar e-mails, e você gostaria de limitar o número de vezes que você precisa executá-lo.
  • Você pode dizer que seu bug é causado por valores incorretos que chegam na function X, mas você não sabe de onde esses valores vêm. Ter que percorrer o programa repetidas vezes, definindo pontos de interrupção cada vez mais distantes, pode ser um grande aborrecimento. Especialmente se a function X é chamada de muitos lugares ao longo do programa.

Em todos esses casos, fazer com que seu programa pare abruptamente pode fazer com que os resultados finais sejam diferentes ou percorrer manualmente em busca da única linha em que o erro é causado é muito trabalhoso. Isso pode acontecer igualmente se o seu bug é um comportamento incorreto ou uma falha. Por exemplo, se a corrupção de memory causar uma falha, no momento em que a falha ocorrer, ela estará muito longe de onde ocorreu a corrupção da memory e nenhuma informação útil será deixada.

Então quais são as alternativas?

O mais simples é simplesmente registrar e asserções. Adicione logs ao seu programa em vários pontos e compare o que você obtém com o que você está esperando. Por exemplo, veja se a function em que você acha que há um bug é chamada em primeiro lugar. Veja se as variables ​​em um começo de um método são o que você pensa que são. Ao contrário dos pontos de interrupção, não há problema em existirem muitas linhas de registro em que nada de especial acontece. Você pode simplesmente pesquisar o log depois. Uma vez que você atingiu uma linha de log diferente do que você está esperando, adicione mais na mesma área. Abaixe cada vez mais, até que seja pequeno o suficiente para ser capaz de registrar todas as linhas na área de escuta.

As asserções podem ser usadas para interceptar valores incorretos à medida que ocorrem, em vez de terem um efeito visível para o usuário final. Quanto mais rápido você pegar um valor incorreto, mais próximo você está da linha que o produziu.

Refator e teste unitário. Se o seu programa for muito grande, pode valer a pena testá-lo uma class ou uma function de cada vez. Dê-lhe inputs, analise as saídas e veja quais não são as esperadas. Ser capaz de restringir um bug de um programa inteiro a uma única function pode fazer uma enorme diferença no tempo de debugging.

Em caso de vazamentos de memory ou de memory, use ferramentas apropriadas que possam analisá-las e detectá-las em tempo de execução. Ser capaz de detectar onde ocorre a corrupção real é o primeiro passo. Depois disso, você pode usar os logs para trabalhar de volta para onde os valores incorretos foram introduzidos.

Lembre-se de que a debugging é um processo que vai para trás. Você tem o resultado final – um bug – e encontra a causa, que o precedeu. É sobre trabalhar o seu caminho de volta e, infelizmente, os depuradores apenas avançam. É aí que uma boa análise de log e postmortem pode dar a você resultados muito melhores.