Valgrind e Sanitizers
Como detectar erros de memória e comportamento indefinido em C com o Valgrind e os sanitizers do GCC e do Clang.
Em C, é muito fácil escrever um código que compila e até parece funcionar, mas que contém erros graves de memória: acessos fora dos limites de um vetor, uso de memória já liberada, variáveis não inicializadas, vazamentos. Esses erros raramente travam o programa na hora; muitas vezes o problema aparece depois, de forma intermitente, o que os torna difíceis de encontrar "no olho".
Há duas famílias de ferramentas que encontram esses problemas automaticamente:
- o Valgrind, que analisa o programa já compilado enquanto ele executa;
- os sanitizers do GCC e do Clang, que inserem verificações no programa no momento da compilação.
Os editores de código deste site compilam com
-fsanitize=address,undefined. Os exemplos desta página mostram os
relatórios reais: execute-os e abra Ver relatório completo abaixo da
mensagem.
Valgrind
O Valgrind é uma ferramenta de análise dinâmica muito usada para depurar programas em C e C++. Ele funciona como uma camada entre o programa e o processador, acompanhando cada instrução executada. Assim, detecta:
- acessos inválidos à memória (fora dos limites de um bloco, depois do
freeetc.); - vazamentos de memória;
- uso de valores não inicializados;
- problemas de concorrência em programas com várias threads;
- gargalos de desempenho (profiling, análise de cache e de heap).
Como simula a execução instrução por instrução, o Valgrind deixa o programa bem mais lento (cerca de 20 a 30 vezes). Por isso, é usado em testes e depuração, não em produção.
Execução básica
Compile com -g (símbolos de depuração), para que os relatórios mostrem
nomes de funções e números de linha, e execute o programa por meio do
Valgrind:
gcc -g -Wall programa.c -o programa
valgrind ./programaSem outra opção, o Valgrind usa a ferramenta Memcheck (equivale a
--tool=memcheck).
Opções principais
| Opção | Efeito |
|---|---|
--leak-check=full | Relatório detalhado de cada vazamento |
--show-leak-kinds=all | Mostra todos os tipos de vazamento (ver abaixo) |
--track-origins=yes | Mostra onde surgiu um valor não inicializado |
--num-callers=20 | Mostra mais níveis da pilha de chamadas (padrão: 12) |
--error-limit=no | Não limita o número de erros exibidos |
--log-file=saida.txt | Salva o relatório num arquivo |
Os tipos de vazamento são:
- definitely lost: vazamento certo; nenhum ponteiro aponta mais para o bloco;
- indirectly lost: o bloco só era acessível a partir de outro bloco perdido (por exemplo, os nós de uma lista cuja cabeça se perdeu);
- possibly lost: há apenas ponteiros para o meio do bloco; pode ser vazamento;
- still reachable: o bloco não foi liberado, mas ainda havia um ponteiro para ele no fim do programa.
Exemplos de uso
# verificação completa de vazamentos
valgrind --leak-check=full --show-leak-kinds=all ./programa
# origem de valores não inicializados
valgrind --track-origins=yes ./programa
# relatório salvo em arquivo
valgrind --leak-check=full --log-file=valgrind_log.txt ./programa
# pilha de chamadas mais detalhada
valgrind --leak-check=full --num-callers=20 ./programaLendo o relatório
Este programa chama duas vezes uma função que aloca um vetor e nunca o libera:
Compilado com gcc -g e executado com valgrind --leak-check=full, o
relatório (resumido) é:
==12345== HEAP SUMMARY:
==12345== in use at exit: 60 bytes in 2 blocks
==12345== total heap usage: 3 allocs, 1 frees, 4,156 bytes allocated
==12345==
==12345== 20 bytes in 1 blocks are definitely lost in loss record 1 of 2
==12345== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x1091C8: mostraQuadrados (vazamento.c:5)
==12345== by 0x109259: main (vazamento.c:15)
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 2 of 2
==12345== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x1091C8: mostraQuadrados (vazamento.c:5)
==12345== by 0x109263: main (vazamento.c:16)
==12345==
==12345== LEAK SUMMARY:
==12345== definitely lost: 60 bytes in 2 blocksLeia de baixo para cima: o bloco foi alocado por malloc na linha 5 de
vazamento.c, dentro de mostraQuadrados, que foi chamada pelo main na
linha 15 (e, o outro, na linha 16). O 1 frees do resumo é um bloco interno
da biblioteca de entrada e saída.
Mensagens mais comuns:
| Mensagem | Significado |
|---|---|
Invalid read / Invalid write | Acesso fora de um bloco alocado ou a um bloco já liberado |
Use of uninitialised value / Conditional jump or move depends on uninitialised value(s) | Uso de uma variável sem valor inicial |
Invalid free() | free de um endereço não alocado ou já liberado |
Mismatched free() / delete / delete [] | Liberação com a função errada (em C++) |
definitely lost | Vazamento de memória |
still reachable | Memória não liberada, mas ainda acessível no fim |
Dicas
- Compile sempre com
-g. - Comece com entradas pequenas: a execução é lenta.
- Combine com o depurador gdb quando precisar inspecionar o estado do programa.
Sanitizers
São verificações que o próprio compilador (GCC ou Clang) insere no programa.
A opção -fsanitize=address,undefined liga dois deles:
- o AddressSanitizer (ASan), que detecta erros de acesso à memória;
- o UndefinedBehaviorSanitizer (UBSan), que detecta comportamentos indefinidos da linguagem.
Como funcionam
O Valgrind executa o programa pronto dentro de um simulador. Os sanitizers,
ao contrário, atuam na compilação: o compilador acrescenta verificações
ao código gerado. Por exemplo, cerca cada bloco alocado com pequenas regiões
proibidas (redzones), para detectar acessos fora dos limites, e marca a
memória liberada, para detectar uso depois do free. O custo é bem menor
(o programa fica cerca de 2 a 5 vezes mais lento, contra 20 a 30 do
Valgrind), mas é preciso recompilar com a opção.
Compilando e executando
gcc -g -fno-omit-frame-pointer -fsanitize=address,undefined programa.c -o programa
./programaNão há comando extra: se um erro acontecer, o sanitizer imprime o relatório
no stderr durante a execução. As opções -g e -fno-omit-frame-pointer
fazem o relatório mostrar nomes de funções e linhas em vez de apenas
endereços.
AddressSanitizer
Ativado com -fsanitize=address. Detecta:
- acesso fora dos limites (buffer overflow) na heap, na pilha e em variáveis globais;
- uso de memória depois do
free(use-after-free); - double free e
freede endereços inválidos; - vazamentos de memória, por meio do LeakSanitizer, que no Linux vem junto com o ASan.
Execute os exemplos e compare com as mensagens do Valgrind acima:
O vazamento.c da seção do Valgrind também é detectado: execute-o e veja o
relatório do LeakSanitizer, gerado quando o programa termina.
UndefinedBehaviorSanitizer
Ativado com -fsanitize=undefined. Detecta:
- overflow de inteiros com sinal;
- divisão por zero e
INT_MIN / -1; - deslocamentos (shift) inválidos, maiores que o número de bits do tipo ou negativos;
- desreferência de ponteiro
NULLou desalinhado; - conversões que causam overflow.
Várias dessas situações passam despercebidas pelo Valgrind, como o overflow de inteiro abaixo:
-fsanitize=leak: só o LeakSanitizer, mais leve, quando só interessa procurar vazamentos;-fsanitize=thread: ThreadSanitizer (TSan), para condições de corrida (data races) em programas com threads;-fsanitize=memory: MemorySanitizer (MSan), para uso de memória não inicializada; existe só no Clang.
Nesta disciplina usamos -fsanitize=address,undefined, que cobre a grande
maioria dos erros.
-fsanitize=address e -fsanitize=thread não podem ser usados na
mesma compilação. Para depurar concorrência, compile uma versão separada só
com -fsanitize=thread.
O que os sanitizers não pegam
O ASan e o UBSan não detectam uso de variáveis não inicializadas (isso é papel do MemorySanitizer, do Clang, ou do Valgrind). O programa abaixo roda sem nenhum aviso e imprime lixo:
Com valgrind --track-origins=yes, o erro aparece, com a origem do valor (trecho):
==12345== Conditional jump or move depends on uninitialised value(s)
==12345== at 0x48D20CB: __printf_buffer (vfprintf-process-arg.c:58)
==12345== by 0x48C81B2: printf (printf.c:33)
==12345== by 0x109208: main (nao_inicializada.c:12)
==12345== Uninitialised value was created by a stack allocation
==12345== at 0x109169: soma (nao_inicializada.c:3)Ligar os avisos do compilador (-Wall -Wextra), e de preferência compilar
também com otimização (-O2), faz o GCC apontar muitos desses casos já na
compilação.
Valgrind ou sanitizers?
Um programa compilado com -fsanitize=address (ou undefined) não deve
ser executado com o Valgrind. As duas ferramentas controlam a memória de
formas incompatíveis: o ASan já intercepta malloc e free e usa suas
próprias redzones, e o Valgrind tenta fazer o mesmo por cima. Na prática, o
programa trava, fica extremamente lento ou acusa erros que não existem. Para
usar o Valgrind, compile uma versão sem -fsanitize.
| Critério | Valgrind (Memcheck) | Sanitizers (ASan e UBSan) |
|---|---|---|
| Quando atua | Na execução (simulação) | Na compilação (instrumentação) |
| Precisa recompilar | Não | Sim, com -fsanitize=... |
| Lentidão | Alta (cerca de 20 a 30 vezes) | Moderada (cerca de 2 a 5 vezes) |
| Vazamentos | Sim | Sim (LeakSanitizer) |
| Valores não inicializados | Sim | Não (só o MSan, do Clang) |
| Comportamento indefinido | Não | Sim (UBSan) |
| Condições de corrida | Sim (Helgrind e DRD) | Sim (TSan, incompatível com ASan) |
| Relatórios | Pilha de chamadas completa | Aponta a linha exata do erro |
Vantagens e desvantagens
- Não exige recompilação: funciona com qualquer executável compilado com
-g. - Ferramenta madura, com relatórios detalhados e várias ferramentas (Memcheck, Helgrind, Cachegrind, Massif etc.).
- Detecta uso de valores não inicializados.
- Ótimo para uma verificação completa antes de entregar um trabalho.
- Deixa o programa muito mais lento, o que atrapalha testes com entradas grandes.
- Não detecta comportamento indefinido da linguagem (overflow de inteiro, shift inválido etc.).
- Suporte limitado em algumas plataformas (por exemplo, macOS em processadores Apple Silicon).
- Muito mais rápidos: dá para usar em toda compilação, no dia a dia.
- O UBSan cobre erros que o Valgrind não cobre.
- As mensagens apontam diretamente a linha do erro.
- Exigem recompilar o programa.
- ASan e TSan não podem ser usados juntos.
- Não detectam valores não inicializados (no GCC).
- Aumentam o tamanho do executável e o consumo de memória.
Use os sanitizers (-fsanitize=address,undefined) no dia a dia: compile
assim durante todo o desenvolvimento, porque o custo é pequeno e os erros
aparecem assim que o programa roda. Use o Valgrind para uma verificação
mais completa antes de entregar um trabalho, para procurar valores não
inicializados ou quando não puder recompilar o programa.
Exercícios
1. Volte aos programas da seção Encontre e corrija o erro
de Alocação de Memória. Para cada um, identifique no relatório do sanitizer o
tipo do erro (heap-buffer-overflow, heap-use-after-free,
attempting double-free, detected memory leaks) e a linha apontada.
2. Para cada situação, diga qual ferramenta você usaria e por quê:
- (a) um programa às vezes imprime valores estranhos, e você suspeita de uma variável não inicializada;
- (b) você quer verificar, a cada compilação, se há acessos fora dos limites;
- (c) recebeu apenas o executável de um colega (compilado com
-g) e quer procurar vazamentos.
Resposta
- (a) Valgrind com
--track-origins=yes(ou o MemorySanitizer, no Clang): o ASan e o UBSan não detectam valores não inicializados. - (b) Sanitizers: o custo é pequeno o bastante para usar sempre.
- (c) Valgrind com
--leak-check=full: não exige recompilar.
Referências
- Valgrind User Manual.
- AddressSanitizer. Google Sanitizers Wiki.
- Program Instrumentation Options. Manual do GCC.
- UndefinedBehaviorSanitizer. Documentação do Clang.