[LINUX] Débugger ses propres programmes [GDB/Valgrind]

Pardonnez pour cette longue absence, la fin d’année scolaire est plutôt riche en examens et projets, donc je suis en manque crucial de temps pour écrire de petits articles !

Je disais donc : fin d’année scolaire riche en projets. Ce qui inclut évidemment plantages à tout-va et dysfonctionnements en série, et c’est là que j’interviens pour faire un petit mémo ou même vous aider à mieux comprendre comment débugger vos programmes (pour l’instant qu’en C mais je suis sûr que plus tard, cette liste de langages sera enrichie). Alors face à cette étape récurrente et quasi obligatoire pour tout développeur, le débug, on a deux grandes écoles :

  • La vieille école : je mets des printf partout pour voir où je m’arrête, ou où ça plante, je supprime/commente des parties de code pour voir lesquelles plantent, etc.. Si vous êtes ici c’est que vous êtes forcément passés par là (et j’en connais des bons qui débuggent toujours comme ça) !
  • La bonne école : utiliser un ou plusieurs outils qui vous permettront de débugger. Et c’est ici qu’arrive à point nommé : GDB et Valgrind.

Ces deux programmes m’ont été d’un grand secours dans mon petit projet à rendre (disponible sur mon git) pour une de mes UV d’info (LO22 pour les UTBohéMiens mais bientôt renommée et refaite, heureusement), et m’a surtout appris qu’il ne fallait JAMAIS utiliser strncpy, dont le comportement est bien trop aléatoire ! Mais ça c’est une autre histoire.

Petit exemple, ci-contre :

 #include <string.h>;
#include <stdio.h>;
#include <stdlib.h>;

int main (int argc, char** argv){
    char string[42] = "J'adore tricksandprojects.wordpress.com !";
    char* chaine = NULL;
    strcpy(chaine, string);
    puts(chaine);
    return EXIT_SUCCESS;
}

Et là, belle erreur de segmentation et vu que le code est tout petit, la seule manière est d’y aller à tâtons, en essayant un peu tout. Imaginez dans un long code, il faut d’abord identifier la portion de code défectueuse et ensuite travailler à tâtons ! Comme le dit un célèbre même :

Aint-Nobody-Got-Time-for-That

Bon, ici l’erreur est assez grossière mais c’est pour servir d’exemple, et montrer que Valgrind et GDB sont bien complémentaires. On a dans ce cas une belle erreur de segmentation et GDB nous dit froidement :

Program received signal SIGSEGV, Segmentation fault.
__strcpy_ssse3 () at ../sysdeps/x86_64/multiarch/strcpy-ssse3.S:98
98    ../sysdeps/x86_64/multiarch/strcpy-ssse3.S: Aucun fichier ou dossier de ce type.

On fait appel à la petite fonction bt (pour « backtrace ») et il nous explique où est l’erreur :

(gdb) bt
#0  __strcpy_ssse3 () at ../sysdeps/x86_64/multiarch/strcpy-ssse3.S:98
#1  0x0000000000400671 in main (argv=1, argc=0x7fffffffe828) at test_segfault.c:9

On sait désormais que l’erreur est à la ligne 9 du fichier test_segfault.c. Pourtant, on en ignore toujours la raison (bon, en fait on s’en doute mais c’est pour le test) et c’est pourquoi on lance Valgrind, qui nous donne à son tour :

