Pplware

Introdução ao debugging de software

Por Luís Soares para o PPLWARE.COM

O debugging de um programa baseia-se em alguns princípios e técnicas transversais à maioria das linguagens e ambientes de programação. Tentarei, neste artigo, sintetizar o que entendo por debugging, introduzindo o tema, colocando algumas luzes nos conceitos fundamentais e mostrando que há um mundo para além de alerts e prints.

Debugging (ou depuração) é o processo pelo qual se identificam e corrigem bugs de software (ou hardware). Dominar o debugging é vital para um programador: os bugs vão inevitavelmente aparecer quando a complexidade (número de programadores, de requisitos, de linhas de código, de dependências, etc.) aumentar. No início, fazer debug costuma ser algo chato e/ou custoso (pelo menos para mim, era); mas, com a prática, torna-se mais interessante, pois:

Evitando o debug

A melhor forma de fazer debug é evitar ter de o fazer. Devemos adotar técnicas e boas práticas que reduzam a probabilidade de ter bugs e outras que facilitem a sua correção caso ocorram. Eis algumas (cuja explicação sai fora do âmbito atual):

O processo de debug

O debugging, apesar de muito ligado à programação, é uma disciplina com o seu próprio processo. Mesmo não se pensando nisso, está-se implicitamente a segui-lo:

  1. Reprodução: saber os passos a seguir, as condições iniciais, assunções, etc;
  2. Diagnóstico: gravidade, prioridade, impactos, riscos, a zona em causa. Para este último:
  3. Correção;
  4. Reflexão: aplicação de medidas que garantam que o problema não se repete noutro formato: testes, documentação, validações de input e corner cases, criação de código mais resiliente, refactoring, etc.

Logo na reprodução do problema é frequente que o programador perceba o que está mal, passando logo para a correção. Se isso não suceder, passa-se então ao diagnóstico (o debugging propriamente dito), altura em que são aplicadas as seguintes técnicas (entre muitas outras):

Vejamos o tracing e o debugging com ferramenta em mais detalhe.

Imprimindo (tracing)

Quem nunca fez debug com prints? Tracing é o método mais usado para debug. Fazer tracing é usar a consola (há quem use a própria GUI, embora isso não seja muito elegante…) para imprimir (i.e. exibir) o estado do programa sem bloquear o seu fluxo. Por outras palavras, permite saber o valor de variáveis (ou simplesmente dizer que se passou lá) quando se passa nos sítios com prints. Pode ser usado para debugging ou apenas registo (logging). Eis alguns exemplos de prints em diferentes linguagens:

C

printf(“Olá\n”)

C++

cout << “Olá\n”

Java

System.out.println(“Olá”)   System.err.println(“Olá”)

C# / Visual Basic

Console.WriteLine(“Olá”)

Python

print ‘Olá’

Objective C

NSLog(@”Olá”)

JavaScript*

console.info(Olá)  console.log(‘Olá’)  console.warn(‘Olá’)  console.error(‘Olá’)

* Em JavaScript, não use o alert(…), pois:

Consoante o ambiente (linguagem, plataforma, IDE e bibliotecas) em uso, cada linha de tracing pode conter várias informações, entre as quais:

Características

Deverá analisar as características da situação e perceber quando aplicar esta técnica. Muitas vezes, tem mais a ganhar recorrendo a debuggers

Recorrendo a debuggers

Imagine que houve um acidente em cadeia na autoestrada… Agora, tinha de identificar a causa do acidente. Poderia repetir tudo, tinha o poder de parar todos os veículos em simultâneo, podia tirar fotografias, analisar cada veículo, dar ordem para se prosseguir, etc. Esta metáfora ilustra o papel de um debugger de software: uma ferramenta de apoio ao debug sistematizado. Pode ser oferecido:

Falar em detalhe num debugger exigiria um conjunto de artigos e tutoriais. Tentarei apenas resumir os conceitos principais e transversais a qualquer debugger e linguagem, os quais deve tentar dominar.

É importante que perceba que um programa a correr representa (pelo menos) um fluxo de execução (na prática, uma thread). O estado de um programa é o conjunto de elementos que o descrevem num determinado momento (o conjunto de todas as variáveis).

