====== Depuração ====== Videos desta aula: {{ progc_debug_gdb.mkv |Parte 1: GDB}}, {{ progc_debug_memoria.mkv |Parte 2: memória}} e {{ progc_debug_tracing.mkv |Parte 3: tracing e outros}}. {{ debug.png|Made by https://www.flaticon.com/authors/freepik}} Depurar significa "purificar" ou "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. ===== Preparação ===== 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 {{ fatorial.c |}}, por exemplo, seria: $ gcc -g -o fatorial fatorial.c Feito isso, o programa está pronto para ser depurado com as ferramentas apresentadas a seguir. ===== Depuração de execução: GDB ===== 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''): $ gdb fatorial GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1 ... (msgs diversas) Lendo símbolos de fatorial...concluído. (gdb) run Starting program: /home/prof/maziero/fatorial O fatorial de 4 é 24 O fatorial de 10 é 3628800 [Inferior 1 (process 24652) exited normally] (gdb) quit 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: |< 100% 20% 45% >| ^ comando ^ ação ^ exemplos ^ | ''run''\\ ''r'' | inicia a execução (//run//) | ''run''\\ ''r < dados.dat''\\ ''r > saida.txt'' | | ''list''\\ ''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//), sem parar dentro das 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}}. 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. Para testar, use o GDB para encontrar os erros de acesso à memória neste programa: {{memerror.c|}}. 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]]. O GDB está integrado em IDEs (//Integrated Development Environments//) gráficos como //Visual Studio Code// e //Eclipse//. Além disso, existem 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]] e [[http://gede.dexar.se|Gede]]. /* ===== Record and Replay: RR ===== FIXME [[https://rr-project.org]] */ ===== 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. O despejo de memória é útil para identificar a causa de erros fatais em programas, sobretudo para **erros esporádicos ou difíceis de reproduzir**. 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: $ ulimit -c unlimited - Compilar o programa com o flag de depuração (''-g''):$ cc -g memerror.c -o memerror - Lançar o programa:$ memerror - Quando o programa abortar por erro, será gerado um arquivo //core// no diretório corrente: - $ 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 - 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):$ kill -3 PID 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) e analisar seu estado:$ gdb memerror core Em algumas versões de Linux (Ubuntu, etc) o serviço //Apport// trata os erros fatais e impede a geração de arquivos //core//. Pode-se desligar temporariamente esse serviço através do comando ''sudo service apport stop'', para obter os arquivos //core//. ===== 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). 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. ==== 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 ==== Ferramentas como [[https://en.wikipedia.org/wiki/Mtrace|Mtrace]] e [[http://valgrind.org/|Valgrind]] realizam a análise dinâmica do programa (ou seja, durante sua execução) para encontrar erros relacionados à memória. A ferramenta mais usada e popular é sem dúvida o **Valgrind**. Ele é particularmente útil para encontrar problemas de vazamento de memória (//memory leaks//), mas pode realizar diversas análises envolvendo memória, caches e //profiling// da execução. Algumas opções usuais do Valgrind são: * ''--tool=toolname'' : escolhe a ferramenta de análise, que pode ser: * ''memcheck'' : análise de acesso à memória (default) * ''cachegrind'' : análise de uso dos caches * ''exp-sgcheck'' : análise mais detalhada de variáveis globais e do stack * ... * ''--leak-check=full'' : relatório detalhado sobre //memory leaks// Eis abaixo um programa com alguns erros de memória frequentes: #include #include #define VETSIZE 100 int main() { int *vet1, *vet2 ; int x ; vet1 = malloc(VETSIZE * sizeof (int)) ; vet2 = malloc(VETSIZE * sizeof (int)) ; // erro 1: acesso a uma posição fora do vetor (buffer overflow) vet1[VETSIZE] = 0 ; // erro 2: leitura de uma variável não inicializada if (x == 0) printf ("x vale zero\n") ; free (vet2) ; // erro 3: liberar duas vezes a mesma área (double free) free (vet2) ; // erro 4: usar uma área após tê-la liberado (use after free) vet2[0] = 0 ; // erro 5: a área de vet1 não foi liberada (memory leak) } Primeiro, deve-se compilar o código com a opção ''-g''. Em seguida, executar o Valgrind com as opções de depuração de memória: $ gcc -Wall -g errors.c -o errors $ valgrind --leak-check=full ./errors O relatório gerado pelo Valgrind pode ser extenso e deve ser analisado com atenção. A seguir destacamos os trechos do relatório relativos aos quatro erros de memória do código acima. O trecho abaixo diz respeito ao **erro 1**: ==29519== Invalid write of size 4 ==29519== at 0x1086F8: main (errors.c:15) ==29519== Address 0x522d1d0 is 0 bytes after a block of size 400 alloc'd ==29519== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==29519== by 0x1086DB: main (errors.c:11) * escrita de 4 bytes inválida, na função ''main'', na linha 15 de ''errors.c'' * essa escrita ocorreu depois do bloco de 400 bytes que foi alocado na linha 11 O trecho abaixo diz respeito ao **erro 2**: ==29519== Conditional jump or move depends on uninitialised value(s) ==29519== at 0x108702: main (errors.c:18) * a condicional na linha 18 depende de um valor não inicializado (pode conter lixo) O trecho abaixo diz respeito ao **erro 3**: ==29519== Invalid free() / delete / delete[] / realloc() ==29519== at 0x4C30D3B: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==29519== by 0x108727: main (errors.c:24) ==29519== Address 0x522d210 is 0 bytes inside a block of size 400 free'd ==29519== at 0x4C30D3B: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==29519== by 0x10871B: main (errors.c:21) ==29519== Block was alloc'd at ==29519== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==29519== by 0x1086E9: main (errors.c:12) * ''free()'' inválido na linha 24 * a área de 400 bytes em questão já foi liberada na linha 21 * essa área havia sido alocada na linha 12. O trecho abaixo diz respeito ao **erro 4**: ==29519== Invalid write of size 4 ==29519== at 0x10872C: main (errors.c:27) ==29519== Address 0x522d210 is 0 bytes inside a block of size 400 free'd ==29519== at 0x4C30D3B: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==29519== by 0x10871B: main (errors.c:21) ==29519== Block was alloc'd at ==29519== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==29519== by 0x1086E9: main (errors.c:12) * escrita de 4 bytes inválida na linha 27 * essa área de memória de 400 bytes já foi liberada na linha 21 * essa área havia sido alocada na linha 12. O trecho abaixo diz respeito ao **erro 5**: ==29519== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==29519== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==29519== by 0x1086DB: main (errors.c:11) * //memory leak//: a área de memória alocada na linha 11 não foi liberada ao encerrar o programa. Como exercício, analise o programa {{memerror.c|}} usando o Valgrind. ===== 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 {{fatorial.c}}: $ strace ./fatorial execve("./fatorial", ["./fatorial"], 0x7fff761a4a10 /* 63 vars */) = 0 brk(NULL) = 0x5628852a9000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=141702, ...}) = 0 mmap(NULL, 141702, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f39dc9c3000 close(3) = 0 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 ... fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0 brk(NULL) = 0x5628852a9000 brk(0x5628852ca000) = 0x5628852ca000 write(1, "O fatorial de 4 \303\251 24\n", 22O fatorial de 4 é 24 ) = 22 write(1, "O fatorial de 10 \303\251 3628800\n", 28O fatorial de 10 é 3628800 ) = 28 exit_group(0) = ? +++ exited with 0 +++ De forma similar, o comando ''ltrace'' gera uma listagem sequencial de todas as chamadas de biblioteca geradas durante a execução de um programa: $ ltrace ./fatorial printf("O fatorial de %d \303\251 %ld\n", 4, 24O fatorial de 4 é 24 ) = 22 printf("O fatorial de %d \303\251 %ld\n", 10, 3628800O fatorial de 10 é 3628800 ) = 28 +++ exited (status 0) +++ ===== 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 gasto em cada função * o grafo de chamadas ([[https://en.wikipedia.org/wiki/Call_graph|call graph]]) * 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: $ gcc -pg -g -o fatorial.c $ ./fatorial 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: $ gprof fatorial gmon.out O relatório de //profilling// do programa {{ demo-profile.c |}} obtido com o ''gprof'' pode ser encontrado {{ profile.txt |neste arquivo}}. O grafo de chamadas (//call graph//) pode ser visualizado de forma gráfica através da ferramenta [[https://github.com/jrfonseca/gprof2dot|Gprof2dot]]. Mais detalhes e opções de relatório podem ser obtidas no [[https://sourceware.org/binutils/docs/gprof/|manual GNU gprof]]. 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]]. ===== 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.// */ > //Outra técnica efetiva [de depuração] é explicar seu código a alguém. Isto vai geralmente fazer você explicar o bug para si mesmo. Às vezes bastam algumas frases, seguidas de um envergonhado "Esqueça, eu estou vendo o que está errado. Desculpe incomodá-lo". Isto funciona notavelmente bem; você pode até mesmo usar não-programadores como ouvintes. O centro de computação de uma universidade mantinha um ursinho de pelúcia perto do "help desk". Alunos com bugs misteriosos deviam explicá-los ao ursinho antes de poder falar com um assistente humano.// > > //[[https://en.wikipedia.org/wiki/The_Practice_of_Programming|The Practice of Programming]]//, Brian Kernighan and Rob Pike, 1999. {{ 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.