Invalid write of size 1
==7198==    at 0x4C2D7FC: strcpy (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==7198==    by 0x400670: main (test_segfault.c:9)
==7198==  Address 0x0 is not stack'd, malloc'd or (recently) free'd

On apprend que notre programme fait une écriture mémoire invalide d’une case, ce qui signifie un malloc trop petit d’un octet. Mais c’est surtout la dernière ligne ici qui doit vous interpeller : l’adresse 0x0 (l’adresse NULL en fait) n’a pas d’espace mémoire alloué ! Il manque donc le malloc, tout simplement !

Facile à corriger, donc on modifie notre code pour donner :

 #include <string.h>;
#include <stdio.h>;
#include <stdlib.h>;

int main (int argc, char** argv){
    char string[42] = "J'adore tricksandprojects.wordpress.com !";
    char* chaine = NULL;
    chaine = malloc(sizeof(char)*strlen(string));
    strcpy(chaine, string);
    puts(chaine);
    return EXIT_SUCCESS;
}

On lance notre programme et tout va bien ! Mais en fait, pas vraiment. Si vous lancez le programme avec GDB ou sans outil de débug, la console affichera bien : J'adore tricksandprojects.wordpress.com ! sans aucune erreur. Et pourtant, il y a une erreur qu’il faut absolument corriger ou alors le programme aura un comportement tout à fait aléatoire suivant les machines sur lesquelles on le lance (à cause des espaces mémoires alloués). On ne peut voir cette erreur qu’avec Valgrind :

==7229== Invalid write of size 1
==7229==    at 0x4C2D812: strcpy (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==7229==    by 0x400718: main (test_segfault.c:9)
==7229==  Address 0x51fc069 is 0 bytes after a block of size 41 alloc'd
==7229==    at 0x4C2CD7B: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==7229==    by 0x400701: main (test_segfault.c:8)
==7229== 
==7229== Invalid read of size 1
==7229==    at 0x4C2D7D4: __GI_strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==7229==    by 0x4EA4ECB: puts (ioputs.c:36)
==7229==    by 0x400724: main (test_segfault.c:10)
==7229==  Address 0x51fc069 is 0 bytes after a block of size 41 alloc'd
==7229==    at 0x4C2CD7B: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==7229==    by 0x400701: main (test_segfault.c:8)

On apprend donc qu’on écrit et lit illégalement d’une case mémoire. On voit aussi que les erreurs ont lieu dans le strcpy ligne 9 du fichier test_segfault.c et dans le puts ligne 10 du même fichier. On notera que l’ordre donné dans une seule erreur par valgrind est l’ordre de la fonction la plus interne à la plus externe (comprenez, le plus proche de la machine jusqu’au plus proche de l’utilisateur ou du développeur). Le plus généralement, un Invalid write est associé avec un Invalid read de la même variable, donc en en résolvant un, l’autre devrait partir gentiment 🙂 On remplace donc la ligne 8 du fichier test_segfault.c par :

chaine = malloc(sizeof(char)*(strlen(string)+1));

Et là aucun problème ! Eh bien en fait, il y en a toujours un. Raté. Un rapide coup d’œil au log de Valgrind va vous dire pourquoi :

==9691== HEAP SUMMARY:
==9691==     in use at exit: 42 bytes in 1 blocks
==9691==   total heap usage: 1 allocs, 0 frees, 42 bytes allocated
==9691== 
==9691== LEAK SUMMARY:
==9691==    definitely lost: 42 bytes in 1 blocks
==9691==    indirectly lost: 0 bytes in 0 blocks
==9691==      possibly lost: 0 bytes in 0 blocks
==9691==    still reachable: 0 bytes in 0 blocks
==9691==         suppressed: 0 bytes in 0 blocks
==9691== Rerun with --leak-check=full to see details of leaked memory
==9691== 
==9691== For counts of detected and suppressed errors, rerun with: -v
==9691== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)

Vous avez un fuite mémoire. Ouch !

Quand vous attribuez un espace mémoire à une variable (malloc, calloc, realloc…), il ne faut jamais oublier de le libérer (avec free) ! Tout malloc a son free. Dans les petits programmes que vous développez, l’impact est tout à fait risible mais on peut déjà atteindre facilement les 20Mo de fuites avec un programme de listes chaînées mal optimisé (un projet que j’ai fait l’année dernière) sans y faire attention, alors imaginez si les développeurs de jeux ou logiciels lourds que vous utilisez tous les jours ne faisaient pas attention à ces fuites : votre mémoire vive diminuerait à vue d’œil et ralentirait considérablement votre ordinateur. Valgrind ne vous permet cependant pas de détecter quel est le pointeur dont l’espace alloué n’a pas été libéré, il vous faudra donc le trouver par vous même. Ici, la correction est triviale étant donné que l’on n’a qu’un seul pointeur. Il suffit donc de terminer le programme par :

    free(chaine);
    chaine = NULL; /*Obligatoire après un free !*/
    return EXIT_SUCCESS;

Un petit argument à rajouter à l’exécution de Valgrind qui vous permettra de détecter les fuites mémoires comme des erreurs et vous indiquer l’endroit dans le programme où l’espace a été alloué :

valgrind --leak-check=full ./mon_prog

Retournera dans notre cas :

==5009== 42 bytes in 1 blocks are definitely lost in loss record 1 of 1
==5009==    at 0x4C2CD7B: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5009==    by 0x400705: main (test_segfault.c:8)

Le malloc effectué à la ligne 8 du fichier test_segfault.c n’a pas eu son free !

Laisser un commentaire