Valgrind
Valgrind est une suite d’outils d’analyse dynamique qui exécute un programme dans un environnement instrumenté afin d’observer son comportement mémoire et, selon l’outil utilisé, son activité CPU ou ses interactions concurrentes.
Dans ce dossier, c’est principalement Memcheck qui nous intéresse. C’est l’outil par défaut de Valgrind, et il permet de détecter des lectures et écritures invalides, des usages de mémoire non initialisée, des libérations incorrectes ainsi que des fuites mémoire.
L’intérêt principal de Valgrind est qu’il ne nécessite pas de recompiler le binaire avec une instrumentation particulière. En contrepartie, l’exécution est beaucoup plus lente qu’en natif, ce qui en fait un excellent outil de diagnostic, mais rarement un outil à utiliser en production.
Leak
Dans sa configuration par défaut, Valgrind est déjà capable de repérer qu’un
programme termine en laissant de la mémoire allouée. Ici, le programme alloue
10 octets puis se termine sans jamais appeler free.
On voit toutefois une différence importante avec LSan : dans ce premier mode,
Valgrind signale seulement qu’un bloc reste alloué à la sortie, sans détailler
immédiatement l’origine précise de la fuite. Il faut ensuite affiner l’analyse
avec des options supplémentaires comme --leak-check=full.
// leak.c
#include <stdlib.h>
void main() {
void *a = malloc(10);
}
$ gcc leak.c -o leak
$ valgrind ./leak
==12315== Memcheck, a memory error detector
==12315== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==12315== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==12315== Command: ./leak
==12315==
==12315==
==12315== HEAP SUMMARY:
==12315== in use at exit: 10 bytes in 1 blocks
==12315== total heap usage: 1 allocs, 0 frees, 10 bytes allocated
==12315==
==12315== LEAK SUMMARY:
==12315== definitely lost: 0 bytes in 0 blocks
==12315== indirectly lost: 0 bytes in 0 blocks
==12315== possibly lost: 0 bytes in 0 blocks
==12315== still reachable: 10 bytes in 1 blocks
==12315== suppressed: 0 bytes in 0 blocks
==12315== Rerun with --leak-check=full to see details of leaked memory
==12315==
==12315== For lists of detected and suppressed errors, rerun with: -s
==12315== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Le terme still reachable signifie ici que la mémoire est encore référencée au moment de la terminaison. Ce n’est donc pas exactement la même qualification qu’une fuite définitivement perdue, mais cela reste une mémoire non libérée.
Memory check
Valgrind est particulièrement utile pour les lectures de mémoire non initialisée. Contrairement à un simple crash, ce type de bug peut influencer silencieusement une branche conditionnelle, un calcul ou une sortie formatée.
L’option --track-origins=yes est ici essentielle : elle permet
non seulement de détecter l’usage d’une valeur non initialisée, mais aussi de
remonter jusqu’à l’allocation ou l’écriture manquante à l’origine du problème.
#include <stdio.h>
#include <stdlib.h>
int main(){
int *mem = malloc(sizeof(int));
printf("%d\n", *mem);
return 0;
}
$ gcc uninitialised.c -o uninitialised
$ valgrind --track-origins=yes ./uninitialised
==12735== Memcheck, a memory error detector
==12735== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==12735== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==12735== Command: ./uninitialised
==12735==
==12735== Use of uninitialised value of size 8
==12735== at 0x48DAA1B: _itoa_word (_itoa.c:183)
==12735== by 0x48E4D59: __printf_buffer (vfprintf-process-arg.c:155)
==12735== by 0x48E6F90: __vfprintf_internal (vfprintf-internal.c:1544)
==12735== by 0x48DB9AA: printf (printf.c:33)
==12735== by 0x10917A: main (in /home/galas/HEIG-VD/SSE/src/C/struct/uninitialised)
==12735== Uninitialised value was created by a heap allocation
==12735== at 0x4844818: malloc (vg_replace_malloc.c:446)
==12735== by 0x10915A: main (in /home/galas/HEIG-VD/SSE/src/C/struct/uninitialised)
==12735==
==12735== Conditional jump or move depends on uninitialised value(s)
==12735== at 0x48DAA2C: _itoa_word (_itoa.c:183)
==12735== by 0x48E4D59: __printf_buffer (vfprintf-process-arg.c:155)
==12735== by 0x48E6F90: __vfprintf_internal (vfprintf-internal.c:1544)
==12735== by 0x48DB9AA: printf (printf.c:33)
==12735== by 0x10917A: main (in /home/galas/HEIG-VD/SSE/src/C/struct/uninitialised)
==12735== Uninitialised value was created by a heap allocation
==12735== at 0x4844818: malloc (vg_replace_malloc.c:446)
==12735== by 0x10915A: main (in /home/galas/HEIG-VD/SSE/src/C/struct/uninitialised)
==12735==
==12735== Conditional jump or move depends on uninitialised value(s)
==12735== at 0x48E5B73: __printf_buffer (vfprintf-process-arg.c:186)
==12735== by 0x48E6F90: __vfprintf_internal (vfprintf-internal.c:1544)
==12735== by 0x48DB9AA: printf (printf.c:33)
==12735== by 0x10917A: main (in /home/galas/HEIG-VD/SSE/src/C/struct/uninitialised)
==12735== Uninitialised value was created by a heap allocation
==12735== at 0x4844818: malloc (vg_replace_malloc.c:446)
==12735== by 0x10915A: main (in /home/galas/HEIG-VD/SSE/src/C/struct/uninitialised)
==12735==
==12735== Conditional jump or move depends on uninitialised value(s)
==12735== at 0x48E5B9F: __printf_buffer (vfprintf-process-arg.c:208)
==12735== by 0x48E6F90: __vfprintf_internal (vfprintf-internal.c:1544)
==12735== by 0x48DB9AA: printf (printf.c:33)
==12735== by 0x10917A: main (in /home/galas/HEIG-VD/SSE/src/C/struct/uninitialised)
==12735== Uninitialised value was created by a heap allocation
==12735== at 0x4844818: malloc (vg_replace_malloc.c:446)
==12735== by 0x10915A: main (in /home/galas/HEIG-VD/SSE/src/C/struct/uninitialised)
==12735==
0
==12735==
==12735== HEAP SUMMARY:
==12735== in use at exit: 4 bytes in 1 blocks
==12735== total heap usage: 2 allocs, 1 frees, 1,028 bytes allocated
==12735==
==12735== LEAK SUMMARY:
==12735== definitely lost: 4 bytes in 1 blocks
==12735== indirectly lost: 0 bytes in 0 blocks
==12735== possibly lost: 0 bytes in 0 blocks
==12735== still reachable: 0 bytes in 0 blocks
==12735== suppressed: 0 bytes in 0 blocks
==12735== Rerun with --leak-check=full to see details of leaked memory
==12735==
==12735== For lists of detected and suppressed errors, rerun with: -s
==12735== ERROR SUMMARY: 4 errors from 4 contexts (suppressed: 0 from 0)
L’exemple est intéressant car Valgrind signale à la fois le problème de valeur non initialisée et, en fin d’exécution, la fuite du bloc alloué. On voit donc qu’un même lancement peut mettre en évidence plusieurs classes d’erreurs.
Utilisation avec gdb
Valgrind peut aussi être couplé à GDB via vgdb. Ce mode est
particulièrement utile lorsqu’on veut inspecter précisément l’état du programme
au moment où une erreur mémoire est détectée.
Dans l’exemple suivant, on provoque une écriture hors limite sur un tableau alloué dynamiquement, puis on laisse Valgrind interrompre l’exécution afin de reprendre la main dans le débogueur.
#include <stdlib.h>
int main ()
{
int * array = (int *) malloc (10 * sizeof (int));
array[10] = 8;
free (array);
return 0;
}
$ gcc -g -O0 vgdb.c -o vgdb
$ gdb -q
(gdb) file ./vgdb
Reading symbols from ./vgdb...
(gdb) target extended-remote | vgdb --multi
Remote debugging using | vgdb --multi
(gdb) set sysroot /
(gdb) set remote exec-file ./vgdb
(gdb) tb main
Temporary breakpoint 1 at 0x1151: file vgdb.c, line 5.
(gdb) r
Starting program: /home/galas/HEIG-VD/SSE/src/C/tampon/vgdb
==13311== Memcheck, a memory error detector
==13311== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==13311== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==13311== Command: ./vgdb
==13311==
relaying data between gdb and process 13311
warning: File "/usr/libexec/valgrind/valgrind-monitor.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
add-auto-load-safe-path /usr/libexec/valgrind/valgrind-monitor.py
line to your configuration file "/home/galas/.gdbinit".
To completely disable this security protection add
set auto-load safe-path /
line to your configuration file "/home/galas/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual. E.g., run from the shell:
info "(gdb)Auto-loading safe path"
Temporary breakpoint 1, main () at vgdb.c:5
5 int * array = (int *) malloc (10 * sizeof (int));
(gdb) c
Continuing.
==13311== Invalid write of size 4
==13311== at 0x109167: main (vgdb.c:7)
==13311== Address 0x4a79068 is 0 bytes after a block of size 40 alloc'd
==13311== at 0x4844818: malloc (vg_replace_malloc.c:446)
==13311== by 0x10915A: main (vgdb.c:5)
==13311==
Program received signal SIGTRAP, Trace/breakpoint trap.
main () at vgdb.c:7
7 array[10] = 8;
(gdb) x/i $pc
=> 0x109167 <main+30>: movl $0x8,(%rax)
(gdb) p array
$1 = (int *) 0x4a79040
(gdb) monitor who_points_at 0x4a79040
==13311== Searching for pointers to 0x4a79040
==13311== *0x1ffefffa88 points at 0x4a79040
Address 0x1ffefffa88 is on thread 1's stack
in frame #0, created by main (vgdb.c:4)
==13311== tid 1 register RAX pointing at 0x4a79040
==13311== tid 1 register RDX pointing at 0x4a79040
(gdb) monitor leak_check summary
==13311== LEAK SUMMARY:
==13311== definitely lost: 0 (+0) bytes in 0 (+0) blocks
==13311== indirectly lost: 0 (+0) bytes in 0 (+0) blocks
==13311== possibly lost: 0 (+0) bytes in 0 (+0) blocks
==13311== still reachable: 40 (+40) bytes in 1 (+1) blocks
==13311== suppressed: 0 (+0) bytes in 0 (+0) blocks
==13311== To see details of leaked memory, give 'full' arg to leak_check
==13311==
Cette intégration permet d’aller plus loin qu’un simple rapport texte. On peut inspecter le registre d’instruction, l’adresse fautive, la pile, et même demander directement à Valgrind des informations supplémentaires sur les blocs mémoire.