C Valgrind

O Valgrind é um programa que roda o seu programa dentro de uma espécie de máquina virtual e observa cada byte de memória que ele usa: cada malloc, cada free, cada leitura e cada escrita. No fim, ele conta quanta memória ficou sem free (os vazamentos) e, durante a execução, avisa sempre que o programa lê ou escreve onde não devia, ou usa uma variável que nunca recebeu valor. A grande vantagem: não precisa recompilar com nenhuma opção especial. Basta escrever valgrind antes do ./programa.

Você vai aprender
  • A rodar valgrind --leak-check=full ./programa e por que compilar com -g.
  • A ler o HEAP SUMMARY e o LEAK SUMMARY: definitely lost, indirectly lost, still reachable.
  • A ler Invalid write, Invalid read e Invalid free().
  • A achar variáveis sem valor inicial: Conditional jump or move depends on uninitialised value(s) e a opção --track-origins=yes.
  • Quando usar o Valgrind e quando usar os sanitizers, e o que fazer no macOS e no Windows.

Todas as saídas desta aula são reais, feitas com o Valgrind 3.26 e o gcc 15 na máquina fictícia da Ana, na pasta ~/c/vg. O Valgrind não funciona no editor do guia (ele precisa de um programa Linux de verdade); as caixas "Experimente" servem para consertar o que ele encontrou. Para instalar no Ubuntu e no Debian:

$ sudo apt install valgrind

A ideia: o contador de estoque

Na lojinha da Ana, cada produto que sai da prateleira (malloc) tem de voltar ou ser registrado como vendido (free). O Valgrind é o funcionário que fica na porta com uma prancheta: anota tudo o que sai, tudo o que volta, e grita quando alguém mexe numa prateleira vazia ou num produto já devolvido. Quando a loja fecha, ele confere a prancheta e diz: "saíram 3 produtos e voltaram 2; o que faltou saiu pela porta tal, às tantas horas".

Para ele poder dizer "às tantas horas" (o arquivo e a linha), compile com -g, como no gdb.


1. O primeiro relatório: um vazamento

A função copiar reserva memória e devolve o ponteiro. O main usa o texto e esquece do free (veja Vazamentos de Memória):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *copiar(const char *texto) {
    char *copia = malloc(strlen(texto) + 1);
    if (copia != NULL) {
        strcpy(copia, texto);
    }
    return copia;
}

