Vazamentos e erros de memória
Esta aula aborda os principais erros de gerenciamento de memória em C: memory leaks, double free e use-after-free, além de apresentar o Valgrind como ferramenta de detecção. São explicados conceitos, causas e consequências, com exemplos práticos de código. Ao final, exercícios e referências aprofundam o aprendizado.
Gerenciar memória manualmente é uma das responsabilidades mais críticas em C. Diferente de linguagens com coleta de lixo, o programador deve alocar e liberar explicitamente a memória. Erros nesse processo podem levar a vazamentos, corrupção de dados, falhas de segmentação e comportamentos imprevisíveis. Nesta aula, exploraremos os três tipos mais comuns de erros de memória e como detectá-los com ferramentas como Valgrind.
Compreender esses problemas é essencial para escrever código robusto, especialmente em sistemas embarcados, drivers ou aplicações de alto desempenho. Vamos mergulhar em cada um deles com exemplos práticos.
Memory leaks
Um memory leak (vazamento de memória) ocorre quando um programa aloca memória dinamicamente (com malloc, calloc, etc.) mas nunca a libera com free. A memória alocada permanece ocupada até o término do programa, mas se o programa perder a referência para ela (por exemplo, sobrescrevendo o ponteiro), não há como liberá-la. Com o tempo, o programa consome cada vez mais memória, podendo causar lentidão ou falha por falta de recursos.
Exemplo clássico:
#include <stdlib.h>
void leak() {
int *p = malloc(sizeof(int) * 10);
p[0] = 42;
// Nunca chama free(p);
}
int main() {
leak();
// O ponteiro p foi perdido, memória não liberada
return 0;
}Para evitar vazamentos, sempre pareie cada malloc com um free, e use ferramentas como Valgrind para verificar se toda memória alocada foi liberada.
Double free
Double free ocorre quando free é chamado duas vezes para o mesmo bloco de memória. Isso corrompe as estruturas internas do gerenciador de memória (heap), podendo causar falhas de segmentação ou vulnerabilidades de segurança. Após liberar um bloco, o ponteiro ainda contém o endereço (ponteiro solto ou dangling pointer), mas o conteúdo não é mais válido.
Exemplo:
#include <stdlib.h>
int main() {
int *p = malloc(sizeof(int));
*p = 10;
free(p);
// ...
free(p); // ERRO: double free
return 0;
}Para evitar, atribua NULL ao ponteiro após free, pois free(NULL) é seguro (não faz nada). Assim, uma segunda chamada não causará dano.
free(p);
p = NULL;Use-after-free
Use-after-free (UAF) ocorre quando o programa acessa memória depois de ela ter sido liberada. O ponteiro ainda aponta para o endereço, mas a memória pode ter sido reutilizada por outra alocação. Isso pode levar a leitura/escrita de dados corrompidos ou, pior, a exploração por ataques.
Exemplo:
#include <stdlib.h>
#include <stdio.h>
int main() {
int *p = malloc(sizeof(int));
*p = 5;
free(p);
// ...
printf("%d\n", *p); // ERRO: use-after-free
return 0;
}Prevenção: após free, defina o ponteiro como NULL e verifique antes de usar. Em programas complexos, use ferramentas de análise dinâmica para detectar UAF.
Valgrind (visão geral)
Valgrind é uma ferramenta de instrumentação de código que detecta automaticamente muitos erros de memória. Seu módulo padrão, Memcheck, monitora alocações e liberações, reportando vazamentos, double free, use-after-free e acesso a memória não inicializada.
Uso básico:
gcc -g -o meu_programa meu_programa.c
valgrind --leak-check=full ./meu_programaA flag -g inclui informações de depuração, permitindo que Valgrind aponte linhas exatas. Exemplo de saída típica:
==12345== HEAP SUMMARY:
==12345== in use at exit: 40 bytes in 1 blocks
==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2B0E0: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x4004E7: main (meu_programa.c:5)Valgrind é essencial no desenvolvimento C, mas lembre-se de que ele diminui a velocidade de execução (cerca de 10-20x). Use-o durante testes, não em produção.
Boas práticas e observações finais
Sempre inicialize ponteiros com NULL. Após free, atribua NULL ao ponteiro. Evite alocações desnecessárias; prefira variáveis automáticas quando possível. Use ferramentas de análise estática (como splint) e dinâmica (Valgrind, AddressSanitizer) regularmente. Documente a responsabilidade de liberação de memória em funções que retornam ponteiros alocados dinamicamente.
Exercícios
- O código abaixo contém um memory leak. Identifique onde e corrija.
#include <stdlib.h> void func() { int *arr = malloc(100 * sizeof(int)); arr[0] = 1; } int main() { func(); return 0; }✓ Resposta: O memory leak está emfunc()porque a memória alocada pormallocnunca é liberada. Após o uso, deve-se chamarfree(arr). Código corrigido:void func() { int *arr = malloc(100 * sizeof(int)); arr[0] = 1; free(arr); } - Explique por que o seguinte código causa double free e como corrigi-lo.
#include <stdlib.h> int main() { int *p = malloc(sizeof(int)); free(p); free(p); return 0; }✓ Resposta: O erro ocorre porquefree(p)é chamado duas vezes. Após o primeirofree, o ponteiropainda contém o endereço, mas a memória foi liberada. O segundofreetenta liberar memória já liberada, corrompendo o heap. Correção: atribuirp = NULLapós o primeirofree, poisfree(NULL)é seguro. Código corrigido:free(p); p = NULL; free(p); // seguro, não faz nada - O que é use-after-free? Dê um exemplo e como evitá-lo.✓ Resposta: Use-after-free é quando se acessa memória após ela ter sido liberada. Exemplo:
Para evitar, apósint *p = malloc(sizeof(int)); *p = 5; free(p); printf("%d", *p); // errofree, defina o ponteiro comoNULLe verifique antes de usar:if (p != NULL) printf(...). - Como usar Valgrind para detectar memory leaks? Escreva o comando básico.✓ Resposta: Compile com
-ge execute:
Valgrind reportará blocos alocados não liberados.gcc -g -o prog prog.c valgrind --leak-check=full ./prog - Considere o código:
Que tipo de erro ocorre? Como corrigir?void foo() { char *s = malloc(10); sprintf(s, "teste"); free(s); printf("%s", s); }✓ Resposta: Ocorre use-after-free, poisprintfacessasdepois defree(s). Correção: moverprintfantes defreeou copiar a string antes de liberar.void foo() { char *s = malloc(10); sprintf(s, "teste"); printf("%s", s); free(s); }