Controlo de fluxo

Debug

   

A ordem que se dá para se iniciar em modo de debug. Um programa, quando lançado neste modo, abre uma sessão onde se consideram os restantes conceitos.

Breakpoint

O conceito central do debugger. Breakpoints são marcadores colocados nas linhas do código-fonte, correspondendo a pedidos de bloqueio do fluxo. Por isso, sempre que o fluxo do programa pausa num breakpoint, foca o programador na linha respectiva ficando suspenso à espera de novas ordens. O programador pode então inspeccionar o estado da aplicação (com as ferramentas abaixo descritas).

Step

ou

Step over

   

É a ordem do programador para seguir em frente um passo, ou seja, para a próxima instrução (geralmente a próxima linha) e voltar a pausar.

Step into

   

É a ordem de avançar um passo “entrando” na próxima invocação de função/método, voltando aí a pausar.

Continue

ou

Resume

   

A ordem para prosseguir, ou seja, de deixar o programa seguir o seu fluxo normal. Só se pausará no próximo breakpoint, caso exista e lá se passe.

Stop

   

A ordem para parar a sessão de debug.

Estado

Variables

ou

Scope Variables

ou

Locals

O estado corrente, ou seja, o conjunto de variáveis do scope corrente (ex. locais, globais) e os seus valores do momento. Quando o programa pausa num breakpoint, podemos analisar este painel. Podemos até navegar nos objetos (drill down), no caso de programação O.O.

De notar que a maioria dos debuggers permitem, ao passar o rato sobre uma expressão, saber o seu valor em real time (inspeção).

Watches

ou

Expressions

ou

Watch Expressions

Quase o mesmo que Variables, mas estes são expressões “manualmente” definidas pelo programador. São como lupas apontadas continuamente a variáveis ou expressões exibindo o seu valor corrente. Sempre que se pausa num breakpoint, temos esta lista arbitrária de expressões que podemos analisar.

Call stack

É a pilha de chamadas pendentes, aquando de uma pausa num breakpoint. Neste momento, podemos verificar o rol de chamadas que deu origem ao momento atual e estão à espera de voltar a ter o controlo.

Quando há uma exceção, o call stack é imprimido na Consola, dando origem ao chamado stack trace.

Console

ou

Output

Onde é exibido o output, ou seja, o tracing do programador, do programa, do compilador, do servidor web, da BD, … Por vezes, também é consola de input (ex. em JavaScript, no debugger do browser) (consola interativa).

 

 

 

 

Características
Tutoriais de debuggers
Em conclusão

Embora intimamente ligado à programação, o debugging tem o seu próprio processo: reprodução, diagnóstico, correção e reflexão. O diagnóstico é geralmente efetuado recorrendo a tracing – imprimindo – ou utilizando uma ferramenta: o debugger. Estas não são técnicas concorrentes. De facto, são muitas vezes usadas de forma complementar. Um debugger permite analisar à lupa um problema; o tracing permite análises gerais. Ambas passam por:

  1. Colocar de breakpoints e/ou prints nas zonas em causa;
  2. Lançar do programa e reproduzir o problema;
  3. Analisar do estado do programa quando ele passa pelas zonas em causa.

Os conceitos a reter e dominar num debugger são: breakpoints, steps, inspeção de objetos (variables e watches) e console

Quando o debugger é baseado na consola (ex. no GDB um step é um comando escrito), há desculpa para se recorrer ao tracing. Com as ferramentas gráficas disponíveis actualmente (varia com a linguagem), o tracing deve ficar relegado às suas funções. Este tem as suas limitações pelo que é fundamental que o programador domine o debugger. O problema é que se vêem programadores experientes a ignorar o seu poder, algo difícil de explicar (após o conhecerem, a sua produtividade aumenta e já não conseguem viver sem ele).

Possíveis tópicos a desenvolver em futuros artigos são os breakpoints condicionais e com hit count, o debugging remoto (ex. de um browser num telemóvel ou de uma aplicação web num servidor) e o uso de profiling.

Exit mobile version