int main(void) {
    char *item = copiar("arroz");
    printf("Item: %s\n", item);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -g vaza.c -o vaza
$ valgrind ./vaza
==56079== Memcheck, a memory error detector
==56079== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==56079== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info
==56079== Command: ./vaza
==56079==
Item: arroz
==56079==
==56079== HEAP SUMMARY:
==56079==     in use at exit: 6 bytes in 1 blocks
==56079==   total heap usage: 2 allocs, 1 frees, 4,102 bytes allocated
==56079==
==56079== LEAK SUMMARY:
==56079==    definitely lost: 6 bytes in 1 blocks
==56079==    indirectly lost: 0 bytes in 0 blocks
==56079==      possibly lost: 0 bytes in 0 blocks
==56079==    still reachable: 0 bytes in 0 blocks
==56079==         suppressed: 0 bytes in 0 blocks
==56079== Rerun with --leak-check=full to see details of leaked memory
==56079==
==56079== For lists of detected and suppressed errors, rerun with: -s
==56079== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
==56079==
Toda linha do Valgrind começa com o número do processo (PID) entre ==. Isso separa o que é do Valgrind do que é do seu programa: o Item: arroz não tem o prefixo, porque foi o seu printf.
Memcheck, a memory error detector
O Valgrind é uma caixa de ferramentas; a ferramenta padrão é o Memcheck, "conferidor de memória". As três linhas seguintes são a versão e a licença.
Command: ./vaza
O programa que ele está observando.
HEAP SUMMARY: in use at exit: 6 bytes in 1 blocks
"Resumo do heap: em uso na saída, 6 bytes em 1 bloco": quando o programa terminou, ainda havia 1 bloco reservado e não devolvido (o "arroz": 5 letras + o '\0').
total heap usage: 2 allocs, 1 frees, 4,102 bytes allocated
"Uso total: 2 reservas, 1 liberação, 4.102 bytes reservados" (o Valgrind usa a vírgula para separar milhares, como em inglês). Mas o programa só tem um malloc! O outro é do próprio printf, que reserva 4.096 bytes de buffer para a saída (veja stdin, stdout e stderr) e os devolve sozinho no fim. A conta fecha: 4.096 + 6 = 4.102.
LEAK SUMMARY: definitely lost: 6 bytes in 1 blocks
"Resumo de vazamentos: definitivamente perdidos, 6 bytes": um vazamento de verdade (os outros tipos estão na seção 5).
Rerun with --leak-check=full to see details
"Rode de novo com --leak-check=full para ver os detalhes": ele sabe que vazou, mas não disse onde.
ERROR SUMMARY: 0 errors
"Resumo de erros": conta os erros de acesso à memória (as seções 2 a 4). Sem o --leak-check=full, vazamentos não entram nessa conta.

Os detalhes: --leak-check=full

Quase sempre você vai querer rodar assim:

$ valgrind --leak-check=full ./vaza
==56117== Memcheck, a memory error detector
==56117== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==56117== Using Valgrind-3.26.0 and LibVEX; rerun with -h for copyright info
==56117== Command: ./vaza
==56117==
Item: arroz
==56117==
==56117== HEAP SUMMARY:
==56117==     in use at exit: 6 bytes in 1 blocks
==56117==   total heap usage: 2 allocs, 1 frees, 4,102 bytes allocated
==56117==
==56117== 6 bytes in 1 blocks are definitely lost in loss record 1 of 1
==56117==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==56117==    by 0x40011D0: copiar (vaza.c:6)
==56117==    by 0x400120F: main (vaza.c:14)
==56117==
==56117== LEAK SUMMARY:
==56117==    definitely lost: 6 bytes in 1 blocks
«… as outras linhas do resumo, todas com 0 …»
==56117==
==56117== For lists of detected and suppressed errors, rerun with: -s
==56117== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
6 bytes in 1 blocks are definitely lost in loss record 1 of 1
"6 bytes em 1 bloco estão definitivamente perdidos, no registro de perda 1 de 1" (se houvesse 3 vazamentos diferentes, seriam 3 registros).
at 0x4850858: malloc (vg_replace_malloc.c:447)
A pilha de chamadas de onde o bloco foi reservado, de cima (o mais recente) para baixo. O malloc aparece num arquivo do Valgrind porque ele troca o malloc da biblioteca pelo seu, para anotar tudo.
by 0x40011D0: copiar (vaza.c:6)
"Chamado por copiar, linha 6 de vaza.c": é o malloc(strlen(texto) + 1).
by 0x400120F: main (vaza.c:14)
"…que foi chamada pelo main, linha 14". O conserto: dar free(item) no main quando o item não for mais usado.
ERROR SUMMARY: 1 errors from 1 contexts
Com --leak-check=full, cada vazamento conta como erro.

Missão: conserte o vazamento que o Valgrind encontrou: libere o item no main, depois do printf. (O editor não tem Valgrind e um vazamento não muda a saída; a missão confere o seu código. No seu Linux, rode valgrind --leak-check=full e veja All heap blocks were freed -- no leaks are possible.)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *copiar(const char *texto) {
    char *copia = malloc(strlen(texto) + 1);
    if (copia != NULL) {
        strcpy(copia, texto);
    }
    return copia;
}

int main(void) {
    char *item = copiar("arroz");
    printf("Item: %s\n", item);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *copiar(const char *texto) {
    char *copia = malloc(strlen(texto) + 1);
    if (copia != NULL) {
        strcpy(copia, texto);
    }
    return copia;
}

int main(void) {
    char *item = copiar("arroz");
    printf("Item: %s\n", item);
    free(item);
    return 0;
}

Com o free (no arquivo vaza2.c), o relatório fica assim:

$ gcc -std=c23 -Wall -Wextra -g vaza2.c -o vaza2
$ valgrind --leak-check=full ./vaza2
«… o cabeçalho …»
Item: arroz
==64316== 
==64316== HEAP SUMMARY:
==64316==     in use at exit: 0 bytes in 0 blocks
==64316==   total heap usage: 2 allocs, 2 frees, 4,102 bytes allocated
==64316== 
==64316== All heap blocks were freed -- no leaks are possible
==64316== 
==64316== For lists of detected and suppressed errors, rerun with: -s
==64316== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

2 allocs, 2 frees: a conta fecha. E a última frase é a que você quer ver sempre: "todos os blocos do heap foram liberados: não há vazamento possível".


2. Invalid write e Invalid read: passou do fim

O clássico + 1 esquecido: "Ana Maria" tem 9 letras, mas precisa de 10 bytes por causa do '\0' (veja Strings):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    const char *texto = "Ana Maria";
    char *copia = malloc(strlen(texto));
    if (copia == NULL) {
        return 1;
    }
    strcpy(copia, texto);
    printf("%s\n", copia);
    free(copia);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -g nome.c -o nome
$ valgrind ./nome
==56312== Memcheck, a memory error detector
«… as 3 linhas de sempre …»
==56312== Command: ./nome
==56312==
==56312== Invalid write of size 1
==56312==    at 0x485A2F6: strcpy (vg_replace_strmem.c:565)
==56312==    by 0x4001218: main (nome.c:11)
==56312==  Address 0x4aa6049 is 0 bytes after a block of size 9 alloc'd
==56312==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==56312==    by 0x40011F3: main (nome.c:7)
==56312==
==56312== Invalid read of size 1
==56312==    at 0x485A174: strlen (vg_replace_strmem.c:506)
==56312==    by 0x491087A: puts (ioputs.c:35)
==56312==    by 0x4001224: main (nome.c:12)
==56312==  Address 0x4aa6049 is 0 bytes after a block of size 9 alloc'd
==56312==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==56312==    by 0x40011F3: main (nome.c:7)
==56312==
Ana Maria
==56312==
==56312== HEAP SUMMARY:
==56312==     in use at exit: 0 bytes in 0 blocks
==56312==   total heap usage: 2 allocs, 2 frees, 4,105 bytes allocated
==56312==
==56312== All heap blocks were freed -- no leaks are possible
==56312==
==56312== For lists of detected and suppressed errors, rerun with: -s
==56312== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
Invalid write of size 1
"Escrita inválida de 1 byte": o programa escreveu 1 byte onde não podia.
at … strcpy … / by … main (nome.c:11)
Quem escreveu foi o strcpy, chamado na linha 11. A primeira linha com um arquivo seu é a pista.
Address 0x4aa6049 is 0 bytes after a block of size 9 alloc'd
"O endereço fica 0 bytes depois de um bloco de 9 bytes reservado": logo depois do fim do bloco, exatamente onde o '\0' foi parar. E as duas linhas seguintes dizem onde o bloco foi reservado: nome.c:7, o malloc(strlen(texto)).
Invalid read of size 1 … puts … main (nome.c:12)
"Leitura inválida": na linha 12, o printf (que o gcc trocou pelo puts, mais simples, porque a string termina em \n) leu o mesmo byte proibido, procurando o fim da string.
Ana Maria
Diferente do ASan, o Valgrind não para o programa: anota o erro e deixa seguir. Por isso a saída apareceu e vieram 2 erros.
All heap blocks were freed
Não há vazamento: o free está lá. Um programa pode ter erros de acesso sem vazar, e vice-versa.

3. Memória usada depois do free e free duplo

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *pontos = malloc(3 * sizeof *pontos);
    if (pontos == NULL) {
        return 1;
    }
    pontos[0] = 10;
    free(pontos);
    printf("Pontos: %d\n", pontos[0]);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -g depois.c -o depois
«… o aviso -Wuse-after-free, que você já viu em Sanitizers …»
$ valgrind ./depois
«… o cabeçalho …»
==56357== Invalid read of size 4
==56357==    at 0x40011CB: main (depois.c:11)
==56357==  Address 0x4aa6040 is 0 bytes inside a block of size 12 free'd
==56357==    at 0x48538BF: free (vg_replace_malloc.c:990)
==56357==    by 0x40011C6: main (depois.c:10)
==56357==  Block was alloc'd at
==56357==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==56357==    by 0x400119E: main (depois.c:5)
==56357==
Pontos: 10
«… HEAP SUMMARY …»
==56357== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Três pistas: o programa leu 4 bytes (um int) na linha 11; o endereço fica "0 bytes dentro de um bloco de 12 bytes liberado" (free'd) pelo free da linha 10; e o bloco tinha sido reservado (Block was alloc'd at) na linha 5. Repare que, dentro do Valgrind, o programa mostrou Pontos: 10 (o valor antigo, por sorte), e fora dele mostrou lixo: um erro de memória pode mudar de cara a cada execução.

O free duplo vira Invalid free():

#include <stdlib.h>
#include <string.h>

int main(void) {
    char *nome = malloc(20);
    if (nome == NULL) {
        return 1;
    }
    strcpy(nome, "Ana");
    free(nome);
    free(nome);
    return 0;
}
$ valgrind ./duplo
«… o cabeçalho …»
==57258== Invalid free() / delete / delete[] / realloc()
==57258==    at 0x48538BF: free (vg_replace_malloc.c:990)
==57258==    by 0x40011B2: main (duplo.c:11)
==57258==  Address 0x4aa6040 is 0 bytes inside a block of size 20 free'd
==57258==    at 0x48538BF: free (vg_replace_malloc.c:990)
==57258==    by 0x40011A6: main (duplo.c:10)
==57258==  Block was alloc'd at
==57258==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==57258==    by 0x400117E: main (duplo.c:5)
==57258==
==57258==
==57258== HEAP SUMMARY:
==57258==     in use at exit: 0 bytes in 0 blocks
==57258==   total heap usage: 1 allocs, 2 frees, 20 bytes allocated
«…»

"free() inválido" (o título cita também delete, delete[] e realloc porque o Valgrind serve para C++ também). O segundo free está na linha 11; o primeiro, na 10. E o resumo denuncia: 1 allocs, 2 frees, uma reserva e duas liberações.


4. Variável sem valor inicial: Conditional jump…

Esta é a especialidade do Valgrind, e o que os sanitizers do gcc não pegam. A Ana esqueceu o = 0 da soma:

#include <stdio.h>

int main(void) {
    int notas[] = {7, 8, 10};
    int soma;
    for (int i = 0; i < 3; i++) {
        soma += notas[i];
    }
    if (soma > 20) {
        printf("Passou: %d\n", soma);
    } else {
        printf("Não passou: %d\n", soma);
    }
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -g media.c -o media
$ ./media
Passou: 25

Nem o gcc avisou (o aviso -Wuninitialized do gcc nem sempre aparece sem otimização), e o resultado está certo: a soma começou com 0 por pura sorte, porque aquele pedaço da pilha estava zerado. Em outro programa, ou mais tarde neste mesmo, ela pode começar com qualquer lixo (veja Variáveis). O Valgrind percebe:

$ valgrind ./media
«… o cabeçalho …»
==56748== Conditional jump or move depends on uninitialised value(s)
==56748==    at 0x40011BC: main (media.c:9)
==56748==
==56748== Use of uninitialised value of size 8
==56748==    at 0x48E5062: _itoa_word (_fitoa_word.c:38)
==56748==    by 0x48F061C: __printf_buffer (vfprintf-process-arg.c:155)
==56748==    by 0x48F2767: __vfprintf_internal (vfprintf-internal.c:1548)
==56748==    by 0x48E6132: printf (printf.c:33)
==56748==    by 0x40011D6: main (media.c:10)
==56748==
«… mais 3 erros parecidos, todos dentro do printf da linha 10 …»
Passou: 25
«… HEAP SUMMARY …»
==56748== Use --track-origins=yes to see where uninitialised values come from
==56748== For lists of detected and suppressed errors, rerun with: -s
==56748== ERROR SUMMARY: 7 errors from 5 contexts (suppressed: 0 from 0)
Conditional jump or move depends on uninitialised value(s)
"Um desvio condicional (ou uma cópia) depende de valor(es) não inicializado(s)". Em português de gente: um if (ou laço, ou ?:) tomou uma decisão com base num valor que ninguém definiu. É a linha 9: if (soma > 20).
Use of uninitialised value of size 8
"Uso de valor não inicializado": o printf da linha 10 recebeu a soma para escrever na tela. As linhas de cima (_itoa_word, __printf_buffer) são o código interno do printf transformando o número em texto; a linha que interessa é main (media.c:10).
Use --track-origins=yes…
O Valgrind diz onde o valor ruim foi usado, mas não onde ele nasceu. A opção --track-origins=yes ("rastrear origens") responde isso.

Repare que o Valgrind não reclama da linha 7 (soma += notas[i]), onde o lixo entrou na conta: copiar e somar lixo não é proibido. Ele só reclama quando o lixo decide alguma coisa (um if) ou sai do programa (o printf). Por isso, a primeira linha apontada nem sempre é a do erro: siga a pista com o --track-origins=yes.

De onde veio o lixo: --track-origins=yes

$ valgrind --track-origins=yes ./media
«… o cabeçalho …»
==56768== Conditional jump or move depends on uninitialised value(s)
==56768==    at 0x40011BC: main (media.c:9)
==56768==  Uninitialised value was created by a stack allocation
==56768==    at 0x4001169: main (media.c:3)
==56768==
«… os outros erros, cada um com a mesma origem …»

"O valor não inicializado foi criado por uma reserva na pilha, em main (media.c:3)": uma variável local do main (as locais moram na pilha; a linha 3 é onde o main começa). Agora é só olhar as variáveis locais do main que nunca recebem valor: a soma. Se o lixo viesse de um malloc, a mensagem seria created by a heap allocation, com a linha do malloc. O --track-origins=yes deixa o Valgrind ainda mais lento, por isso não vem ligado.

Missão: o editor usa o Clang, que avisa deste erro com -Wall (o gcc não avisou). Rode, leia o aviso e conserte a variável que o Valgrind apontou. A missão termina quando o programa compilar sem nenhum aviso e mostrar Passou: 25.

#include <stdio.h>

int main(void) {
    int notas[] = {7, 8, 10};
    int soma;
    for (int i = 0; i < 3; i++) {
        soma += notas[i];
    }
    if (soma > 20) {
        printf("Passou: %d\n", soma);
    } else {
        printf("Não passou: %d\n", soma);
    }
    return 0;
}
#include <stdio.h>

int main(void) {
    int notas[] = {7, 8, 10};
    int soma = 0;
    for (int i = 0; i < 3; i++) {
        soma += notas[i];
    }
    if (soma > 20) {
        printf("Passou: %d\n", soma);
    } else {
        printf("Não passou: %d\n", soma);
    }
    return 0;
}

5. Os tipos de vazamento

O LEAK SUMMARY tem cinco linhas. Duas aparecem o tempo todo em programas com listas ligadas e structs com ponteiros. Esta lista tem 3 nós, mas o main só libera o primeiro:

#include <stdio.h>
#include <stdlib.h>

struct no {
    int valor;
    struct no *prox;
};

struct no *novo(int valor, struct no *prox) {
    struct no *n = malloc(sizeof *n);
    if (n == NULL) {
        exit(1);
    }
    n->valor = valor;
    n->prox = prox;
    return n;
}

int main(void) {
    struct no *lista = novo(1, novo(2, novo(3, NULL)));
    for (struct no *p = lista; p != NULL; p = p->prox) {
        printf("%d\n", p->valor);
    }
    free(lista);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra -g lista.c -o lista
$ valgrind --leak-check=full ./lista
«… o cabeçalho …»
1
2
3
==57294==
==57294== HEAP SUMMARY:
==57294==     in use at exit: 32 bytes in 2 blocks
==57294==   total heap usage: 4 allocs, 2 frees, 4,144 bytes allocated
==57294==
==57294== 32 (16 direct, 16 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2
==57294==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==57294==    by 0x40011C5: novo (lista.c:10)
==57294==    by 0x400121D: main (lista.c:20)
==57294==
==57294== LEAK SUMMARY:
==57294==    definitely lost: 16 bytes in 1 blocks
==57294==    indirectly lost: 16 bytes in 1 blocks
==57294==      possibly lost: 0 bytes in 0 blocks
==57294==    still reachable: 0 bytes in 0 blocks
==57294==         suppressed: 0 bytes in 0 blocks
«…»

Cada nó tem 16 bytes (um int, 4 bytes de enchimento e um ponteiro de 8; veja sizeof). O nó 1 foi liberado. O nó 2 ficou sem ninguém apontando para ele: definitely lost. O nó 3 ainda tem um ponteiro apontando para ele, mas esse ponteiro mora dentro do nó 2, que está perdido: indirectly lost. Por isso a linha 32 (16 direct, 16 indirect). Conserte o "direto" e o "indireto" some junto.

Linha do LEAK SUMMARYQuer dizerGravidade
definitely lostNenhum ponteiro aponta para o bloco: perdido de vez.Vazamento. Conserte.
indirectly lostSó é apontado por blocos que estão perdidos (o resto de uma lista ou árvore).Some quando você conserta o definitely lost.
possibly lostSó há ponteiros para o meio do bloco, não para o começo.Quase sempre um vazamento; investigue.
still reachableAinda há um ponteiro para o bloco no fim (numa variável global, por exemplo), mas ninguém deu free.Não é perdido, mas é desleixo: libere antes de sair.
suppressedVazamentos que o Valgrind foi mandado ignorar (geralmente de bibliotecas do sistema).Ignore.

Um still reachable de verdade: um ponteiro global que nunca é liberado.

#include <stdio.h>
#include <stdlib.h>

char *historico;

int main(void) {
    historico = malloc(100);
    if (historico == NULL) {
        return 1;
    }
    printf("Histórico pronto.\n");
    return 0;
}
$ valgrind --leak-check=full ./guarda
«…»
==58023== LEAK SUMMARY:
==58023==    definitely lost: 0 bytes in 0 blocks
==58023==    indirectly lost: 0 bytes in 0 blocks
==58023==      possibly lost: 0 bytes in 0 blocks
==58023==    still reachable: 100 bytes in 1 blocks
==58023==         suppressed: 0 bytes in 0 blocks
==58023== Reachable blocks (those to which a pointer was found) are not shown.
==58023== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==58023==
==58023== For lists of detected and suppressed errors, rerun with: -s
==58023== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

O bloco ainda é alcançável pela global historico, então não conta como erro (0 errors) e nem mostra a linha. Para ver de onde ele veio, o próprio Valgrind ensina: --show-leak-kinds=all ("mostre todos os tipos de vazamento"), que acrescenta 100 bytes in 1 blocks are still reachable in loss record 1 of 1 com by 0x400117A: main (guarda.c:7).

Missão: escreva a função liberar, que libera todos os nós da lista. Cuidado: depois do free(p), não dá mais para ler p->prox (seria um Invalid read). Guarde o próximo numa variável antes do free. Para a missão conferir, mostre liberado N antes de cada free.

#include <stdio.h>
#include <stdlib.h>

struct no {
    int valor;
    struct no *prox;
};

struct no *novo(int valor, struct no *prox) {
    struct no *n = malloc(sizeof *n);
    if (n == NULL) {
        exit(1);
    }
    n->valor = valor;
    n->prox = prox;
    return n;
}

void liberar(struct no *lista) {
    /* libere todos os nós aqui; antes de cada free, mostre "liberado N" */
    printf("liberado %d\n", lista->valor);
    free(lista);
}

int main(void) {
    struct no *lista = novo(1, novo(2, novo(3, NULL)));
    liberar(lista);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

struct no {
    int valor;
    struct no *prox;
};

struct no *novo(int valor, struct no *prox) {
    struct no *n = malloc(sizeof *n);
    if (n == NULL) {
        exit(1);
    }
    n->valor = valor;
    n->prox = prox;
    return n;
}

void liberar(struct no *lista) {
    struct no *p = lista;
    while (p != NULL) {
        struct no *prox = p->prox;
        printf("liberado %d\n", p->valor);
        free(p);
        p = prox;
    }
}

int main(void) {
    struct no *lista = novo(1, novo(2, novo(3, NULL)));
    liberar(lista);
    return 0;
}

6. O que o Valgrind não vê, e quanto ele custa

O Memcheck vigia o heap (os blocos do malloc) byte a byte, mas não sabe onde termina cada array local. O estouro de notas[5] da aula Sanitizers, que o ASan chamou de stack-buffer-overflow, passa sem um Invalid read:

$ valgrind ./notas
«… o cabeçalho …»
==56412== Use of uninitialised value of size 8
==56412==    at 0x48E5062: _itoa_word (_fitoa_word.c:38)
==56412==    by 0x48F061C: __printf_buffer (vfprintf-process-arg.c:155)
==56412==    by 0x48F2767: __vfprintf_internal (vfprintf-internal.c:1548)
==56412==    by 0x48E6132: printf (printf.c:33)
==56412==    by 0x40011E5: main (notas.c:9)
«… mais erros iguais, todos no printf …»
Soma: 40
«…»
==56412== ERROR SUMMARY: 6 errors from 4 contexts (suppressed: 0 from 0)

Ele só percebeu o efeito: o byte lido depois do array nunca tinha recebido valor, e a soma que chegou ao printf da linha 9 ficou "contaminada". A linha do erro de verdade (a 7) nem aparece. Para estouros de arrays locais, o ASan é muito melhor.

E o preço: o programa roda dentro do Valgrind, que confere cada instrução. O mesmo programa de somas da aula de sanitizers (com 20 voltas, em vez de 200):

$ time ./pesado
Soma: 59999940

real	0m0,103s
user	0m0,101s
sys	0m0,002s
$ time valgrind -q ./pesado
Soma: 59999940

real	0m2,134s
user	0m2,095s
sys	0m0,039s

Uns 20 vezes mais lento (o -q, quiet, esconde tudo do Valgrind menos os erros). Em geral, conte com 10 a 50 vezes. Para os programas pequenos das aulas, não faz diferença; para um jogo ou um servidor, faz.


Valgrind × sanitizers: qual usar

Valgrind (Memcheck)ASan + UBSan (-fsanitize=address,undefined)
Precisa recompilar?Não (mas -g ajuda muito).Sim, com a opção.
Velocidade10 a 50 vezes mais lento.Umas 2 vezes mais lento.
Estouro no heap, uso depois do free, free duplo, vazamentosSim.Sim.
Estouro de array local (pilha) ou globalNão (no máximo vê o efeito).Sim.
Variável sem valor inicialSim (Conditional jump…).Não (o Clang tem o -fsanitize=memory para isso).
Estouro de int, deslocamento grandeNão.Sim (UBSan).
No primeiro erroAnota e segue.ASan para; UBSan anota e segue.
SistemasLinux (x86, ARM…). Não roda nos macOS recentes nem no Windows.Linux, macOS; no Windows, com o Clang ou o Visual Studio.

Qual usar: no dia a dia, compile os testes com -g -fsanitize=address,undefined, que é rápido e pega mais tipos de erro. Use o Valgrind quando: (1) um valor muda de execução para execução e você suspeita de variável sem valor inicial; (2) você não pode recompilar o programa; (3) quer uma segunda opinião. Nunca os dois juntos: um programa compilado com -fsanitize=address não roda direito no Valgrind (veja o cenário abaixo).

No macOS e no Windows

O Valgrind oficial não roda nas versões recentes do macOS (nem nos Macs com processador Apple) nem no Windows. Alternativas:

  • Qualquer sistema: os sanitizers (-fsanitize=address) do Clang e do gcc.
  • macOS: o comando leaks (procura vazamentos num programa) e o Instruments, do Xcode.
  • Windows: o Visual Studio tem /fsanitize=address; o Dr. Memory é uma ferramenta gratuita parecida com o Valgrind; e no WSL (o Linux dentro do Windows; veja Instalar no Windows e macOS) o Valgrind funciona normalmente.

Quando dá errado

O relatório mostra main (in /home/ana/c/vg/vaza-semg) em vez de main (vaza.c:14)
$ gcc -std=c23 -Wall -Wextra vaza.c -o vaza-semg
$ valgrind --leak-check=full ./vaza-semg
«…»
==64335== 6 bytes in 1 blocks are definitely lost in loss record 1 of 1
==64335==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==64335==    by 0x40011D0: copiar (in /home/ana/c/vg/vaza-semg)
==64335==    by 0x400120F: main (in /home/ana/c/vg/vaza-semg)
«…»

O que aconteceu: o programa foi compilado sem -g; o Valgrind sabe o nome da função, mas não o arquivo nem a linha ("in executável").

Como resolver: recompile com -g (e, de preferência, sem -O2).

ASan runtime does not come first in initial library list
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address notas.c -o notas
$ valgrind ./notas
«…»
==46557==ASan runtime does not come first in initial library list; you should either link runtime to your application or manually preload it with LD_PRELOAD.
==46557==
==46557== HEAP SUMMARY:
==46557==     in use at exit: 0 bytes in 0 blocks
==46557==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
«…»
==46557== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

O que aconteceu: o programa foi compilado com o AddressSanitizer, e os dois fiscais brigam pela memória. O programa nem rodou (0 allocs), e o 0 errors é enganoso.

Como resolver: para usar o Valgrind, compile sem -fsanitize.

Erros dentro de printf, _itoa_word, strlen…

O que aconteceu: a biblioteca recebeu do seu código um valor sem inicializar ou um ponteiro ruim.

Como resolver: desça a pilha até a primeira linha by …: função (seu-arquivo.c:N). Para valores sem inicializar, rode com --track-origins=yes.

"valgrind: comando não encontrado"

O que aconteceu: o Valgrind não está instalado (ou você está no macOS ou no Windows, onde ele não existe).

Como resolver: no Ubuntu e no Debian, sudo apt install valgrind. Nos outros sistemas, veja as alternativas acima.

O programa pede entrada e o Valgrind "trava"

O que aconteceu: nada travou: o seu programa está esperando você digitar, como sempre. A entrada e os argumentos passam normalmente: valgrind ./prog arg1 < dados.txt.

Como resolver: digite a entrada (ou use < para ler de um arquivo). Lembre que tudo fica mais lento.


Referência rápida

Comando ou mensagemO que é
valgrind ./progRoda o programa no Memcheck.
--leak-check=fullMostra onde cada bloco vazado foi reservado.
--show-leak-kinds=allMostra também os still reachable.
--track-origins=yesDiz onde nasceu o valor não inicializado.
--error-exitcode=1Se houver erro, o código de saída vira 1 (bom para testes automáticos).
-qMostra só os erros.
Invalid read of size N / Invalid write of size NLeitura / escrita de N bytes em memória proibida.
Address … is 0 bytes after a block of size N alloc'dPassou do fim de um bloco do malloc.
Address … is … inside a block of size N free'dUsou memória já liberada.
Invalid free()free duplo ou de um ponteiro que não veio do malloc.
Conditional jump or move depends on uninitialised value(s)Um if/laço decidiu com um valor nunca definido.
Use of uninitialised value of size NUm valor nunca definido foi usado (por exemplo, impresso).
definitely lost / indirectly lostVazamento / vazamento por tabela.
still reachableNão liberado, mas ainda apontado no fim.
All heap blocks were freed -- no leaks are possibleNenhum vazamento. É o que você quer ver.

Um exemplo de --error-exitcode, útil num Makefile ou num teste:

$ valgrind -q --leak-check=full --error-exitcode=1 ./vaza
Item: arroz
==57335== 6 bytes in 1 blocks are definitely lost in loss record 1 of 1
==57335==    at 0x4850858: malloc (vg_replace_malloc.c:447)
==57335==    by 0x40011D0: copiar (vaza.c:6)
==57335==    by 0x400120F: main (vaza.c:14)
==57335==
$ echo $?
1

Exercícios

1. O comando de sempre

Complete para ver onde cada bloco vazado foi reservado:

valgrind ./prog

2. De onde veio o lixo?

Apareceu Conditional jump or move depends on uninitialised value(s). Complete para o Valgrind dizer onde o valor nasceu:

valgrind ./prog

3. Sem linhas

O relatório mostra main (in /home/ana/c/vg/prog), sem número de linha. Qual opção faltou no gcc?

4. O LEAK SUMMARY mostra definitely lost: 16 bytes e indirectly lost: 32 bytes. O que fazer primeiro?

5. Qual erro o Valgrind encontra e o -fsanitize=address não?

6. Address 0x4aa6049 is 0 bytes after a block of size 9 alloc'd. O que provavelmente aconteceu?


Resumo

  • Compile com -g e rode valgrind --leak-check=full ./programa. Sem recompilar com opções especiais; as linhas com ==PID== são do Valgrind.
  • Invalid read/Invalid write: acesso a memória proibida; as linhas seguintes dizem onde o bloco foi reservado (alloc'd) ou liberado (free'd). Invalid free(): free duplo.
  • Conditional jump or move depends on uninitialised value(s): uma decisão com uma variável sem valor; --track-origins=yes mostra onde ela nasceu.
  • LEAK SUMMARY: conserte os definitely lost (os indirectly lost vão junto); still reachable é memória não liberada mas ainda apontada. A meta é All heap blocks were freed -- no leaks are possible.
  • Valgrind: lento, sem recompilar, pega variáveis sem valor inicial. Sanitizers: rápidos, pegam estouros na pilha e contas inválidas. Nunca os dois juntos.
  • No macOS e no Windows, use os sanitizers, leaks/Instruments, Dr. Memory ou o WSL.