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. ver e enxergar 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: pato detetive Tarefa.
Resumo
  1. Compilador na depuração
  2. lint e splint
  3. gdb
  4. ddd

--fim da aula--