CI064, 2019-1 © Roberto André Hexsel, 2014-2019
Depuração para Computeiros
Em La Biblioteca de Babel, de 1941, Jorge Luis Borges profetiza que
media docena de monos, provistos de máquinas de escribir, producirán
en unas cuantas eternidades todos los libros que contiene el British
Museum. Bastaría, en rigor, con un solo mono inmortal.
Outra versão popular desta hipótese seria algo como
centenas de chipanzés digitando durante centenas de anos poderiam
quase certamente produzir um soneto (ou a obra completa) de Shakespeare.
Esta segunda versão da hipótese foi enunciada em 1912, um século antes do
advento das redes sociais, que são demasiado recentes para comprová-la.
Tenho observado frequentemente, ao acompanhar alunos a fazer seus
trabalhos, que tal qual os chipanzés de Borges (i) escrevem
rapidamente 100 linhas de código, as compilam e descobrem que o código não
atende ao especificado; (ii) colocam as 100 linhas de código num
balaio, que sacodem vigorosamente, então re-compilam e descobrem que o
código ainda não atende à especificação; (iii) repetem (ii), e a cada
iteração o número de linhas de código no balaio diminui, ou aumenta; e
(iv) 7 minutos antes da hora fatal, alteram mais 3 linhas de código
escolhidas a esmo e então submetem o código à avaliação pelo professor.
Meu colega Renato Carmo, leitor atento de Borges, muito apropriadamente
denominou esta atividade de programação por tentativa e erro.
Uma breve digressão. Talvez esta seja a origem do que é chamado de
"programação evolutiva", ideia que deve fazer o pobre Dijkstra revolver
feito um pião em sua sepultura. Acredito que Darvin sofra também.
Isto é sarcasmo. Melhor explicitar. Fim da digressão.
Alunos mais avançados no curso logo descobrem que inserir montes de
printf's no código ajuda a reduzir o número de iterações com o balaio. Bom
seria. Com alguma frequência observo que os printf's atrapalham mais do
que ajudam. Um caso verídico e dos mais interessantes é o do aluno que
testava o código do trabalho de Redes I, viz. dois processos
devem se comunicar através de raw sockets segundo um protocolo
similar ao Kermit. Em desespero, após sacudir o balaio muitas vezes, o
aluno inseriu no código algo como
printf("c1: %d\n",c1);
para cada um dos 20-30 caracteres que compunham uma mensagem. Para toda
mensagem recebida, os 20 a 30 '\n', emitidos um por caractere, empurravam
para fora da tela a mensagem anterior, sendo portanto extremamente
laborioso seguir uma sequência de mensagens, que era o mínimo necessário
para verificar o andamento da comunicação entre os processos.
O aluno ficou extremamente surpreso, e possivelmente grato, quando sugeri
que alterasse seus 20 printf's para
printf("%d %d %d %d %d %d ...\n",c1, c2, c3, c4, c5, c6, ...);
quando, subitamente ficou possível acompanhar a sequência de mensagens
trocadas pelos dois processos.
Mais surpreso ainda ele ficou com minha sugestão para que imprimisse
também o estado das máquinas de estado dos protocolos nas duas pontas do
enlace. "Como assim, imprimir o estado?" dizia seu rosto surpreso, embora
não sua boca semi escancarada...
Outra breve digressão. Estudantes de eletrônica descobrem cedo que o
osciloscópio (similar ao monitor de um eletrocardiógrafo) só ajuda na
depuração de circuitos se as pontas de prova são aplicadas aos
"sinais corretos". Parafraseando o Gato de Cheshire:
não adianta olhar se você não sabe onde procurar,
ou
não adianta olhar se não sabe o quê procurar.
Fim da digressão.
As três morais destas estórias, numa ordem aleatória, são:
1) não adianta depurar um programa que você não entende;
2) não adianta tentar resolver um problema que você não entendeu; e
3) não adianta tentar resolver um problema se você não detém os conceitos
básicos necessários para sua compreensão e resolução.
Embora estas três 'morais' sejam platitudes de ofuscante obviedade,
programadores iniciantes as ignoram com dolorosa frequência. Essa é minha
experiência como professor, mas não só ela. Quando era aprendiz de
engenheiro na UFRGS, eu próprio fui salvado da terceira pelas mãos amigas
de Renato Brito e Fernando Barbosa.
Talvez a palavra mais importante no texto acima seja especificação.
Se você não entende ou não comprende a especificação, o universo não
comporta chipanzés imortais em quantidade suficiente que possam ajudá-lo na
depuração do seu programa.
Vamos então à depuração para computeiros.
Copie para sua área de trabalho o arquivo com os programas fonte que serão
usados neste laboratório com
wget www.inf.ufpr.br/roberto/ci064/labDebug.tgz
abra-o com tar xzf labDebug.tgz e mude-se para o diretório labDebug.
Os programas exemplo para esta aula são:
rand.c verifica a distribuição, ao redor da média, de um conjunto
de números gerados aleatoriamente;
sieve.c gerador de números primos com o Crivo de Eratóstenes;
strings.c conjunto de programas simples para manipular cadeias de
caracteres; e
frag.c programa que fragmenta pacotes do protocolo IP, caso
contenham mais do que 512 octetos. A versão original deste
programa é parte da suíte Commbench.
Usaremos três ferramentas com grau crescente de "observabilidade", quer
dizer, que nos permitem olhar com um microscópio com maior poder de
ampliação. Como ocorre com microscópios, quanto maior a ampliação, menor
o diâmetro do campo observado.
O compilador pode nos avisar sobre uma série de construções
duvidosas, que se não são propriamente erros de sintaxe, podem levar a
erros de semântica. O lint é a "escova de código" que nos ajuda a
remover aqueles fiapinhos que só aparecem em roupa escura, mas que podem
'sujar' os resultados produzidos pelo código. Finalmente, o depurador
de código fonte nos permite acompanhar a execução do programa, linha a
linha do código fonte, observar as alterações nas variáveis, etc.
Maior observabilidade do que com um depurador só se obtém com a simulação
detalhada do processador, que usaremos mais adiante, para depurar código
para o tratamento de interrupções. Geralmente, não se consegue depurar
tratadores de interrupção com um "depurador de código fonte".
1- Compilador
O GCC possui várias opções que auxiliam com a depuração de programas, no
nosso caso, escritos em C. A opção mais simples, e portanto útil,
é a opção -Wall
gcc -Wall programa.c
Dependendo dos erros encontrados em programa.c, GCC emite mais ou menos
mensagens de aviso (warnings) sobre as construções suspeitas que
detecta no código. Note que programas com erros de linguagem C não
resultam num executável a.out, enquanto que "warnings" resultam em
código executável, mas que pode conter erros de lógica por conta de uso
inadequado da linguagem, ou de código que não é portável entre compiladores
distintos, ou nem mesmo portável entre versões distintas do mesmo compilador.
Compile os programas fonte com -Wall e conserte eventuais problemas,
até eliminar todos os avisos.
Diga gcc --help=warnings para todas as opções de avisos.
-Wall é uma abreviatura para "reclame de quase tudo" ou warn of all.
Vejamos algo mais exigente do que o sistema de avisos do compilador.
2- LINT, ou SPLINT
O programa lint faz(ia) verificação estática do código fonte,
verificando sintaxe e uns poucos elementos de semântica. Este programa foi
reescrito algumas vezes e a instância "moderna" instalada no DInf
chama-se splint. Splint é bastante mais exigente do que o
compilador com relação ao que é ou não permitido num programa C, e foi
escrito para detectar violações de segurança acidentalmente incluídas nos
programas.
O manual do splint encontra-se
aqui
(costumava ser aqui) e a melhor
forma de aprender a usá-lo é usando-o.
Splint é verboso, muito verboso. Alguns dos avisos são pura chateação,
embora a maioria deles mereça nossa atenção e cuidado.
Note que, ao contrário de outros programas já vistos aqui, as opções do
splint são ligadas com +opção e desligadas com -opção.
Inicie pelo programa mais simples:
splint rand.c
resolva os problemas apontados, até que todos os avisos sejam eliminados.
Leia a primeira página do manual e conserte os programas sieve.c e strings.c.
Se é necessário apontar um caminho para arquivos .h emprega-se a
opção +S. Se é necessário definir um símbolo usado em compilação
condicional emprega-se a opção +D. O exemplo abaixo mostra os dois
casos, com a indicação do caminho para um arquivo com cabeçalho e a
definição do símbolo cMIPS, para verificar o fonte dijkstra_small.c.
splint +S./include/cMIPS.h +DcMIPS dijkstra_small.c
Se isso tudo ainda não foi o suficiente, é hora de partir para o que
realmente ajuda, embora a solução não seja tão aparentemente fácil,
nem tão imediatista, quanto encher o código de printf's.
3- GDB
O GNU debugger é o depurador projetado para funcionar integrado ao
editor Emacs, mas pode ser usado separadamente daquele.
Veja a página do Prof. Maziero sobre o GDB.
Importante: o código que será depurado deve ser compilado
com as opções -g -O0:
gcc -Wall -g -O0 programa.c
A opção -g faz o compilador gerar as tabelas que relacionam código
executável com as linhas do código fonte; a opção -O0 (letra O
seguida de zero) impede o compilador de otimizar o executável, que será
portanto uma tradução simples e direta do código fonte.
Depure as cinco funções em strings.c. As funções (exceto main())
contém ao menos um erro. Encontre os erros e conserte-os. Na medida do
possível, tente não procurar os erros lendo o código fonte, e
evidentemente, não acrescente nenhum printf.
4- DDD ou Data Display Debugger
DDD é um depurador de código fonte que permite observar o estado das
variáveis do programa enquanto este é executado. DDD é uma interface de
janelas para o GDB.
A página de manual do DDD está aqui.
Importante: o código que será depurado deve ser compilado
com as opções -g -O0:
gcc -Wall -g -O0 programa.c
A opção -g faz o compilador gerar as tabelas que relacionam código
executável com as linhas do código fonte; a opção -O0 (letra O
seguida de zero) impede o compilador de otimizar o executável, que será
portanto uma tradução simples e direta do código fonte.
O manual contém uma ótima sessão exemplo.
DDD usa uma interface de janela antiquada e pode não
funcionar a contento nos sistemas do Departamento. Neste caso, GDB é a
melhor ferramenta.
Execute a sessão exemplo e então depure as cinco funções em strings.c. As
funções (exceto main()) contém ao menos um erro. Encontre os
erros e conserte-os. Na medida do possível, tente não procurar os
erros lendo o código fonte, e evidentemente, não acrescente
nenhum printf.
5- Se nada disso adiantar
Se nada disso ajudou, explain your code to someone else
Excerpted from Chapter 5 of The Practice of Programming,
by Brian W. Kernighan and Rob Pike, Addison-Wesley, 1999; ISBN
0-201-61586-X.
...
Another effective technique is to explain your code to someone
else. This will often cause you to explain the bug to yourself. Sometimes
it takes no more than a few sentences, followed by an embarrassed "Never
mind, I see what's wrong. Sorry to bother you". This works remarkably
well; you can even use non-programmers as listeners. One university
computer center kept a teddy bear near the help desk. Students with
mysterious bugs were required to explain them to the bear before they could
speak to a human counselor.
ou, desenhando:
Tarefa.
Resumo
- Compilador na depuração
- lint e splint
- gdb
- ddd
--fim da aula--