Estrutura de Dados I
Memória dinâmica

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.
Neste site

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 que é o 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 free etc.);
  • 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 ./programa

Sem outra opção, o Valgrind usa a ferramenta Memcheck (equivale a --tool=memcheck).

Opções principais

OpçãoEfeito
--leak-check=fullRelatório detalhado de cada vazamento
--show-leak-kinds=allMostra todos os tipos de vazamento (ver abaixo)
--track-origins=yesMostra onde surgiu um valor não inicializado
--num-callers=20Mostra mais níveis da pilha de chamadas (padrão: 12)
--error-limit=noNão limita o número de erros exibidos
--log-file=saida.txtSalva 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 ./programa

Lendo o relatório

Este programa chama duas vezes uma função que aloca um vetor e nunca o libera:

vazamento.cC

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 blocks

Leia 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:

MensagemSignificado
Invalid read / Invalid writeAcesso 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 lostVazamento de memória
still reachableMemó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

O que são os 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
./programa

Nã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 free de 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:

fora_dos_limites.cC
uso_apos_free.cC

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 NULL ou desalinhado;
  • conversões que causam overflow.

Várias dessas situações passam despercebidas pelo Valgrind, como o overflow de inteiro abaixo:

overflow_inteiro.cC
Outras opções de -fsanitize
  • -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.

Combinações proibidas

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

nao_inicializada.cC

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?

Não use os dois ao mesmo tempo

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érioValgrind (Memcheck)Sanitizers (ASan e UBSan)
Quando atuaNa execução (simulação)Na compilação (instrumentação)
Precisa recompilarNãoSim, com -fsanitize=...
LentidãoAlta (cerca de 20 a 30 vezes)Moderada (cerca de 2 a 5 vezes)
VazamentosSimSim (LeakSanitizer)
Valores não inicializadosSimNão (só o MSan, do Clang)
Comportamento indefinidoNãoSim (UBSan)
Condições de corridaSim (Helgrind e DRD)Sim (TSan, incompatível com ASan)
RelatóriosPilha de chamadas completaAponta a linha exata do erro

Vantagens e desvantagens

Valgrind: vantagens
  • 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.
Valgrind: desvantagens
  • 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).
Sanitizers: vantagens
  • 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.
Sanitizers: desvantagens
  • 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.
Na prática

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

Nesta página