Usando o Valgrind

Contribuíram neste tutorial Giovanni Bertão, Guilherme Tiaki Sassai Sato

O Valgrind é um conjunto de ferramentas para depuração, incluindo o memcheck, um detector de erros de memória, que poderá ser útil para descobrir causas de erros em seus programas.

Instalação

O Valgrind está disponível para GNU/Linux e Mac, mas a versão para linux é mais estável. Portanto, se possível, dê preferência para a versão para linux. Infelizmente, ele não está disponível para Windows, mas pode ser utilizado através de uma máquina virtual ou do Windows Subsystem for Linux (WSL).

Linux

No Ubuntu ou Debian, a instalação é feita facilmente pelo comando:

user@desktop:~$ sudo apt update && sudo apt install valgrind

Mac

A instalação no Mac requer o uso do gerenciador de pacotes Homebrew. Se ainda não o possuir, a instalação pode ser feita pelo comando:

user@desktop:~$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

A versão atual do Valgrind disponível por padrão no Homebrew não é compatível com as versões mais recentes do MacOS. Porém, podemos utilizar uma versão mantida por Louis Brunner que é compatível. Para instalá-la:

user@desktop:~$ brew tap LouisBrunner/valgrind
user@desktop:~$ brew install --HEAD LouisBrunner/valgrind/valgrind

Uso

Para utilizar o Valgrind, simplesmente utilize o comando valgrind. Vamos testá-lo utilizando um pequeno exemplo em C:

#include <stdlib.h>

int main() {
    int *x = malloc(10 * sizeof(int));
    x[10] = 0;
    return 0;
}

O código acima faz a alocação dinâmica de um vetor, acessa uma posição inválida e termina sem liberar a memória alocada. Para testar o programa com o Valgrind, devemos compilá-lo normalmente, porém incluindo a opção -g, que permitirá que o Valgrind exiba a linha do código onde ocorreu o erro.

user@desktop:~$ gcc example.c -std=c99 -Wall -Werror=vla -g -o example

Executamos o Valgrind fornecendo o nome do binário como argumento:

user@desktop:~$ valgrind --leak-check=full ./example

A saída produzida provavelmente será como essa:

==17407== Memcheck, a memory error detector
==17407== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==17407== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==17407== Command: ./example
==17407==
==17407== Invalid write of size 4
==17407==    at 0x10916B: main (example.c:5)
==17407==  Address 0x4a46068 is 0 bytes after a block of size 40 alloc'd
==17407==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==17407==    by 0x10915E: main (example.c:4)
==17407==
==17407==
==17407== HEAP SUMMARY:
==17407==     in use at exit: 40 bytes in 1 blocks
==17407==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==17407==
==17407== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==17407==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==17407==    by 0x10915E: main (example.c:4)
==17407==
==17407== LEAK SUMMARY:
==17407==    definitely lost: 40 bytes in 1 blocks
==17407==    indirectly lost: 0 bytes in 0 blocks
==17407==      possibly lost: 0 bytes in 0 blocks
==17407==    still reachable: 0 bytes in 0 blocks
==17407==         suppressed: 0 bytes in 0 blocks
==17407==
==17407== For lists of detected and suppressed errors, rerun with: -s
==17407== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

Podemos visualizar os dois erros contidos no programa: Invalid write of size 4 na linha 5 e 40 bytes in 1 blocks are definitely lost in loss record 1 of 1 na linha 4.

Vamos analisar mais um exemplo:

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

#define N 10

int** mul(int** A, int** B){
	int** C = malloc(N * sizeof(int*));
	for (int i = 0; i < N; i++)
		C[i] = calloc(N, sizeof(int));

	for (int i = 0; i < N; ++i){
		for (int j = 0; j < N; ++j){
			for (int k = 0; k < N; k++){
				C[i][j] += A[i][k] * B[k][j];
			}
		}
	}
	return C;
}

void eye(int** matriz){
	for (int i = 0; i < N; i++){
		for (int j = 0; j < N; j++){
			if (i == j)
				matriz[i][j] = 1;
			else
				matriz[i][j] = 0;
		}
	}
}

int main(){
	srand(time(NULL));
	
	int** A = malloc(N * sizeof(int*));
	int** B = malloc(N * sizeof(int*));
	for (int i = 0; i < N; i++){
		A[i] = malloc(N * sizeof(int));
		B[i] = malloc(N * sizeof(int));
		for (int j = 0; j < N; j++){
			A[i][j] = rand() % 10;
			B[i][j] = rand() % 10;
		}
	}

	int** C = mul(A,B);	

	for (int i = 0; i < N; i++){
		for (int j = 0; j < N; j++){
			printf("%3d ", C[i][j]);		
		}
		printf("\n");
	}

	free(A);
	free(B);
	free(C);

	return 0;
}

Esse programa faz a alocação de duas matrizes aleatórias e calcula o produto delas, aparentemente liberando todas as matrizes ao final. Usando o Valgrind, podemos encontrar alguns erros:

