Lire un rapport Valgrind : Invalid read, definitely lost

Invalid read of size, uninitialised value, definitely lost, still reachable : chaque message de Valgrind expliqué sur un programme fautif, et comment le corriger.

Par
Publié le

Valgrind trouve les bugs mémoire que le compilateur ne voit pas : lectures hors bornes, valeurs jamais initialisées, blocs jamais libérés. Encore faut-il décoder son rapport, qui empile les adresses et les mots anglais. Chaque message a pourtant une structure fixe, et une fois qu'on la connaît, on remonte à la ligne fautive en quelques secondes.

En bref

Un rapport Valgrind se lit de haut en bas : la première ligne nomme le type d'erreur (Invalid read of size 4, definitely lost...), les lignes at et by donnent la pile d'appels, et tu cherches la première qui pointe dans ton fichier. Compile avec -g pour avoir les numéros de ligne, et lance valgrind --leak-check=full --track-origins=yes ./prog. La dernière ligne, ERROR SUMMARY, compte les erreurs. Chaque famille de message a une cause précise et une correction simple, détaillées ci-dessous.

La forme d'un message

Prends ce programme, qui alloue cinq entiers et en lit un sixième :

notes.cÀ éviter
#include <stdio.h>#include <stdlib.h>
int main(void){    int *notes = malloc(5 * sizeof(int));    if (notes == NULL)        return 1;    for (int i = 0; i <= 5; i++)        notes[i] = 10 + i;    printf("dernière note : %d\n", notes[5]);    free(notes);    return 0;}
Terminal
$ gcc -Wall -Wextra -g notes.c -o notes$ valgrind --leak-check=full --track-origins=yes ./notes==14== Memcheck, a memory error detector==14== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.==14== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info==14== Command: ./notes==14== ==14== Invalid write of size 4==14==    at 0x40070C: main (notes.c:10)==14==  Address 0x4a7f054 is 0 bytes after a block of size 20 alloc'd==14==    at 0x488545C: malloc (vg_replace_malloc.c:446)==14==    by 0x4006D3: main (notes.c:6)==14== ==14== Invalid read of size 4==14==    at 0x400730: main (notes.c:11)==14==  Address 0x4a7f054 is 0 bytes after a block of size 20 alloc'd==14==    at 0x488545C: malloc (vg_replace_malloc.c:446)==14==    by 0x4006D3: main (notes.c:6)==14== dernière note : 15...==14== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

Chaque élément a un rôle. Le ==14== au début de chaque ligne est le PID du processus : c'est juste un préfixe qui sépare les commentaires de Valgrind de la sortie de ton programme. La première ligne d'un bloc donne le type d'erreur. La ligne at est l'endroit exact de l'accès fautif, les lignes by sont les appelants. Ici, main (notes.c:10) et main (notes.c:11) : ta boucle écrit puis lit une case de trop.

La ligne suivante explique l'adresse : 0 bytes after a block of size 20 alloc'd. Le bloc fait 20 octets (cinq int), et l'accès tombe juste après. Le deuxième groupe at/by dit où ce bloc avait été alloué : notes.c:6. La correction tient à la condition de boucle, i < 5 au lieu de i <= 5.

Invalid read / write of size N

C'est le message du programme ci-dessus. Invalid veut dire que l'accès tombe hors d'un bloc valide, read ou write selon que tu lis ou écris, et size N donne la taille de l'accès en octets (4 pour un int). La ligne sur l'adresse précise où, par rapport au bloc le plus proche :

  • 0 bytes after a block of size 20 : juste après la fin, un dépassement par le haut ;

  • is 2 bytes inside a block of size 32 free'd : dans un bloc déjà libéré, donc un accès après free ;

  • is not stack'd, malloc'd or (recently) free'd : une adresse qui ne correspond à rien de connu, souvent un pointeur nul ou fantaisiste.

La cause est toujours la même famille : un indice hors bornes, un pointeur mal calculé, ou l'usage d'un bloc après l'avoir rendu. Valgrind signale l'accès mais ne l'empêche pas, donc un crash peut suivre juste après le message. Memcheck ne voit ces dépassements que sur le tas : sortir d'un tableau local ne donne pas d'Invalid read.

Conditional jump or move depends on uninitialised value(s)

Celui-là surprend, parce que le programme compile et affiche souvent un résultat qui semble correct. Il apparaît quand une décision du programme dépend d'une valeur jamais initialisée :

somme.cÀ éviter
#include <stdio.h>
static int somme(const int *t, int n){    int total;
    for (int i = 0; i < n; i++)        total += t[i];    return total;}
