Valgrind

Valgrind tem esse nome estranho mas é a ferramenta que todo o programador deveria ter no seu cinto de utilidades do Batman! O nome provêm da mitologia nórdica e significa a entrada de Valhalla – wikipédia tem mais explicações. Agora, por que todo programador deve ter essa ferramenta no bat-cinto? A resposta é simples: ela instrumenta o aplicativo em tempo de execução e identifica leaks de memória (memória que foi alocada mas nunca liberada), uso inválidos de ponteiros, variáveis não inicializadas, múltiplos frees na mesma váriavel, etc. Se isso não bastasse, há ainda outros backends que identificam chamadas de função, uso de cache, memória alocada, etc, e obviamente você pode escrever o seu próprio backend!

O foco deste post é mostrar o uso do backend memcheck para solucionar problemas de memória. Necessitamos, para isso, de um código com alguns bugs. Sabemos que um bom programador, lendo este código, identificará todos os problemas que existem nele, mas muitas vezes estes mesmo problemas surgem de forma escondida no código, o que torna necessário o uso de uma ferramenta para ajudar a identificá-los.

O código a seguir exercita a maior parte das funcionalidades do backend memcheck – o padrão do valgrind. Compile o código executando a seguinte linha, observe que o gcc é chamado com a flag -Wall que habilita diversos warnings durante a compilação, apesar disso, o código deve compilar sem nenhum problema.

gcc -Wall -O1 -g -o valtest valtest.c
#include <stdio.h>
#include <stdlib.h>

struct uninit {
	char *x;
	char *y;
};

void heap(void)
{
	char *x;
	x = malloc(5 * sizeof(char));
	x[5] = 'a';
	free(x);
}

void leak(void)
{
	char *x;
	x = malloc(5 * sizeof(char));
}

void uninitialized(void)
{
	struct uninit *u;
	u = malloc(sizeof(struct uninit));
	if (u->y == 0) {
		printf("y is 0\n");
	}
	free(u);
}

char * double_free(void)
{
	char *x;
	x = malloc(5 * sizeof(char));
	free(x);
	return x;
}

int main(int argc, char *argv[])
{
	char *x;
	
	printf("Calling heap()\n");
	heap();
	
	printf("Calling leak()\n");
	leak();
	
	printf("Calling uninitialized()\n");
	uninitialized();
	
	printf("Calling double_free()\n");
	x = double_free();
	free (x);
	
	return 0;
}

Se executarmos este código, ainda sem utilizar o valgrind, obteremos um erro por causa do duplo free. A saída da execução será algo parecido com as linhas abaixo, talvez inclua um backtrace ou mais alguma informação, mas isso depende da libc utilizada. Repare que a mensagem de erro não indica onde ocorreu o problema no código.

pedro@urubu:~/code$ ./valtest
Calling heap()
Calling leak()
Calling uninitialized()
y is 0
Calling double_free()
*** glibc detected *** ./valtest: double free or corruption (fasttop): 0x08b09028 ***

Let the Bug Hunting begin!

Sabemos que existe um problema com o código, temos uma vaga idéia de onde ele ocorre – depois da função double_free(), mas há mais problemas no código e nenhum deles foi reportado durante a execução. Hora de usar o valgrind! Executando a seguinte linha, obteremos um relatório dos erros encontrados pelo valgrind. Abreviei a saída gerada pelo valgrind para mostrar justamente o relatório de erros encontrados.

pedro@urubu:~/code$ valgrind ./valtest
==13586== Memcheck, a memory error detector
==13586== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==13586== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
==13586== Command: ./valtest
==13586==
...
==13586== HEAP SUMMARY:
==13586==     in use at exit: 5 bytes in 1 blocks
==13586==   total heap usage: 4 allocs, 4 frees, 23 bytes allocated
==13586==
==13586== LEAK SUMMARY:
==13586==    definitely lost: 5 bytes in 1 blocks
==13586==    indirectly lost: 0 bytes in 0 blocks
==13586==      possibly lost: 0 bytes in 0 blocks
==13586==    still reachable: 0 bytes in 0 blocks
==13586==         suppressed: 0 bytes in 0 blocks
==13586== Rerun with --leak-check=full to see details of leaked memory
==13586==
==13586== For counts of detected and suppressed errors, rerun with: -v
==13586== Use --track-origins=yes to see where uninitialised values come from
==13586== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 13 from 8 )

A ferramenta encontrou 3 erros em 3 contextos diferentes e 5 bytes de memória alocada. É claro que existe alguns bugs neste código! No texto de saída há ainda a sugestão do uso das flags --leak-check=full e -v. Vamos rodar novamente o valgrind com estes dois parâmetros antes de chamar o valtest. O número de mensagens aumenta bastante, mas dá novos indicativos do que está acontecendo de errado no código.

Heap overrun

O primeiro erro apresentado pela ferramenta é no métodoheap(), meus grifos:

Calling heap()
--13590-- REDIR: 0x40a7f40 (malloc) redirected to 0x4024e9b (malloc)
==13590== Invalid write of size 1
==13590==    at 0x80484FC: heap (valtest.c:13)
==13590==    by 0x804852B: main (valtest.c:46)
==13590==  Address 0x419402d is 0 bytes after a block of size 5 alloc'd
==13590==    at 0x4024F20: malloc (vg_replace_malloc.c:236)
==13590==    by 0x80484FB: heap (valtest.c:12)
==13590==    by 0x804852B: main (valtest.c:46)
==13590==

Isto indica que tentou-se escrever 1 byte após os 5 bytes alocados, se olharmos o código isto fica bastante claro! Alocamos 5 bytes para a variável x, isto em um array significa os índices entre 0 e 4. Na linha 13 do código tentamos escrever na posição 5, que é uma área de memória além do espaço alocado. A saída da ferramenta ainda nos indica em que lugar no código a memória que está sendo utilizada foi alocada, no caso na linha 12.