==235== Memcheck, a memory error detector
==235== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==235== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==235== Command: ./mul
==235==
164 230 149 204 143 221 206 161 187 181
234 193 172 240 204 254 250 162 199 214
211 149 155 148 118 208 206 128 161 114
165 181 157 196 155 212 235 182 151 143
261 237 254 283 154 266 255 218 214 174
303 208 208 226 158 283 237 183 222 174
125 204 108 188  93 231 167 170 206 178
222 219 167 183 126 240 198 194 190 192
273 309 248 273 223 293 310 238 231 260
184 178 142 161 122 198 207 141 124 149
==235==
==235== HEAP SUMMARY:
==235==     in use at exit: 1,200 bytes in 30 blocks
==235==   total heap usage: 34 allocs, 4 frees, 2,464 bytes allocated
==235==
==235== 400 bytes in 10 blocks are definitely lost in loss record 1 of 3
==235==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==235==    by 0x109474: main (mul.c:39)
==235==
==235== 400 bytes in 10 blocks are definitely lost in loss record 2 of 3
==235==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==235==    by 0x109496: main (mul.c:40)
==235==
==235== 400 bytes in 10 blocks are definitely lost in loss record 3 of 3
==235==    at 0x483DD99: calloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==235==    by 0x109278: mul (mul.c:10)
==235==    by 0x10957E: main (mul.c:47)
==235==
==235== LEAK SUMMARY:
==235==    definitely lost: 1,200 bytes in 30 blocks
==235==    indirectly lost: 0 bytes in 0 blocks
==235==      possibly lost: 0 bytes in 0 blocks
==235==    still reachable: 0 bytes in 0 blocks
==235==         suppressed: 0 bytes in 0 blocks
==235==
==235== For lists of detected and suppressed errors, rerun with: -s
==235== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)

As chamadas de free realizadas liberam apenas o vetor de ponteiros, e o Valgrind nos mostra exatamente quais alocações não foram liberadas: mul.c:39, mul.c:40 e mul.c:10. Se ao final do programa a liberação fosse feita conforme o bloco abaixo, toda a memória seria liberada.

for (int i = 0; i < N; ++i){
	free(A[i]);
}
free(A);
for (int i = 0; i < N; ++i){
	free(B[i]);
}
free(B);
for (int i = 0; i < N; ++i){
	free(C[i]);
}
free(C);

Além de detectar acessos inválidos e vazamento de memória, o Valgrind também pode detectar o uso de variáveis não inicializadas. O programa a seguir implementa a função strlen, que calcula o tamanho de uma string.

#include <stdio.h>

unsigned int strlen(char* string){
	unsigned int i;
	while(string[i])
		i++;

	return i;
}

int main(){
	char* mensagem = "Hello\n";

	printf("Comprimento da mensagem: %u caracteres\n", strlen(mensagem));

	return 0;
}

Executando o programa dentro do Valgrind:

==229== Memcheck, a memory error detector
==229== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==229== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==229== Command: ./uninitialised
==229==
==229== Use of uninitialised value of size 8
==229==    at 0x109165: strlen (uninitialised.c:5)
==229==    by 0x109193: main (uninitialised.c:14)
==229==
==229== Use of uninitialised value of size 8
==229==    at 0x48AC81B: _itoa_word (_itoa.c:179)
==229==    by 0x48C86F4: __vfprintf_internal (vfprintf-internal.c:1687)
==229==    by 0x48B2EBE: printf (printf.c:33)
==229==    by 0x1091A6: main (uninitialised.c:14)
==229==
==229== Conditional jump or move depends on uninitialised value(s)
==229==    at 0x48AC82D: _itoa_word (_itoa.c:179)
==229==    by 0x48C86F4: __vfprintf_internal (vfprintf-internal.c:1687)
==229==    by 0x48B2EBE: printf (printf.c:33)
==229==    by 0x1091A6: main (uninitialised.c:14)
==229==
==229== Conditional jump or move depends on uninitialised value(s)
==229==    at 0x48C93A8: __vfprintf_internal (vfprintf-internal.c:1687)
==229==    by 0x48B2EBE: printf (printf.c:33)
==229==    by 0x1091A6: main (uninitialised.c:14)
==229==
==229== Conditional jump or move depends on uninitialised value(s)
==229==    at 0x48C886E: __vfprintf_internal (vfprintf-internal.c:1687)
==229==    by 0x48B2EBE: printf (printf.c:33)
==229==    by 0x1091A6: main (uninitialised.c:14)
==229==
Comprimento da mensagem: 6 caracteres
==229==
==229== HEAP SUMMARY:
==229==     in use at exit: 0 bytes in 0 blocks
==229==   total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==229==
==229== All heap blocks were freed -- no leaks are possible
==229==
==229== Use --track-origins=yes to see where uninitialised values come from
==229== For lists of detected and suppressed errors, rerun with: -s
==229== ERROR SUMMARY: 11 errors from 5 contexts (suppressed: 0 from 0)

O Valgrid reporta o erro Use of uninitialised value of size 8 na linha 5, dentro da função strlen, que é onde há o laço while que utilizado a variável i não inicializada.

Os outros erros reportados ocorrem dentro da chamada à função printf, como consequencia da não inicialização e são solucionados caso o primeiro erro seja corrigido.