Valgrind & Embedded Linux

Como continuação ao último post, resolvi testar o comportamento do valgrind em um sistema embarcado. Primeiro passo obviamente é ter um ambiente de desenvolvimento apropriado, isto é, um toolchain para cross-compilar os pacotes necessários, a glibc compilada para a plataforma em questão, etc. A maioria dos fabricantes de processadores para arquiteturas embarcadas fornecem um pacote contendo toda a toolchain necessária para compilar para a arquitetura em questão. Para escrever este blog eu utilizei o toolchain de powerpc fornecido pela Freescale.

Começei por instalar o pacote em um local conhecido digamos $HOME/toolchains/powerpc-linux-gnu, ajustei minha variável $PATH de modo que fosse possível encontrar os compiladores e ferramentas da toolchain – algo como export PATH=$PATH:$HOME/toolchains/powerpc-linux-gnu/bin, isto irá mudar dependendo do toolchain usado e onde ele foi instalado.

Com as ferramentas instaladas, hora de conseguir o source do valgrind! Primeiro baixei o tarball (versão 3.5) do site do valgrind, mas logo descobri que havia um problema com umas macros usadas que não permitiam cross-compilar o pacote – vide bug, que já está corrigido, mas não liberaram um pacote novo. Bom, hora de baixar o código fonte do SVN! Vamos colocar o código dentro do diretório pkgs do projeto my-project.

cd $HOME/my-project/pkgs/
svn co svn://svn.valgrind.org/valgrind/trunk valgrind

Feito isso, hora de compilar o valgrind! Antes um aviso para os navegantes: o valgrind coloca o caminho $PREFIX/bin hardcoded no binário gerado, e utiliza esse caminho para achar as diversas ferramentas geradas (callgrind, memcheck, etc). Obviamente eu não percebi isso na primeira vez que compilei, mas depois de umas tentativas consegui acertar os valores corretos. A melhor solução é deixar o $PREFIX apontando para /usr como em qualquer instalação normal, compilar normalmente o código e na hora de instalar usar a variável DESTDIR para apontar o diretório correto para realizar a instalação. No meu caso isso é necessário, pois quero instalar num diretório que será montado por NFS no dispositivo embarcado ($HOME/my-project/rootfs). O -j4 é por que eu sou impaciente e tenho cores demais na máquina.

cd valgrind
./configure --host=powerpc-linux-gnu --prefix=/usr --disable-tls
make -j4
make install DESTDIR=$HOME/my-project/rootfs

Com isso temos o valgrind instalado no diretório raíz utilizado pelo sistema embarcado que queremos testar! Hora de bootar o sistema, esperar ele carregar e fire it away! Abaixo um exemplo do valgrind rodando em um sistema embarcado.

-sh-4.0# valgrind test
==1653== Memcheck, a memory error detector
==1653== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al.
==1653== Using Valgrind-3.6.0.SVN and LibVEX; rerun with -h for copyright info
==1653== Command: test
==1653==
Prompt>exit
==1653== Invalid read of size 1
==1653==    at 0x10007F88: test_execute (in /bin/test)
==1653==    by 0x10009A23: main (in /bin/test)
==1653==  Address 0x403e0fd is 0 bytes after a block of size 5 alloc'd
==1653==    at 0xFFBA4B0: malloc (vg_replace_malloc.c:236)
==1653==    by 0xFF5FA2F: xmalloc (in /lib/libreadline.so.6.0)
==1653==    by 0xFF419CB: readline_internal_teardown (in /lib/libreadline.so.6.0)
==1653==    by 0xFF41D1B: readline (in /lib/libreadline.so.6.0)
==1653==    by 0x100098AB: main (in /bin/test)
==1653==
==1653==
==1653== HEAP SUMMARY:
==1653==     in use at exit: 24,589 bytes in 145 blocks
==1653==   total heap usage: 229 allocs, 84 frees, 53,399 bytes allocated
==1653==
==1653== LEAK SUMMARY:
==1653==    definitely lost: 0 bytes in 0 blocks
==1653==    indirectly lost: 0 bytes in 0 blocks
==1653==      possibly lost: 0 bytes in 0 blocks
==1653==    still reachable: 24,589 bytes in 145 blocks
==1653==         suppressed: 0 bytes in 0 blocks
==1653== Rerun with --leak-check=full to see details of leaked memory
==1653==
==1653== For counts of detected and suppressed errors, rerun with: -v
==1653== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 5 from 3)

Só para completar, o valgrind ajuda bastante a achar os problemas no software, mas lembre-se que um sistema embarcado possuiu outras limitações, como pouca memória, processador com clock baixo, etc. Essas limitações se refletem no desempenho do valgrind, o software ficará bastante lento, algumas flags do valgrind não funcionarão – no meu caso foi por falta de memória, precisaria de mais do que os 64M de RAM disponíveis para utilizar o --track-origins (Ah, o manual do valgrind informa que é necessário no mínimo 100M de RAM para este parâmetro funcionar!).

Happy Hacking!

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!