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 arquivo executável todos 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 (arquivo pi.c):
macalan:~$ gcc -g -o pi pi.c -lm
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:
macalan:~$ 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 | set variable soma = 100 |
bt | Mostra a posição atual do programa (backtrace) | bt |
frame | Seleciona/muda o frame de execução | frame 3 |
^x^a | alterna entre o modo padrão e NCurses |
Uma relação mais extensa de comandos pode ser encontrada neste GDB Reference Card.
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.
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.
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
: idemOpçõ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).
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.
colocar um roteiro simples de uso do Valgrind, com exemplo.
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:
macalan:~$ ulimit -c unlimited
-g
):macalan:~$ cc -g teste.c -o teste
macalan:~$ teste
macalan:~$ ls -l -rw------- 1 maziero maziero 262144 Nov 30 14:57 core -rwxrwxr-x 1 maziero maziero 9417 Nov 30 14:56 teste -rw-rw-r-- 1 maziero maziero 101 Nov 30 14:56 teste.c
SIGQUIT
(sinal n° 3). Esse sinal deve ser enviado ao processo através do comando kill
(a parti de outro terminal, se necessário):macalan:~$ kill -3 PID
onde PID é o identificador numérico do processo a ser encerrado.
macalan:~$ gdb teste core
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
:
macalan:~$ 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:
macalan:~$ 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) +++
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:
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.
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 nula, 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.
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.
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.