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.
- A rodar
valgrind --leak-check=full ./programae por que compilar com-g. - A ler o
HEAP SUMMARYe oLEAK SUMMARY: definitely lost, indirectly lost, still reachable. - A ler
Invalid write,Invalid readeInvalid 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: oItem: arroznão tem o prefixo, porque foi o seuprintf. - 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óprioprintf, 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=fullpara 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
mallocaparece num arquivo do Valgrind porque ele troca omallocda biblioteca pelo seu, para anotar tudo. - by 0x40011D0: copiar (vaza.c:6)
- "Chamado por
copiar, linha 6 devaza.c": é omalloc(strlen(texto) + 1). - by 0x400120F: main (vaza.c:14)
- "…que foi chamada pelo
main, linha 14". O conserto: darfree(item)nomainquando 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, omalloc(strlen(texto)). - Invalid read of size 1 … puts … main (nome.c:12)
- "Leitura inválida": na linha 12, o
printf(que o gcc trocou peloputs, 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
freeestá 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
printfda linha 10 recebeu asomapara escrever na tela. As linhas de cima (_itoa_word,__printf_buffer) são o código interno doprintftransformando 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 SUMMARY | Quer dizer | Gravidade |
|---|---|---|
definitely lost | Nenhum ponteiro aponta para o bloco: perdido de vez. | Vazamento. Conserte. |
indirectly lost | Só é apontado por blocos que estão perdidos (o resto de uma lista ou árvore). | Some quando você conserta o definitely lost. |
possibly lost | Só há ponteiros para o meio do bloco, não para o começo. | Quase sempre um vazamento; investigue. |
still reachable | Ainda 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. |
suppressed | Vazamentos 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. |
| Velocidade | 10 a 50 vezes mais lento. | Umas 2 vezes mais lento. |
Estouro no heap, uso depois do free, free duplo, vazamentos | Sim. | Sim. |
| Estouro de array local (pilha) ou global | Não (no máximo vê o efeito). | Sim. |
| Variável sem valor inicial | Sim (Conditional jump…). | Não (o Clang tem o -fsanitize=memory para isso). |
Estouro de int, deslocamento grande | Não. | Sim (UBSan). |
| No primeiro erro | Anota e segue. | ASan para; UBSan anota e segue. |
| Sistemas | Linux (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
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.
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.
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 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 mensagem | O que é |
|---|---|
valgrind ./prog | Roda o programa no Memcheck. |
--leak-check=full | Mostra onde cada bloco vazado foi reservado. |
--show-leak-kinds=all | Mostra também os still reachable. |
--track-origins=yes | Diz onde nasceu o valor não inicializado. |
--error-exitcode=1 | Se houver erro, o código de saída vira 1 (bom para testes automáticos). |
-q | Mostra só os erros. |
Invalid read of size N / Invalid write of size N | Leitura / escrita de N bytes em memória proibida. |
Address … is 0 bytes after a block of size N alloc'd | Passou do fim de um bloco do malloc. |
Address … is … inside a block of size N free'd | Usou 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 N | Um valor nunca definido foi usado (por exemplo, impresso). |
definitely lost / indirectly lost | Vazamento / vazamento por tabela. |
still reachable | Não liberado, mas ainda apontado no fim. |
All heap blocks were freed -- no leaks are possible | Nenhum 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:
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:
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
-ge rodevalgrind --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():freeduplo.Conditional jump or move depends on uninitialised value(s): uma decisão com uma variável sem valor;--track-origins=yesmostra 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.