Ferramentas do usuário

Ferramentas do site


prog2:depuracao

Diferenças

Aqui você vê as diferenças entre duas revisões dessa página.

Link para esta página de comparações

Ambos lados da revisão anterior Revisão anterior
prog2:depuracao [2019/04/30 12:22]
maziero
prog2:depuracao [2019/04/30 12:23] (atual)
maziero
Linha 1: Linha 1:
 +====== Depuração ======
 +
 +Depurar significa "​purificar",​ "​limpar"​. Ao programar em C, frequentemente temos necessidade de depurar a lógica de um programa, para resolver erros de execução, de alocação/​liberação de memória ou de desempenho. Esta página descreve algumas ferramentas disponíveis para tal propósito.
 +
 +O primeiro passo para a depuração de um programa executável é compilá-lo de forma a **incluir no executável os símbolos** necessários ao processo de depuração (como os nomes das variáveis e funções, referências às linhas do código fonte, etc). Isso é feito adicionando a opção ''​-g''​ ao comando de compilação.
 +
 +O comando de compilação do arquivo {{pi.c|pi.c}},​ por exemplo, seria:
 +
 +<​code>​
 +$ gcc -g -o pi pi.c -lm
 +</​code>​
 +
 +===== Depuração de execução =====
 +
 +O depurador padrão para a linguagem C no Linux é o GDB ([[https://​en.wikipedia.org/​wiki/​GNU_Debugger|GNU Debugger]]). O GDB é um depurador em modo texto com muitas funcionalidades,​ mas relativamente complexo de usar para os iniciantes.
 +
 +Para iniciar uma depuração,​ basta invocar o GDB com o executável (compilado com a opção ''​-g''​):​
 +
 +<​code>​
 +$ gdb pi
 +GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
 +... (msgs diversas)
 +Lendo símbolos de pi...concluído.
 +(gdb) run
 +Starting program: /​home/​prof/​maziero/​organizar/​pi/​pi ​
 +O valor aproximado de Pi é: 3.141592
 +[Inferior 1 (process 24652) exited normally]
 +(gdb) quit
 +</​code>​
 +
 +O //prompt// do GDB aceita diversos comandos, entre eles o //run// e o //quit//, ilustrados acima. Os comandos mais básicos disponíveis são:
 +
 +^ comando ^ ação ^ exemplos ^
 +| ''​r'' ​ | inicia a execução (//run//) | ''​run''​\\ ''​r < dados.dat''​\\ ''​r > saida.txt''​ |
 +| ''​l''​ | lista linhas do código-fonte | ''​l 23''​ |
 +| ''​b''​ | cria um ponto de parada (//​breakpoint//​) | ''​b 17''​\\ ''​b main''​ |
 +| ''​c''​ | continua após o ponto de parada | ''​c''​ |
 +| ''​s''​ | avança para a próxima linha de código (//step//) | ''​s''​ |
 +| ''​n''​ | avança para a próxima linha de código (//next// - não entra em funções) | ''​n''​ |
 +| ''​p''​ | imprime o valor de uma variável ou expressão | ''​p soma''​\\ ''​p /x soma''​ |
 +| ''​watch''​ | avisa quando uma variável muda de valor | ''​ watch i''​ |
 +| ''​disp''​ | mostra o valor de uma variável ou expressão a cada pausa (//​display//​) | ''​disp soma''​ |
 +| ''​set variable''​ | Ajusta o valor de uma variável do programa em análise | ''​set variable soma = 100''​ |
 +| ''​bt''​ | Mostra a posição atual do programa (//​backtrace//​),​ incluindo as funções ativas no momento | ''​bt''​ |
 +| ''​frame''​ | Seleciona o frame de execução a analisar (o nível de chamada de função) | ''​frame 3''​ |
 +| ''​^x^a''​ | alterna entre as interfaces padrão e NCurses ||
 +
 +Uma relação mais extensa de comandos pode ser encontrada neste {{:​unix:​gdb-refcard.pdf|GDB Reference Card}}.
 +
 +<note tip>
 +O GDB proporciona uma forma fácil de localizar erros fatais de memória, como //​Segmentation Fault// e outros. Basta executar o programa no GDB (usando //run//) até ocorrer o erro. Quando este ocorrer, o número da linha será informado e os valores das variáveis envolvidas podem ser inspecionados para encontrar o erro. Experimente usar o GDB para encontrar os erros de acesso à memória neste programa: {{memerror.c|}}.
 +</​note>​
 +
 +Vários guias de uso do GDB podem ser encontrados nos links abaixo: ​
 +
 +  * [[http://​beej.us/​guide/​bggdb|Beej'​s Quick Guide to GDB]]
 +  * [[http://​www-2.cs.cmu.edu/​%7Egilpin/​tutorial/​|A GDB tutorial]]
 +  * [[http://​www.delorie.com/​gnu/​docs/​gdb/​gdb_toc.html|Debugging with GDB]]
 +  * [[http://​heather.cs.ucdavis.edu/​%7Ematloff/​UnixAndC/​CLanguage/​Debug.html|Guide to faster, less frustrating debugging]]
 +  * [[http://​www.dirac.org/​linux/​gdb/​|Using GNU's GDB debugger]]
 +  * [[http://​www.lrc.ic.unicamp.br/​~luciano/​courses/​mc202-2s2009/​tutorial_gdb.txt|Tutorial de GDB]] (em português)
 +
 +Como interfaces alternativas para o GDB, existe o modo //​NCurses//,​ que pode ser ativado invocando o GDB com a opção ''​-tui''​ ou pelo comando ''​^x^a''​ (//ctrl-x ctrl-a//). Outras interfaces disponíveis em modo texto são o ''​cgdb'',​ que usa comandos similares aos do VI, e o modo GDB do [[http://​www.gnu.org/​software/​emacs/​manual/​html_node/​emacs/​GDB-Graphical-Interface.html|EMACS]].
 +
 +Também existem diversas interfaces gráficas para o GDB, como o [[http://​www.gnu.org/​software/​ddd/​|Data Display Debugger]] (ddd), [[https://​wiki.gnome.org/​Apps/​Nemiver|Nemiver]],​ [[http://​gede.acidron.com/​|Gede]] e [[http://​www.ultragdb.com|UltraGDB]]. O GDB também pode ser usado através de IDEs (//​Integrated Development Environments//​) gráficos como //​Code::​Blocks//,​ //​Codelite//,​ //​Eclipse//,​ //​KDevelop//,​ //​NetBeans//,​ etc.
 +
 +===== Despejo de memória =====
 +
 +A maioria dos sistemas UNIX permite salvar em um arquivo o conteúdo da memória de um programa em execução (processo), quando este é interrompido por um erro. Esse arquivo se chama ''​core file''​ ([[https://​en.wikipedia.org/​wiki/​Core_dump|despejo de memória]]) e pode ser aberto por depuradores,​ para auxiliar na compreensão da causa do erro.
 +
 +Para usar esse recurso deve-se seguir os seguintes passos (usando como exemplo o arquivo {{memerror.c}}):​
 +
 +  - Habilitar a geração de arquivo //core// no terminal atual: <​code>​$ ulimit -c unlimited</​code>​
 +  - Compilar o programa com o flag de depuração (''​-g''​):<​code>​$ cc -g memerror.c -o memerror</​code>​
 +  - Lançar o programa:<​code>​$ memerror</​code>​
 +  - Quando o programa abortar por erro, será gerado um arquivo //core// no diretório corrente:
 +  - <​code>​
 +$ ls -l
 +-rw------- ​ 1 maziero maziero 262144 Nov 30 14:57 core
 +-rwxrwxr-x ​ 1 maziero maziero ​ 11104 Nov 30 14:56 memerror
 +-rw-rw-r-- ​ 1 maziero maziero ​   839 Nov 30 14:56 memerror.c
 +</​code>​
 +  - Alternativamente,​ pode-se **forçar o encerramento** (e a consequente geração do arquivo //core//) através de um sinal ''​SIGQUIT''​ (sinal n° 3). Esse sinal deve ser enviado ao processo através do comando ''​kill''​ (a partir de outro terminal, se necessário):<​code>​$ kill -3 PID</​code>​ onde PID é o identificador numérico do processo a ser encerrado.
 +  - Uma vez obtido o arquivo //core//, basta abri-lo através do depurador (GDB):<​code>​$ gdb memerror core</​code>​
 +
 +===== Depuração de memória =====
 +
 +Um dos problemas mais frequentes (e de depuração mais difícil) na programação em C é o uso incorreto da memória. Situações como acesso a posições inválidas de vetores ou matrizes (// buffer overflow//​),​ uso de ponteiros não inicializados,​ não-liberação de memória dinâmica (//memory leaks//) podem gerar comportamentos erráticos difíceis de depurar.
 +
 +Há várias ferramentas para auxiliar na depuração de problemas de memória, vistas na sequência.
 +
 +==== Análise estática ====
 +
 +Ferramentas de análise estática examinam o código-fonte de uma forma mais detalhada que o compilador, permitindo encontrar diversos erros que podem passar despercebidos,​ como índices de vetores fora da faixa válida.
 +
 +Algumas ferramentas disponíveis para análise estática de código:
 +
 +  * ''​cppcheck'': ​ verificador estático de código C/C++ (recomenda-se usar  flag ''​--enable=all''​)
 +  * ''​splint'':​ idem
 +
 +==== Flags do compilador ====
 +
 +Opções do GCC para depuração de memória:
 +
 +  * ''​-fsanitize=address'':​ ativa o [[https://​en.wikipedia.org/​wiki/​AddressSanitizer|AddressSanitizer]],​ um detector de erros de memória em tempo de execução. O código do executável é instrumentado (são adicionadas instruções) para verificar erros de acesso a posições inválidas de memória.
 +  * ''​-fcheck-pointer-bounds'':​ ativa a verificação de limites de ponteiros.
 +  * ''​-fstack-protector'':​ gera código adicional para verificar a integridade da pilha (//flag// habilitado por default).
 +
 +<note important>​
 +As verificações adicionadas por esses //flags// são efetuadas a cada acesso à memória, por isso têm um forte impacto no desempenho e no uso de memória do executável. Então, só devem ser usadas durante o processo de desenvolvimento e nunca no produto final.
 +</​note>​
 +
 +==== Bibliotecas de depuração ====
 +
 +São bibliotecas que instrumentam as rotinas de alocação/​liberação de memória, permitindo depurar erros relacionados ao uso de memória dinâmica, como //memory leaks//, //double free// e //use after free//.
 +
 +  * [[http://​dmalloc.com/​|DMalloc]]
 +  * [[https://​launchpad.net/​ubuntu/​+source/​electric-fence/​2.2.3|Electric Fence]]
 +
 +==== Depuradores de memória ====
 +
 +  * [[https://​en.wikipedia.org/​wiki/​Mtrace|Mtrace]]
 +  * [[http://​valgrind.org/​|Valgrind]] (vide opções ''​--tool=memcheck''​ ou ''​--tool=exp-sgcheck''​)
 +
 +A análise dinâmica do programa {{:​prog2:​memerror.c|}} pela ferramenta Valgrind permite encontrar todos os erros de acesso à memória presentes. O Valgrind é particularmente útil para encontrar problemas de vazamento de memória (//memory leaks//).
 +
 +FIXME colocar um roteiro simples de uso do Valgrind, com exemplo.
 +
 +===== Tracing =====
 +
 +Uma ferramenta útil para auxiliar na depuração de programas é o utilitário ''​strace'',​ que permite listar as chamadas de sistema efetuadas por um executável qualquer. Por não precisar de opções especiais de compilação,​ nem do código-fonte do executável,​ é uma ferramenta útil para investigar o comportamento de executáveis de terceiros. Além disso, o ''​strace''​ pode analisar processos já em execução (através da opção ''​-p''​).
 +
 +Eis um exemplo (abreviado) da execução de ''​strace''​ sobre o programa {{pi.c}}:
 +
 +<​code>​
 +$ strace ./pi
 +execve("​./​pi",​ ["​./​pi"​],​ [/* 31 vars */]) = 0
 +brk(0) ​                                 = 0x1d27000
 +access("/​etc/​ld.so.nohwcap",​ F_OK)      = -1 ENOENT (No such file or directory)
 +mmap(NULL, 8192, PROT_READ|PROT_WRITE,​ MAP_PRIVATE|MAP_ANONYMOUS,​ -1, 0) = 0x7f24cac2f000
 +access("/​etc/​ld.so.preload",​ R_OK)      = -1 ENOENT (No such file or directory)
 +open("/​etc/​ld.so.cache",​ O_RDONLY|O_CLOEXEC) = 3
 +fstat(3, {st_mode=S_IFREG|0644,​ st_size=237552,​ ...}) = 0
 +mmap(NULL, 237552, PROT_READ, MAP_PRIVATE,​ 3, 0) = 0x7f24cabf5000
 +close(3) ​                               = 0
 +access("/​etc/​ld.so.nohwcap",​ F_OK)      = -1 ENOENT (No such file or directory)
 +open("/​lib/​x86_64-linux-gnu/​libm.so.6",​ O_RDONLY|O_CLOEXEC) = 3
 +read(3, "​\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>​\0\1\0\0\0\20V\0\0\0\0\0\0"​...,​ 832) = 832
 +...
 +mprotect(0x600000,​ 4096, PROT_READ) ​    = 0
 +mprotect(0x7f24cac31000,​ 4096, PROT_READ) = 0
 +munmap(0x7f24cabf5000,​ 237552) ​         = 0
 +fstat(1, {st_mode=S_IFCHR|0620,​ st_rdev=makedev(136,​ 9), ...}) = 0
 +mmap(NULL, 4096, PROT_READ|PROT_WRITE,​ MAP_PRIVATE|MAP_ANONYMOUS,​ -1, 0) = 0x7f24cac2e000
 +write(1, "O valor aproximado de Pi \303\251: 3.1"​...,​ 38O valor aproximado de Pi é: 3.141592
 +) = 38
 +exit_group(0) ​                          = ?
 ++++ exited with 0 +++
 +</​code>​
 +
 +De forma similar, o comando ''​ltrace''​ gera uma listagem sequencial de todas as chamadas de biblioteca geradas durante a  execução de um programa:
 +
 +<​code>​
 +$ ltrace ./pi
 +__libc_start_main(0x40063d,​ 1, 0x7ffdb52b7238,​ 0x4006e0 <​unfinished ...>
 +pow(1, 0x7ffdb52b7238,​ 0x7ffdb52b7248,​ 0)        = 0x3ff0000000000000
 +pow(1, 0x3ff00000, 0x7fefffffffffffff,​ 0x7fffffffffffffff) = 0x3ff0000000000000
 +pow(1, 0x3ff00000, 0x7fefffffffffffff,​ 0x7fffffffffffffff) = 0x3ff0000000000000
 +pow(1, 0x3ff00000, 0x7fefffffffffffff,​ 0x7fffffffffffffff) = 0x3ff0000000000000
 +pow(0x4330000000000000,​ 0xc3700000, 0x7fefffffffffffff,​ 0x7fffffffffffffff) = 0x3ff0000000000000
 +pow(0x4330000000000000,​ 0xc3700000, 0x7fefffffffffffff,​ 0x7fffffffffffffff) = 0x3ff0000000000000
 +pow(0x4330000000000000,​ 0xc3700000, 0x7fefffffffffffff,​ 0x7fffffffffffffff) = 0x3ff0000000000000
 +...
 +pow(0x4330000000000000,​ 0xc3700000, 0x7fefffffffffffff,​ 0x7fffffffffffffff) = 0x3ff0000000000000
 +pow(0x4330000000000000,​ 0xc3700000, 0x7fefffffffffffff,​ 0x7fffffffffffffff) = 0x3ff0000000000000
 +pow(0x4330000000000000,​ 0xc3700000, 0x7fefffffffffffff,​ 0x7fffffffffffffff) = 0x3ff0000000000000
 +printf("​O valor aproximado de Pi \303\251: %f\n"​...,​ 3.141493) = 38
 +O valor aproximado de Pi é: 3.141493
 ++++ exited (status 0) +++
 +</​code>​
 +
 +===== Performance profiling =====
 +
 +Além da depuração propriamente dita, em busca de erros, por vezes torna-se necessário analisar o comportamento temporal do programa. Para realizar essa análise pode-se utilizar o //GNU Profiler// (''​gprof''​). O ''​gprof''​ permite verificar:
 +
 +  * o tempo dispendido em cada função
 +  * o grafo de chamadas
 +    * que funções são chamadas por que funções
 +    * que funções chamam outras funções
 +  * quantas vezes cada função é chamada
 +  * ...
 +
 +Para realizar o //​profiling//​ de um executável,​ é necessário inicialmente compilá-lo com o flag adequado (''​-pg''​) e em seguida executá-lo:​
 +
 +<​code>​
 +$ gcc -pg -g -o pi pi.c -lm
 +$ pi
 +</​code>​
 +
 +A execução irá gerar um arquivo binário ''​gmon.out'',​ que contém os dados de //​profiling//​. Esse arquivo é usado pelo utilitário ''​gprof''​ para gerar as estatísticas desejadas: ​
 +
 +<​code>​
 +$ gprof pi gmon.out
 +</​code>​
 +
 +
 +Um exemplo de relatório de saída do programa ''​gprof''​ pode ser encontrado {{unix:​exemplo.gprof.txt|neste arquivo}}. Mais detalhes e opções de relatório podem ser obtidas no [[http://​www.gnu.org/​software/​binutils/​manual/​gprof-2.9.1/​|manual GNU gprof]].
 +
 +O grafo de chamadas (//call graph//) pode ser visualizado de forma gráfica através da ferramenta [[https://​github.com/​jrfonseca/​gprof2dot|Gprof2dot]].
 +
 +O Valgrind também permite realizar //​profiling//,​ através de sua ferramenta interna ''​callgrind''​ e do visualizador externo [[https://​kcachegrind.github.io/​html/​Home.html|KCachegrind]].
 +
 +===== Tratamento de erros =====
 +
 +A maior parte das chamadas de sistema e funções UNIX retorna erros na forma de códigos numéricos, que são descritos nas páginas de manual das chamadas e funções (vide ''​man fopen''​ para um bom exemplo). Normalmente,​ uma chamada com erro retorna o valor -1 e ajusta a variável global inteira ''​errno''​ para o código do erro. Além disso, as seguintes funções podem ser úteis na interpretação dos erros:
 +
 +  * ''​assert (int expression)''​ : se a expressão indicada for falsa, encerra a execução com uma mensagem de erro da forma:<​code>​assertion failed in file xxx.c, function yyy(), line zzz</​code>​
 +  * ''​perror (char * msg)''​ : imprime na saída de erro (''​stderr''​) a mensagem ''​msg''​ seguida de uma descrição do erro encontrado. O programa não é encerrado.
 +  * ''​strerror(int errnum)''​ : retorna a descrição do erro indicado por ''​errnum''​.
 +
 +Além disso, alguns erros de execução, como operações matemáticas inválidas (divisão por zero, etc) ou violações de acesso à memória (ponteiros inválidos, etc) pode ser interceptados e tratados pelo programa, sem que seja necessária sua finalização. Esses erros geram sinais que são enviados ao processo em execução, que pode interceptá-los e tratá-los de forma a contornar o erro e continuar funcionando.
 +
 +===== Rubber duck debugging =====
 +
 +Se nada mais funcionar, **explique seu código a alguém** !
 +
 +Extraído do livro //The Practice of Programming//,​ de Brian W. Kernighan and Rob Pike: //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.//​
 +
 +{{  debuggingduck.jpg ​ |}}
 +
 +===== Outras ferramentas =====
 +
 +  * ''​strip'':​ permite remover os símbolos e código não usado de um executável ou arquivo-objeto,​ diminuindo consideravelmente seu tamanho (mas impedindo futuras depurações no mesmo).
 +  * ''​diff'':​ permite comparar dois arquivos ou diretórios (recursivamente),​ apontando as diferenças entre seus conteúdos. Muito útil para comparar diferentes versões de árvores de código fonte.
 +  * ''​patch '':​ permite aplicar um arquivo de diferenças (gerado pelo comando ''​diff''​) sobre uma árvore de arquivos, modificando os arquivos originais de forma a obter uma nova árvore. Muito usado para divulgar novas versões de códigos-fonte muito grandes. ​
 +  * ''​grep'':​ permite encontrar linhas em arquivos contendo uma determinada string ou expressão regular. Pode ser muito útil para encontrar trechos de código específicos em grandes volumes de código.
 +  * ''​indent'':​ reformatador de código-fonte,​ aceita diversas opções de endentação automática.
  
prog2/depuracao.txt · Última modificação: 2019/04/30 12:23 por maziero