Na aula anterior, o GDB nos deu poder sobre a lógica do programa — pausar, inspecionar, avançar. Mas há uma categoria de bugs em C que o GDB sozinho tem dificuldade de pegar: os erros de memória. São os mais traiçoeiros da linguagem, porque frequentemente não travam onde acontecem — um vazamento consome recursos silenciosamente, um acesso inválido corrompe dados que só causam estrago muito depois. Já encontramos as ferramentas certas para isso lá na aula Vazamentos de Memória e Como Caçá-los, quando fechamos a fase de memória. Hoje voltamos a elas com profundidade e as integramos ao nosso fluxo de trabalho profissional, porque, em C, dominá-las não é opcional.
Por que erros de memória são especiais
Vale relembrar por que essa categoria de bug merece ferramentas próprias. Um erro de lógica normalmente produz um resultado errado imediato e reproduzível — você vê a saída incorreta e investiga. Um erro de memória é diferente: ele viola as regras do gerenciamento de memória de formas que o C não detecta por conta própria. Ler além de um vetor, usar memória já liberada, esquecer um free — nada disso gera um erro claro na hora. O programa pode continuar como se nada fosse, produzir lixo esporadicamente, ou travar num ponto totalmente distante da causa. É essa distância entre a causa e o sintoma que torna esses bugs tão difíceis, e que justifica ferramentas especializadas capazes de vigiar cada acesso à memória.
Valgrind: revisão e aprofundamento
O Valgrind, que conhecemos na aula Vazamentos de Memória e Como Caçá-los, executa seu programa dentro de uma máquina virtual instrumentada, monitorando cada alocação e cada acesso à memória. O uso básico, relembrando:
gcc -g programa.c -o programa
valgrind --leak-check=full ./programa
A flag -g (a mesma do GDB) permite ao Valgrind apontar linhas exatas, e --leak-check=full pede o relatório detalhado de vazamentos. Já vimos que ele detecta vazamentos, mas seu alcance é maior. O Valgrind flagra toda a família de erros de memória: vazamentos (memória alocada e nunca liberada), acessos inválidos (leitura ou escrita fora dos limites de um bloco), uso de memória não inicializada (ler uma variável antes de atribuir-lhe valor), use-after-free (usar memória já liberada) e double-free (liberar duas vezes). Cada categoria vem com uma mensagem específica e a localização no código.
Interpretando os relatórios do Valgrind
O valor do Valgrind está em ler seus relatórios com fluência. Veja um caso de acesso inválido:
#include <stdlib.h>
int main(void) {
int *v = malloc(5 * sizeof(int));
v[10] = 42; // escreve fora dos limites do bloco de 5 ints
free(v);
return 0;
}
O Valgrind produz algo como:
Invalid write of size 4
at 0x...: main (programa.c:4)
Address 0x... is 20 bytes after a block of size 20 alloc'd
at 0x...: malloc
by 0x...: main (programa.c:3)
Leia a história que ele conta: houve uma escrita inválida de 4 bytes (Invalid write of size 4) na linha 4; o endereço acessado está 20 bytes depois de um bloco de 20 bytes (5 inteiros × 4) alocado na linha 3. Em outras palavras, o Valgrind identificou exatamente que você escreveu além do vetor, e ligou o acesso inválido (linha 4) à alocação original (linha 3). Essa capacidade de conectar o sintoma à origem é o que torna o Valgrind tão valioso — ele transforma um comportamento indefinido invisível numa mensagem precisa e acionável.
Os sanitizers: velocidade e integração
A alternativa complementar, que também vimos na aula Vazamentos de Memória e Como Caçá-los, são os sanitizers — instrumentação embutida no próprio compilador, ativada por flags. O AddressSanitizer (ASan) é o principal:
gcc -g -fsanitize=address programa.c -o programa
./programa
Rodando o mesmo programa com o bug de acesso inválido, o ASan interrompe a execução no exato momento do erro e imprime um relatório detalhado, com a pilha de chamadas do ponto do erro e a descrição do problema (heap-buffer-overflow). A diferença central em relação ao Valgrind é o desempenho: os sanitizers deixam o programa apenas algumas vezes mais lento, enquanto o Valgrind pode torná-lo dezenas de vezes mais lento. Isso faz dos sanitizers a escolha ideal para uso frequente e para rodar em testes automatizados.
Há vários sanitizers, cada um focado numa categoria. O AddressSanitizer (-fsanitize=address) pega acessos inválidos, use-after-free e (com o LeakSanitizer acoplado) vazamentos. O UndefinedBehaviorSanitizer (-fsanitize=undefined) flagra comportamentos indefinidos como estouro de inteiros com sinal e deslocamentos inválidos — um prenúncio da próxima aula. Você pode combiná-los na compilação para cobertura ampla.
Valgrind versus sanitizers: quando usar cada um
Não é uma escolha excludente — programadores experientes usam ambos, em momentos diferentes. Os sanitizers brilham no ciclo de desenvolvimento diário e nos testes automatizados: são rápidos o suficiente para você compilar sempre com eles ligados durante o desenvolvimento, pegando erros no instante em que surgem. O Valgrind brilha nas auditorias profundas: não exige recompilar (roda sobre o binário já compilado), detecta algumas categorias que os sanitizers podem não pegar, e é excelente para uma varredura completa antes de considerar um código pronto. A combinação prática que muitos adotam: sanitizers ligados durante todo o desenvolvimento e nos testes contínuos, e uma passada final de Valgrind como verificação de qualidade. Ter as duas ferramentas no arsenal, e saber quando cada uma serve melhor, é parte de trabalhar profissionalmente em C.
Integrando ao fluxo de trabalho
A lição maior desta aula não é apenas saber que essas ferramentas existem, mas incorporá-las ao hábito. Em C, onde a linguagem confia em você e não vigia a memória, essas ferramentas são a rede de segurança que a linguagem não oferece. O fluxo profissional recomendado: durante o desenvolvimento, compile sempre com -g -fsanitize=address para pegar erros de memória assim que aparecem; ao escrever testes (tema da aula Testes Automatizados em C), rode-os sob sanitizers para que qualquer erro de memória falhe o teste; e, periodicamente ou antes de entregas importantes, faça uma auditoria completa com valgrind --leak-check=full. Adotar esse hábito muda a natureza da programação em C — em vez de descobrir erros de memória tarde, em produção, você os pega cedo, na sua própria máquina, com localização precisa. É a diferença entre software frágil e software confiável.
Uma nota sobre combinar com o GDB
As ferramentas de hoje e o GDB da aula anterior trabalham juntas. Um fluxo poderoso: quando o AddressSanitizer aponta um erro numa linha específica, você abre o programa no GDB, coloca um breakpoint naquela linha, e investiga o estado exato que levou ao problema — inspecionando variáveis e a pilha de chamadas. O sanitizer encontra o erro e sua localização; o GDB permite investigar o contexto que o causou. Juntos, eles cobrem tanto o "onde" quanto o "porquê" dos bugs de memória, formando o arsenal completo de depuração do programador C.
O que vem a seguir
Hoje aprofundamos e integramos ao fluxo de trabalho as ferramentas de caça a bugs de memória: o Valgrind, para auditorias profundas e leitura precisa de relatórios, e os sanitizers, para detecção rápida no dia a dia e nos testes. Vimos por que erros de memória exigem ferramentas próprias — a distância entre causa e sintoma — e como combiná-las com o GDB. Na próxima aula, deixamos temporariamente a depuração para resolver um problema que cresce com todo projeto: a compilação. Conforme acumulamos múltiplos arquivos (como na aula Compilação Separada: Cabeçalhos e Múltiplos Arquivos), recompilar tudo à mão vira um tormento. O Make vai automatizar esse processo, recompilando apenas o necessário com um único comando.
Fontes e leituras recomendadas
- Valgrind — manual do Memcheck e guia de interpretação de relatórios — https://valgrind.org/docs/manual/mc-manual.html
- AddressSanitizer — documentação e comparação com o Valgrind — https://github.com/google/sanitizers/wiki/AddressSanitizer
- UndefinedBehaviorSanitizer — https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
- The C Programming Language (K&R) — fundamenta o modelo de memória que as ferramentas vigiam
- CERT C — a família MEM de regras, que essas ferramentas ajudam a garantir — https://wiki.sei.cmu.edu/confluence/display/c
Exercícios
Exercício 1
Escreva um programa que aloque um vetor de 5 inteiros e leia (com printf) a posição v[7], fora dos limites. Compile com -g -fsanitize=address, rode, e leia o relatório. Que tipo de erro o ASan reporta e em que linha?
Ver resposta
✓ Resposta:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *v = malloc(5 * sizeof(int));
printf("%d\n", v[7]); // leitura fora dos limites
free(v);
return 0;
}
Compilado com -g -fsanitize=address, o ASan reporta um erro heap-buffer-overflow (estouro de buffer no heap), do tipo READ, apontando a linha do printf como o local do acesso inválido. Ele também informa o tamanho do bloco alocado e a que distância do bloco o acesso ocorreu, além de mostrar onde o bloco foi alocado (a linha do malloc). O programa é interrompido no exato momento do acesso inválido.
Exercício 2
Escreva um programa com um vazamento claro (aloque memória numa função e não a libere). Rode sob valgrind --leak-check=full e identifique, no relatório, quantos bytes vazaram e em que linha foram alocados.
Ver resposta
✓ Resposta:
#include <stdlib.h>
void vazar(void) {
int *p = malloc(100 * sizeof(int)); // 400 bytes
p[0] = 1;
// sem free
}
int main(void) {
vazar();
return 0;
}
Sob valgrind --leak-check=full, o relatório mostra 400 bytes in 1 blocks are definitely lost, apontando a linha do malloc dentro de vazar como o local da alocação perdida, e a cadeia de chamadas (vazar chamada por main). O resumo indica 1 allocs, 0 frees, evidenciando o desequilíbrio.
Exercício 3
Escreva um programa com um use-after-free (libere um ponteiro e depois o desreferencie). Rode-o sob o AddressSanitizer e descreva o que o relatório informa sobre onde a memória foi alocada, liberada e usada indevidamente.
Ver resposta
✓ Resposta:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p); // use-after-free
return 0;
}
O AddressSanitizer reporta um erro heap-use-after-free, interrompendo na linha do printf. O relatório do ASan é rico: ele mostra três pilhas de chamadas — onde o acesso inválido ocorreu (a linha do printf), onde a memória foi liberada (a linha do free), e onde a memória foi originalmente alocada (a linha do malloc). Essa tripla informação — alocação, liberação e uso indevido — deixa a sequência completa do erro clara, permitindo entender exatamente o que aconteceu.
Exercício 4
Explique, com suas palavras, por que erros de memória em C são mais difíceis de depurar do que erros de lógica. O que significa dizer que há uma "distância entre a causa e o sintoma"?
Ver resposta
✓ Resposta: Erros de lógica normalmente produzem um resultado incorreto imediato e reproduzível: a saída está errada logo ali, e você pode rastrear o cálculo de volta. Erros de memória são mais difíceis porque o C não os detecta: ao ler fora de um vetor ou usar memória liberada, o programa não gera erro nem para — ele simplesmente acessa o que estiver naquele endereço e continua. A "distância entre a causa e o sintoma" significa que o ponto onde o erro acontece (por exemplo, escrever além de um buffer, corrompendo memória vizinha) pode estar muito longe, no tempo e no código, do ponto onde o problema finalmente se manifesta (um valor estranho ou um crash numa função totalmente diferente, executada muito depois). Você observa o sintoma num lugar, mas a causa está em outro — e, sem ferramentas, não há pista ligando os dois. Um vazamento é ainda mais silencioso: seu sintoma (memória esgotada) pode aparecer horas ou dias após a causa. Essa desconexão é o que torna os erros de memória tão traiçoeiros e o que justifica ferramentas que vigiam cada acesso e ligam o sintoma à origem.
Exercício 5
Compare Valgrind e sanitizers em três aspectos: necessidade de recompilar, impacto no desempenho, e situação ideal de uso. Descreva um fluxo de trabalho que aproveite os pontos fortes de cada um.
Ver resposta
✓ Resposta: Necessidade de recompilar: os sanitizers exigem recompilar o programa com a flag apropriada (-fsanitize=...); o Valgrind não exige recompilação — roda sobre o binário já compilado (embora -g ajude a localizar os erros). Impacto no desempenho: os sanitizers deixam o programa moderadamente mais lento (algumas vezes); o Valgrind o deixa muito mais lento (dezenas de vezes), por emular cada acesso à memória. Situação ideal de uso: os sanitizers são ideais para o desenvolvimento diário e testes automatizados, por serem rápidos o bastante para rodar sempre; o Valgrind é ideal para auditorias profundas e ocasionais, por sua abrangência e por não exigir recompilação. Fluxo de trabalho combinado: durante o desenvolvimento, compile sempre com -g -fsanitize=address para pegar erros de memória no instante em que surgem; rode a suíte de testes sob sanitizers, para que qualquer erro de memória faça um teste falhar; e, antes de entregas importantes ou periodicamente, faça uma auditoria completa com valgrind --leak-check=full como verificação final de qualidade. Assim, os sanitizers dão feedback rápido e contínuo, e o Valgrind dá a varredura profunda de segurança — cada um no momento em que rende mais.