int main(void){    int notes[3] = {12, 15, 9};
    if (somme(notes, 3) >= 30)        printf("admis\n");    else        printf("refusé\n");    return 0;}
Terminal
$ gcc -Wall -Wextra -g somme.c -o somme$ valgrind -q --track-origins=yes ./somme==14== Conditional jump or move depends on uninitialised value(s)==14==    at 0x4006D4: main (somme.c:16)==14==  Uninitialised value was created by a stack allocation==14==    at 0x400644: somme (somme.c:4)==14== admis

total n'a jamais reçu de valeur de départ, donc total += t[i] part d'un contenu indéterminé. Valgrind ne se plaint pas quand tu copies une valeur non initialisée, seulement quand elle influence ce que fait le programme : une décision, comme le if de la ligne 16, une adresse calculée, ou un appel système. C'est tout l'intérêt de --track-origins=yes : la deuxième partie du message remonte à l'origine de la valeur, a stack allocation dans somme à la ligne 4. Cette option double le temps d'exécution et ajoute au moins 100 Mo de mémoire, mais elle fait gagner un temps fou sur ce genre de bug. La correction : int total = 0;.

Le message dépend de l'usage, pas de l'origine : Use of uninitialised value of size N apparaît quand la valeur sert d'adresse ou d'indice (souvent dans printf, qui convertit le nombre en texte). Si la valeur vient d'un malloc, l'origine indique a heap allocation au lieu de a stack allocation. Le remède est le même, initialiser avant de lire, ou allouer avec calloc(3) qui met tout à zéro.

Invalid free

Valgrind suit chaque bloc du tas, donc il sait quand un free est invalide :

Terminal
$ valgrind -q ./doublegrademe==16== Invalid free() / delete / delete[] / realloc()==16==    at 0x48884F8: free (vg_replace_malloc.c:989)==16==    by 0x40071B: main (double.c:13)==16==  Address 0x4a7f040 is 0 bytes inside a block of size 16 free'd==16==    at 0x48884F8: free (vg_replace_malloc.c:989)==16==    by 0x400713: main (double.c:12)==16==  Block was alloc'd at==16==    at 0x488545C: malloc (vg_replace_malloc.c:446)==16==    by 0x4006D3: main (double.c:7)

Trois dates clés dans un seul bloc : le free fautif (ligne 13), le free précédent du même bloc (ligne 12), et l'allocation d'origine (ligne 7). C'est un double free. Les messages de la glibc pour ces cas (double free detected in tcache 2, invalid pointer) et leur correction ont leur propre article, double free detected.

Les fuites : definitely, indirectly, possibly, still reachable

À la fin, Valgrind liste la mémoire encore allouée et la classe en quatre catégories. Ce programme en produit une de chaque :

fuites.c
#include <stdlib.h>
struct noeud {    int valeur;    struct noeud *suivant;};
char *curseur;char *journal;
int main(void){    char *copie = malloc(32);   /* perdu : pointeur réaffecté */    copie = NULL;
    struct noeud *tete = malloc(sizeof(struct noeud));    tete->suivant = malloc(sizeof(struct noeud));   /* tenu par tete */    tete = NULL;                /* toute la liste devient inatteignable */
    curseur = malloc(64);    curseur += 10;              /* seul un pointeur vers l'intérieur reste */
    journal = malloc(128);      /* jamais libéré, mais encore pointé */    return 0;}
Terminal
$ valgrind --leak-check=full --show-leak-kinds=all ./fuites...==14== 16 bytes in 1 blocks are indirectly lost in loss record 1 of 5==14==    at 0x488545C: malloc (vg_replace_malloc.c:446)==14==    by 0x40066F: main (fuites.c:17)==14== 32 bytes in 1 blocks are definitely lost in loss record 2 of 5==14==    at 0x488545C: malloc (vg_replace_malloc.c:446)==14==    by 0x400653: main (fuites.c:13)==14== 32 (16 direct, 16 indirect) bytes in 1 blocks are definitely lost in loss record 3 of 5==14==    at 0x488545C: malloc (vg_replace_malloc.c:446)==14==    by 0x400663: main (fuites.c:16)==14== 64 bytes in 1 blocks are possibly lost in loss record 4 of 5==14==    at 0x488545C: malloc (vg_replace_malloc.c:446)==14==    by 0x400687: main (fuites.c:20)==14== 128 bytes in 1 blocks are still reachable in loss record 5 of 5==14==    at 0x488545C: malloc (vg_replace_malloc.c:446)==14==    by 0x4006BB: main (fuites.c:23)==14== ==14== LEAK SUMMARY:==14==    definitely lost: 48 bytes in 2 blocks==14==    indirectly lost: 16 bytes in 1 blocks==14==      possibly lost: 64 bytes in 1 blocks==14==    still reachable: 128 bytes in 1 blocks