Com isso podemos arrumar o primeiro bug do código, acertando o índice utilizado na linha 13 para ser um valor entre 0 e 4. One down, three to go!

void heap(void)
{
	char *x;
	x = malloc(5 * sizeof(char));
	x[0] = 'a';
	free(x);
}

Uninitialized value

O próximo erro apresentado é do uso de uma variável não inicializada e é apresentado pelo valgrind da seguinte forma. Repare que a função leak() foi executada e não apresentou nenhum erro, mas como veremos a ferramenta identificou o erro que acontece na função.

Calling leak()
Calling uninitialized()
==13590== Conditional jump or move depends on uninitialised value(s)
==13590==    at 0x80484B2: uninitialized (valtest.c:27)
==13590==    by 0x804855D: main (valtest.c:52)
==13590==
y is 0

Olhando a linha 27, conforme indicado pelo valgrind, não parece ter nada de errado com o teste realizado ali. Se olharmos as linhas anteriores fica claro que a estrutura u foi alocada, mas que seus valores nunca foram inicializados e dependendo da implementação do malloc os valores dos membros de uma estrutura não serão nulos. Alterando o código da função uninitialized para que os valores de x e y sejam inicializados resolve este problema.

void uninitialized(void)
{
	struct uninit *u;
	u = malloc(sizeof(struct uninit));
	u->x = NULL;
	u->y = NULL;

	if (u->y == 0) {
		printf("y is 0\n");
	}
	free(u);
}

Double free

O próximo problema já foi apresentado pela própria libc, mas não tinhamos um indicativo preciso do local no código onde ocorreu o erro. O valgrind nos fornece este dado!

Calling double_free()
==13590== Invalid free() / delete / delete[]
==13590==    at 0x4024B3A: free (vg_replace_malloc.c:366)
==13590==    by 0x804857E: main (valtest.c:56)
==13590==  Address 0x41940d0 is 0 bytes inside a block of size 5 free'd
==13590==    at 0x4024B3A: free (vg_replace_malloc.c:366)
==13590==    by 0x8048490: double_free (valtest.c:37)
==13590==    by 0x8048576: main (valtest.c:55)
==13590==

A ferramenta indica onde ocorreu o segundo free, linha 56, como também onde está o primeiro, linha 37! Olhando o código percebemos que o free da linha 37 é desnecessário, pois queremos retornar um ponteiro para a memória alocada pela função. Removendo esta linha, a função double_free fica:

char * double_free(void)
{
	char *x;
	x = malloc(5 * sizeof(char));
	return x;
}

Leak

O último problema apresentado pelo código é uma área de memória alocada e nunca liberada. Durante a execução do programa, este problema não é manifestado, somente quando finalizamos o programa é que a ferramenta retorna uma relatório identificando que áreas de memória foram usadas e nunca liberadas.

==13590== HEAP SUMMARY:
==13590==     in use at exit: 5 bytes in 1 blocks
==13590==   total heap usage: 4 allocs, 4 frees, 23 bytes allocated
==13590==
==13590== Searching for pointers to 1 not-freed blocks
==13590== Checked 56,484 bytes
==13590==
==13590== 5 bytes in 1 blocks are definitely lost in loss record 1 of 1
==13590==    at 0x4024F20: malloc (vg_replace_malloc.c:236)
==13590==    by 0x80484E7: leak (valtest.c:20)
==13590==    by 0x8048544: main (valtest.c:49)

Analisando a linha 20, vemos que uma área de memória é alocada e o método acaba logo em seguida, sem retornar o ponteiro alocado (para que outra função utilize e libere a memória, por exemplo) ou liberar a memória. Portando estamos vazando essa memória! Se essa função for chamadas diversas vezes durante a execução do programa ocuparemos muita mais memória do que necessitamos, e dependendo do sistema podemos esgotar a memória disponível (pense em um sistema embarcado com 8M de ram, ou menos). É necessário liberar a memória, então:

void leak(void)
{
	char *x;
	x = malloc(5 * sizeof(char));
	free(x);
}

Conclusão

Tendo corrigido os 4 problemas que existiam no código, podemos rodar novamente o valgrind e desta vez não obteremos nenhum erro!

pedro@urubu:~/code$ valgrind --leak-check=full ./valtest-fix
==13845== Memcheck, a memory error detector
==13845== Copyright (C) 2002-2009, and GNU GPL'd, by Julian Seward et al.
==13845== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info
==13845== Command: ./valtest-fix
==13845== 
Calling heap()
Calling leak()
Calling uninitialized()
y is 0
Calling double_free()
==13845== 
==13845== HEAP SUMMARY:
==13845==     in use at exit: 0 bytes in 0 blocks
==13845==   total heap usage: 4 allocs, 4 frees, 23 bytes allocated
==13845== 
==13845== All heap blocks were freed -- no leaks are possible
==13845== 
==13845== For counts of detected and suppressed errors, rerun with: -v
==13845== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 13 from 8 )

Com isso, vimos os principais tipos de erro de memória que o valgrind identifica. Em um código não fictício é mais complexo encontrar o local exato onde ocorrem e por que ocorrem os erros, mas o valgrind dá boas indicações. Costumo utilizar o valgrind sempre que acabo de escrever um pedaço de código, que compila sem warnings, mas que não tenho certeza se desaloquei a memória no lugar certo. É boa prática rodar sempre o valgrind para identificar os mais diversos bugs que existem no seu código.

Happy bug hunting!

One thought on “Valgrind

Leave a Reply

Your email address will not be published. Required fields are marked *