Ferramentas do usuário

Ferramentas do site


prog2:depuracao

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, por exemplo, seria:

$ gcc -g -o pi pi.c -lm

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 (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 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

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 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. Experimente usar 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:

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 EMACS.

Também existem diversas interfaces gráficas para o GDB, como o Data Display Debugger (ddd), Nemiver, Gede e 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.

Record and Replay: RR

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 (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):

  1. Habilitar a geração de arquivo core no terminal atual:
    $ ulimit -c unlimited
  2. Compilar o programa com o flag de depuração (-g):
    $ cc -g memerror.c -o memerror
  3. Lançar o programa:
    $ memerror
  4. Quando o programa abortar por erro, será gerado um arquivo core no diretório corrente:
  5. $ 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
  6. 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.

  7. Uma vez obtido o arquivo core, basta abri-lo através do depurador (GDB):
    $ gdb memerror 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 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.

Depuradores de memória

Ferramentas como Mtrace e 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:

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)
}

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.

Este trecho 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)

Este trecho diz respeito ao erro 2:

==29519== Conditional jump or move depends on uninitialised value(s)
==29519==    at 0x108702: main (errors.c:18)

Este trecho 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)

Este trecho 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)

Este trecho 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)

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 pi.c:

$ 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 +++

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 ./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) +++

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:

$ gcc -pg -g -o pi pi.c -lm
$ pi

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 pi gmon.out

Um exemplo de relatório de saída do programa gprof pode ser encontrado neste arquivo. Mais detalhes e opções de relatório podem ser obtidas no manual GNU gprof.

O grafo de chamadas (call graph) pode ser visualizado de forma gráfica através da ferramenta Gprof2dot.

O Valgrind também permite realizar profiling, através de sua ferramenta interna callgrind e do visualizador externo 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:
    assertion failed in file xxx.c, function yyy(), line zzz
  • 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.

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: 2020/04/09 15:14 por maziero