Menu

Vazamentos de memória em C: como acontecem e como encontrá-los

O que um vazamento realmente é, as três maneiras pelas quais programas em C os produzem, a disciplina de posse que os previne e como achar o resto com valgrind e -fsanitize=address - terminando com um programa vazando consertado passo a passo.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Um vazamento de memória não é memória que sumiu. É memória que ainda é sua, ainda está reservada, e da qual você perdeu a capacidade de devolver - porque o último ponteiro para ela se foi. Nada trava. O programa continua rodando, um pouco mais pesado a cada vez, até que algo acabe falhando em um lugar não relacionado.

Esta página cobre como vazamentos surgem, a disciplina que previne a maioria deles e as duas ferramentas que acham o resto.

Como é um vazamento

Cada iteração sobrescreve block com um ponteiro novo. O bloco anterior continua alocado; nenhuma variável guarda seu endereço; ele nunca poderá ser liberado. Três iterações perdem doze kilobytes. Um servidor fazendo isso uma vez por requisição perde para sempre, na taxa em que as requisições chegarem.

A correção é uma linha - free(block); no fim do corpo - mas reconhecer onde ela cabe é a habilidade de verdade.

Como vazamentos acontecem

1. O ponteiro perdido

Qualquer atribuição a um ponteiro que ainda guarda a única referência para um bloco vivo o vaza.

char *name = malloc(32);
name = malloc(64);     /* os primeiros 32 bytes agora são inalcançáveis */

O laço acima é o mesmo bug vestido de laço. O mesmo vale para reatribuir um campo de struct, e o mesmo vale para o atalho de realloc de calloc e realloc:

p = realloc(p, n);     /* em caso de falha: p vira NULL, bloco antigo órfão */

2. O retorno antecipado

Todo caminho de saída de uma função tem que liberar o que a função já tomou. O que é esquecido é sempre um caminho de erro.

O caminho feliz está correto e o caminho de erro vaza, e é por isso que isso sobrevive aos testes: o ramo de falha quase nunca roda durante o desenvolvimento. A correção é uma única seção de limpeza para a qual cada caminho salta:

Este é o único uso de goto que programadores C experientes recomendam ativamente. Funciona porque todo ponteiro começa em NULL e free(NULL) não faz nada, então um único bloco de saída está correto não importa até onde a função tenha chegado.

3. Posse indefinida

Os vazamentos mais sutis não são erros de codificação - são duas funções discordando sobre de quem era o trabalho.

char *build_message(void);   /* quem chama libera isso? */
void  store(char *text);     /* store assume a posse? */

Se build_message devolve memória alocada e store a copia, quem chama tem que liberar. Se store guarda o ponteiro, quem chama não deve liberar. Nada no código diz qual dos dois, então uma das duas suposições é feita duas vezes - e você ganha ou um vazamento ou um free duplo.

O remédio é uma convenção, declarada em um comentário ao lado de toda função que aloca:

/* Devolve uma string recém-alocada; quem chama deve liberá-la. */
char *build_message(void);

/* Assume a posse de 'text'; ele será liberado por store_free(). */
void store(char *text);

Escreva a regra na função, não em um documento de projeto. É o hábito de maior valor em gerenciamento de memória em C.

A disciplina de posse

Quatro regras cobrem quase tudo:

  1. Toda alocação tem exatamente um dono - um trecho de código responsável por liberá-la.
  2. Pareie cada função que aloca com uma que libera. vec_init / vec_free, config_load / config_free. A simetria torna visível uma chamada faltando.
  3. Libere na mesma camada que alocou, a menos que o comentário da função transfira a posse explicitamente.
  4. Ponha o ponteiro em NULL depois de liberá-lo, para que um uso acidental posterior trave no ponto da falha em vez de corromper o heap em silêncio.

Encontrando vazamentos: valgrind

No Linux, o valgrind não precisa de recompilação, embora símbolos de depuração deixem o relatório legível:

gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program

Para o laço que vaza no topo desta página, o relatório termina com algo assim:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 12,000 bytes in 3 blocks
==12345==   total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345==    by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 12,000 bytes in 3 blocks

