Próxima revisão | Revisão anterior |
c:depuracao [2023/08/01 18:07] – criada maziero | c:depuracao [2024/10/25 17:17] (atual) – maziero |
---|
| ====== 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: |
| |
| <code> |
| $ gcc -g -o fatorial fatorial.c |
| </code> |
| |
| 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''): |
| |
| <code> |
| $ 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 |
| </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: |
| |
| |< 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}}. |
| |
| <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. |
| |
| Para testar, use 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]]. |
| |
| 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. |
| |
| <note tip> |
| 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**. |
| </note> |
| |
| 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) e analisar seu estado:<code>$ gdb memerror core</code> |
| |
| <note important> |
| 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//. |
| </note> |
| |
| ===== 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 ==== |
| |
| 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: |
| |
| <code c errors.c> |
| #include <stdio.h> |
| #include <stdlib.h> |
| |
| #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) |
| } |
| </code> |
| |
| 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: |
| |
| <code> |
| $ gcc -Wall -g errors.c -o errors |
| $ valgrind --leak-check=full ./errors |
| </code> |
| |
| 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**: |
| |
| <code> |
| ==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) |
| </code> |
| |
| * 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**: |
| |
| <code> |
| ==29519== Conditional jump or move depends on uninitialised value(s) |
| ==29519== at 0x108702: main (errors.c:18) |
| </code> |
| |
| * a condicional na linha 18 depende de um valor não inicializado (pode conter lixo) |
| |
| O trecho abaixo diz respeito ao **erro 3**: |
| |
| <code> |
| ==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) |
| </code> |
| |
| * ''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**: |
| |
| <code> |
| ==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) |
| </code> |
| |
| * 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**: |
| |
| <code> |
| ==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) |
| </code> |
| |
| * //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}}: |
| |
| <code> |
| $ 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 +++ |
| |
| </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 ./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) +++ |
| </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 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: |
| |
| <code> |
| $ gcc -pg -g -o fatorial.c |
| $ ./fatorial |
| </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 fatorial gmon.out |
| </code> |
| |
| 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. |
| |