Chaque catégorie décrit une relation différente entre ton programme et le bloc oublié :

CatégorieCe que ça veut dire
definitely lostplus aucun pointeur ne vise le bloc. Fuite franche, à corriger.
indirectly lostle bloc n'est perdu que parce que le bloc qui le pointait l'est aussi (un maillon d'une liste dont la tête a fui).
possibly lostil reste un pointeur, mais vers l'intérieur du bloc, pas son début. Valgrind ne peut pas jurer que c'est une vraie référence.
still reachablele bloc est encore pointé à la fin (souvent par une globale). Tu aurais pu le libérer, tu ne l'as pas fait.

Avec --leak-check=full, Valgrind ne détaille que definitely et possibly (la valeur par défaut de --show-leak-kinds est definite,possible). Ajoute --show-leak-kinds=all pour voir aussi indirectly et still reachable. Dans ce programme, libérer proprement la liste à partir de tete fait disparaître d'un coup la perte directe et la perte indirecte : corrige la racine, pas chaque maillon.

ERROR SUMMARY : erreurs et contextes

La dernière ligne compte. Lance un programme qui lit trois cases au-delà d'un tableau alloué sur le tas, dans une boucle :

Terminal
$ valgrind ./boucle 2>&1 | tail -1==16== ERROR SUMMARY: 3 errors from 1 contexts (suppressed: 0 from 0)

3 errors from 1 contexts : la même lecture fautive s'est produite trois fois (trois tours de boucle), mais depuis un seul endroit du code. Valgrind regroupe les erreurs identiques par « contexte », c'est-à-dire par pile d'appels : ça évite de noyer le rapport quand un bug est dans une boucle. Vise toujours zéro, et attaque les contextes un par un. suppressed compte les erreurs connues qu'on a choisi d'ignorer via un fichier de suppression, à zéro ici.

C

read_the_leak

build_path renvoie le bon chemin mais perd un bloc à chaque appel. Le texte affiché est correct, c'est la vérification mémoire qui échoue : l'exercice te met exactement dans la situation d'un definitely lost à lire et à corriger. Certains exercices C de Grademe ajoutent cette vérification mémoire avec Memcheck, et la moindre fuite rend la cellule mémoire rouge même quand la sortie est juste.

Ouvrir l'exercice

La commande et ses options sont résumées dans la fiche de valgrind(1).

Questions fréquentes

Que veut dire « Invalid read of size 4 » ?

Ton programme a lu 4 octets (la taille d'un int) à une adresse hors d'un bloc valide. La ligne suivante précise par rapport à quel bloc : 0 bytes after a block of size 20 est un dépassement juste après la fin d'un tableau de cinq int. La cause typique est un indice hors bornes.

Pourquoi « conditional jump depends on uninitialised value » alors que mon code marche ?

Parce qu'une variable sert à une décision sans avoir reçu de valeur de départ. Le résultat « correct » n'est qu'un coup de chance sur le contenu de la mémoire ce jour-là. Initialise la variable, ou alloue avec calloc. Ajoute --track-origins=yes pour voir d'où vient la valeur.

Quelle différence entre « definitely lost » et « still reachable » ?

definitely lost veut dire qu'aucun pointeur ne vise plus le bloc : tu ne peux même plus le libérer, c'est une vraie fuite. still reachable veut dire que le bloc est encore pointé à la fin du programme, souvent par une globale : tu aurais pu le libérer. Les deux signalent un free manquant.

Faut-il corriger les « still reachable » ?

Dans un programme qui se termine, le système récupère toute la mémoire à la sortie, donc un still reachable est moins grave qu'une vraie fuite. Mais dans un service qui tourne longtemps, ces blocs s'accumulent. Dans les exercices C qui vérifient la mémoire, la correction de Grademe les compte comme des erreurs : vise une sortie sans aucun bloc restant.

Que comptent « errors » et « contexts » ?

errors est le nombre total d'erreurs rencontrées, contexts le nombre d'endroits distincts du code d'où elles viennent. Une erreur dans une boucle compte plusieurs fois mais depuis un seul contexte. Corrige par contexte.

Sources

À toi d'écrire le code.