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_programa

A 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

  1. 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á em func() porque a memória alocada por malloc nunca é liberada. Após o uso, deve-se chamar free(arr). Código corrigido:
    void func() {
        int *arr = malloc(100 * sizeof(int));
        arr[0] = 1;
        free(arr);
    }
  2. 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 porque free(p) é chamado duas vezes. Após o primeiro free, o ponteiro p ainda contém o endereço, mas a memória foi liberada. O segundo free tenta liberar memória já liberada, corrompendo o heap. Correção: atribuir p = NULL após o primeiro free, pois free(NULL) é seguro. Código corrigido:
    free(p);
    p = NULL;
    free(p); // seguro, não faz nada
  3. 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:
    int *p = malloc(sizeof(int));
    *p = 5;
    free(p);
    printf("%d", *p); // erro
    Para evitar, após free, defina o ponteiro como NULL e verifique antes de usar: if (p != NULL) printf(...).
  4. Como usar Valgrind para detectar memory leaks? Escreva o comando básico.

    ✓ Resposta: Compile com -g e execute:
    gcc -g -o prog prog.c
    valgrind --leak-check=full ./prog
    Valgrind reportará blocos alocados não liberados.
  5. Considere o código:
    void foo() {
        char *s = malloc(10);
        sprintf(s, "teste");
        free(s);
        printf("%s", s);
    }
    Que tipo de erro ocorre? Como corrigir?

    ✓ Resposta: Ocorre use-after-free, pois printf acessa s depois de free(s). Correção: mover printf antes de free ou copiar a string antes de liberar.
    void foo() {
        char *s = malloc(10);
        sprintf(s, "teste");
        printf("%s", s);
        free(s);
    }

Referências