Leia de baixo para cima. "definitely lost" significa que nenhum ponteiro para o bloco existia na saída - um vazamento de verdade. O rastro de pilha nomeia a linha do malloc que o criou, não a linha onde ele foi perdido, o que geralmente basta para achar o free que falta.

Aparecem outras duas categorias:

  • indirectly lost - blocos alcançáveis apenas por um bloco que ele mesmo foi perdido, como os elementos de uma lista encadeada vazada. Conserte o "definitely lost" e estes somem.
  • still reachable - alocados na saída mas com um ponteiro vivo, tipicamente um cache global. Não é um vazamento no sentido perigoso, mas vale liberar para que o relatório fique vazio.

O valgrind também pega leituras de memória não inicializada e escritas além do fim de um bloco, que muitas vezes é como você descobre o bug por trás do vazamento.

Encontrando vazamentos: AddressSanitizer

O AddressSanitizer é embutido no GCC e no Clang, roda muito mais rápido que o valgrind e funciona onde o valgrind não funciona (incluindo o macOS atual):

gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program

O relatório de vazamento é impresso automaticamente no encerramento:

=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 12000 byte(s) in 3 object(s) allocated from:
    #0 0x7f... in malloc
    #1 0x1086... in main program.c:6

SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).

O ASan também transforma use-after-free e estouros de buffer no heap em abortos imediatos e claramente rotulados, em vez de corrupções misteriosas mais tarde. Construa suas rodadas de teste com ele ligado; construa as releases com ele desligado, já que ele custa memória e velocidade.

Se a detecção de vazamentos não disparar na sua plataforma, defina ASAN_OPTIONS=detect_leaks=1 no ambiente antes de rodar.

Consertando um programa que vaza, passo a passo

Aqui está um pequeno programa com três vazamentos separados:

O valgrind relata três registros "definitely lost" com três números de linha diferentes. Consertados um de cada vez:

A correção 1 é o comentário de posse tornado real: shout aloca, main libera. A correção 2 remove a alocação dobrada por completo em vez de liberar a primeira - o código mais simples também é o código correto. A correção 3 acrescenta o free que faltava no retorno antecipado; com mais alocações em jogo, o rótulo único cleanup: mostrado antes escala melhor do que repetir os frees.

Hábitos que previnem vazamentos

  • Escreva o free imediatamente depois de escrever o malloc, e só então preencha o código no meio.
  • Dê a cada função que aloca uma função de liberação correspondente.
  • Declare a posse em um comentário em qualquer função que devolve ou recebe um ponteiro que ela alocou.
  • Use um único bloco de saída cleanup: em funções que guardam várias alocações.
  • Rode seus testes com -fsanitize=address por hábito, não só quando algo parece errado.
  • Trate "definitely lost: 0 bytes" como parte de uma rodada de testes aprovada.

Perguntas frequentes

O que é um vazamento de memória em C?

Memória que você alocou com malloc e que não consegue mais liberar, porque nada no programa ainda aponta para ela. O bloco fica reservado por toda a vida do processo. Não é uma falha - o programa continua funcionando, só usando mais memória a cada passagem até acabar ficando sem.

Como encontro vazamentos de memória em C?

Rode o programa sob o valgrind: valgrind --leak-check=full ./program. Ele relata cada bloco ainda alocado na saída, com o rastro de pilha do malloc que o criou. No macOS ou onde o valgrind não está disponível, compile com -fsanitize=address e o mesmo relatório sai no encerramento.

O que causa vazamentos de memória em C?

Três padrões cobrem quase todos: sobrescrever o único ponteiro para um bloco (incluindo p = realloc(p, n) em caso de falha), retornar cedo de uma função que já alocou, e posse indefinida - duas funções cada uma supondo que a outra libera, então nenhuma libera.

Vazamentos de memória importam se o programa termina de qualquer jeito?

Para um programa que roda uma vez e termina, o sistema operacional recupera tudo, então o impacto prático é nulo. Importa para qualquer coisa de longa duração - um servidor, um laço de jogo, um daemon - onde um vazamento por requisição cresce sem limite. Libere de forma consistente mesmo assim: um relatório de vazamento é ruído que esconde os que realmente importam.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR