Valgrind est une suite permettant le debugging / profiling de programmes. Elle est livrée avec plusieurs outils, tels que Memcheck, Helgrind ou encore Callgrind.
Nous parlerons uniquement de Memcheck, qui est l'outil activé par défaut, permettant de traquer les erreurs de gestion de la mémoire.
N'hésitez pas à chercher des informations à propos des autres utilitaires de la suite !
Afin d'analyser un processus avec valgrind, vous devrez le lancer grâce à une commande de cette forme (cf man valgrind) :
valgrind [options de valgrind] [chemin vers l'executable] [options de votre programme]
NB : la commande valgrind ./a.out
est équivalente à valgrind --tool=memcheck ./a.out
Lors de l'execution d'un processus, si une erreur est détectée par Memcheck, une stack trace (ou stack backtrace) s'affichera sur votre terminal, qui correspond au cheminement d'appel de fonctions qui a mené à cette erreur.
Je vous recommande vivement de vous renseigner sur ce qu'est exactement une pile d'execution (call stack).
Memcheck pourra même vous indiquer avec précision la ligne qui a provoqué l'erreur, mais il faudra pour cela que vous utilisiez une option de gcc lors de la compilation. Il s'agit de -g
qui permet de générer des informations de debug utilisables par valgrind (et gdb, dont nous parlerons plus tard). Il est aussi recommandé de ne pas activer les options d'optimisations de gcc (le man gcc explique pourquoi).
-
Accès à des zones non allouées (invalid read/write)
Si vous tentez d'acceder en lecture ou en ecriture à une zone mémoire qui n'est pas allouée à votre executable, vous allez alors générer une erreur de type "invalid read/write of size N" (et potentiellement un crash), N étant la taille de cette zone. Un exemple commun est celui du parcours d'une chaine de caractères qui n'aurait pas de'\0'
final. -
Utilisation de valeurs non initialisées (uninitialised values)
L'utilisation de variables non initialisées est une source d'erreurs courante. Pensez à bien initialiser chaque variable avant utilisation. Afin de detecter plus facilement les variables que vous auriez pu omettre, gcc fournit des options qui permettent d'augmenter la quantité et la diversité des warning qui seront lancés lors de la compilation. En voici une liste non exhaustive
-W
,-Wall
,-Wextra
Leur utilisation est fortement recommandée, car plus vous aurez d'erreurs lors de la compilation de votre programme, moins vous en aurez lors de l'execution. -
Fuites mémoires (memory leaks)
Chaque bloc de memoire alloué grace àmalloc()
doit être liberé grace àfree()
dès l'instant ou vous n'en aurez plus besoin. Dans le cas contraire votre programme finira par monopoliser beaucoup plus de ressources que necessaire, ce qui peut rapidement devenir gênant. Si vous ne me croyez pas, vous pouvez essayer d'executer ce code :
while (1)
malloc(42); // kaboom
Puis ensuite cette version
while (1)
free(malloc(42)); // free instantanement la memoire allouée
- Free invalides (double frees, ...)
Si vous choisissez une valeur arbitraire que vous assignez à un pointeur, que cette valeur ne corresponds pas à l'adresse du début d'un bloc alloué à votre programme, et que vous appellezfree()
avec ce pointeur en paramètre il y aura un invalid free. De la même façon, si vous essayez de liberer une adresse mémoire qui à déjà été libérée auparavant, vous allez obtenir une erreur du type "double free or corruption". Pour eviter un double free, il existe deux solutions : Soit vous avez fait une erreur, et il y a unfree()
en trop dans votre code, que vous devrez retrouver et supprimer. Soit, si vous ne pouvez pas eviter que le même pointeur soit utilisé deux fois parfree()
, il sera conseillé de le reinitialiser àNULL
(ceci s'explique par le fait quefree()
n'a aucun effet sur un pointeurNULL
, cf man free).
==PID== Memcheck, a memory error detector
==PID== Copyright (C) 2002-2012, and GNU GPL'd, by Julian Seward et al.
==PID== Using Valgrind-3.8.1 and LibVEX; rerun with -h for copyright info
==PID== Command: ./test
==PID==
==PID== Invalid free() / delete / delete[] / realloc() [ TYPE D'ERREUR + STACK TRACE ]
==PID== at 0x4C28ADC: free (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so) [ L'ERREUR VIENS DE CETTE FONCTION ]
==PID== by 0x4005CD: main (test.c:19) [ QUI A ETE APPELLÉE DANS test.c l:19]
==PID== Address 0x51e0040 is 0 bytes inside a block of size 42 free'd [ DETAILS A PROPOS L'ERREUR ]
==PID== at 0x4C28ADC: free (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==PID== by 0x4005C1: main (test.c:18)
==PID==
==PID==
==PID== HEAP SUMMARY:
==PID== in use at exit: 110 bytes in 11 blocks [ MEMOIRE TOUJOURS ALLOUÉE A LA SORTIE DU PROCESSUS ]
==PID== total heap usage: 12 allocs, 1 frees, 126 bytes allocated [ NOMBRE D'ALLOCATIONS / LIBERATIONS EFFECTUÉES ]
==PID==
==PID== LEAK SUMMARY: [ INFORMATIONS SUR LES FUITES MEMOIRE ]
==PID== definitely lost: 110 bytes in 11 blocks [ FUITES MEMOIRE ]
==PID== indirectly lost: 0 bytes in 0 blocks [ FUITES INDIRECTES (EX: OUBLI DE FREE LE MEMBRE D'UNE STRUCTURE) ]
==PID== possibly lost: 0 bytes in 0 blocks [ ... ]
==PID== still reachable: 0 bytes in 0 blocks [ ... ]
==PID== suppressed: 0 bytes in 0 blocks
==PID== Rerun with --leak-check=full to see details of leaked memory
==PID==
==PID== For counts of detected and suppressed errors, rerun with: -v
==PID== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 2 from 2)
NB : memcheck ralenti considerablement l'execution du processus
--leak-check=no/summary/full (analyse detaillée des leaks)
--trace-malloc=no/yes (affiche en live les appels a malloc/free)
--trace-children=no/yes (monitoring des enfants du process)
--trace-fds=no/yes (monitoring des fds)
--track-origins=no/yes (donne des informations sur l'origine des valeurs non initialisées)
Il est parfois nécessaire d'avoir une analyse plus approfondie que celle permise par Valgrind. Nous allons maintenant parler de GDB, qui permet de comprendre en temps réel les moindres détails du fonctionnement de votre code.
Pour commencer à utiliser GDB il suffit de faire : gdb chemin_vers_votre_executable
.
Vous pouvez aussi vous attacher à un processus en cours d'exécution grâce à : gdb -p pid
.
NB : avoir un binaire compilé avec -g
est la encore un gros plus
GDB s'attache à un processus, et va permettre d'effectuer plusieurs types d'actions. Il fonctionne comme un shell, c'est à dire qu'il s'utilise avec des lignes de commandes.
Démarrer l'exécution du programme :
- run (ou r) :
usage :r path_to_executable [args]
exemple :r ./a.out --arg1 "argument 2" "argument 3"
Gestion des points d'arrêts (breakpoints, watchpoints) :
Un point d'arrêt est un endroit précis ou l'on va demander au debuggeur de mettre en pause l'exécution d'un processus. Cet état de pause va permettre d'analyser en profondeur l'état de l'execution à un instant T.
-
break (ou b) :
usage :b [fichier:]nom_de_fonction/numero de ligne
Place un point d'arrêt à une ligne, ou une fonction précise. Vous pouvez de manière optionnelle préciser le fichier dans lequel se situe la ligne / fonction souhaitée. -
watch :
usage :watch nom_de_variable
Place un watchpoint sur une variable. L'execution du processus sera interrompue des lors que sa valeur sera modifiée -
info breakpoints / watchpoints :
usage :i b
oui watchpoint
Affiche une liste des breakpoints / watchpoints -
delete (ou d) :
usage :d [n]
Supprime tous les breakpoints.
Si n est précisé, seul le breakpoint numéro n sera affecté
Maitriser l'exécution :
Une fois l'exécution mise en pause par l'intermediaire des breakpoints, plusieurs possibilités s'offrent à nous pour analyser le comportement du processus.
-
disp :
usage :disp expr
Affiche la valeur de expr au cours de l'exécution
expr peut être n'importe quelle expression valide (nom de variable, appel de fonction, ...) -
step (ou s) :
Poursuit l'exécution jusqu'à la ligne suivante -
next (ou n) :
Poursuit l'exécution jusqu'à la ligne suivante
Contrairement à step, next ne rentre pas dans les appels de fonctions -
cont (ou c) :
Reprends l'exécution normale du processus -
i args :
Affiche la liste des arguments de la fonction actuelle et leur valeurs -
i reg :
Affiche la liste des registres du processus et leurs contenu
Manipulation de la call stack :
-
bt :
Affiche la stack backtrace -
up / down :
Deplace gdb un niveau plus haut ou plus bas dans la call stack
Il existe beaucoup d'autres commandes, je pense que vous avez compris que google est votre ami !
Strace est un petit programme qui va vous permettre de voir rapidement et facilement les interactions entre un processus et votre système.
Pour lancer strace il suffit de faire : strace chemin_vers_votre_executable
De la même manière que gdb, vous pouvez l'attacher à un processus en cours d'exécution de cette manière : strace -p pid
strace permet de tracer en temps réel tous les appels systèmes effectués par un processus (y compris les valeurs de leur paramètres et les valeurs de retour) ainsi que les signaux reçus et émis.
ça arrive bientôt
Puisque je sais que vous n'aimez pas lire, j'ai conçu un petit code pour que vous puissiez vous entrainer à utiliser tous ces super outils !
-
Vous trouverez ci-après un code source peu complexe, comportant plusieurs bugs. Votre objectif est de les trouver à l'aide des outils que je vous ai presenté plus tôt.
-
Le programme une fois débuggé génère simplement une liste de chaines de caractères aléatoires.
-
Le programme s'utilise de cette façon :
./a.out size count
, ousize
représente la taille d'une chaine générée, etcount
le nombre de chaines. -
Le code final doit être assez ressemblant au code de base, si de trop lourdes modifications ont été apportées, c'est une erreur de votre part.
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef BASE
# define BASE 10
#endif
void str_randomize(char *s, long size)
{
int i;
for (i = 0; i <= size; i++)
s[i] = (rand() + 32) % 127;
}
char **str_array_alloc(long count)
{
return (malloc(count * sizeof(char *)));
}
void str_alloc(char *s, long size)
{
s = malloc(sizeof(char) * size);
}
void str_array_print(char **result)
{
int i;
while (result[i])
{
printf("%s\n", result[i]);
i++;
}
}
void init_generator(char *av[], long str_size, long str_count)
{
srand(time(NULL));
str_size = strtol(av[1], NULL, BASE);
str_count = strtol(av[2], NULL, BASE);
}
int array_generate(char ***result, long size, long count)
{
int i;
*result = str_array_alloc(count);
if (*result = NULL)
{
perror("allocation failed ");
return (1);
}
for (i = 0; *result[i] != NULL; i++)
{
str_alloc(**result, size);
str_randomize(**result, size);
}
return (0);
}
int main(int argc, char *argv[])
{
char **result;
long str_size = 0;
long str_count = 0;
init_generator(argv, str_size, str_count);
if (array_generate(&result, str_size, str_count) == 1)
return (1);
str_array_print(result);
free(result);
return (0);
}