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
c:depuracao [2023/08/15 14:54] mazieroc:depuracao [2024/10/25 17:17] (atual) maziero
Linha 1: Linha 1:
 +====== 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.