Guide du débogueur

Un article de Prog.

Pour corriger un bug, la première chose à faire est de l'isoler très précisément. Ce chapitre donne des recettes pour arriver à cet objectif.

Sommaire

Protocole pour reproduire le bug

La première chose à faire est d'écrire un protocole (liste ordonnée d'opérations simples) permettant de reproduire le bug.

La première version consiste à noter ce dont on se souvient : ce qu'on a fait pour être confronté au bug. Ensuite, il faut arriver au protocole le plus court possible : le nombre d'opérations minimal pour arriver à reproduire le bug. Enfin, l'essentiel étant d'arriver à rapidement reproduire le bug, et à chaque fois.

Localiser le bug dans le code

Maintenant qu'on sait précisément quand le bug apparait, il faut arriver à le localiser dans le code.

Débogueur et fichier core

Si le programme échoue avec une erreur fatale (SIGSEGV (erreur de segmentation), SIGFPE (divison par zéro), etc.), on peut tenter de reproduire le bug dans un débogueur (tel que gdb). Parfois, gdb inhibe le bug (voir la section "Heisenbug" plus bas). On peut alors demander à Linux de générer un fichier core (dans le dossier courant) avec la commande "ulimit -c unlimited". On se reservira du fichier core avec gdb : "gdb programme core".

Méthode printf

Quand l'utilisation d'un débogueur (gdb) n'est pas possible (mémoire du programme corrompue une fois qu'on arrive au bug ou programme trop dynamique pour pouvoir être interrompu), ou qu'on est confronté à un Heisenbug, il existe une méthode qui fonctionne toujours mais qui est longue à mettre en place : la méthode "printf". L'idée est d'ajouter manuellement des "sondes" pour suivre le fonctionnement interne du programme. On cherche à savoir quand une fonction précise est appelée, connaître l'évolution d'une variable, etc. En pratique, on utilise printf() (ou print en Python ou ...) qu'on ajoute près de l'endroit qu'on veut surveiller.

Cette méthode est longue et incrémentale : il faudra recompiler et relancer le programme à chaque fois qu'on ajoute ou modifie une sonde.

isenbug

Les Heisenbugs sont une catégorie de bug très corriace. Leur nom vient du principe de l'incertitude énoncé par Werner Heisenberg. Ce principe indique que le fait d'observer une expérience va la modifier. Dans notre cas : un programme exécuté dans un débogueur, profileur ou modifié par la méthode des printf va se comporter différement (et le bug va disparaitre).

En pratique, les Heisenbugs sont des erreurs d'accès à la mémoire : lecture de mémoire non allouée, écriture dans une zone mémoire non allouée, double libération d'une zone mémoire, etc.

Il faut alors utiliser des outils dédiés tels que l'outillage minimaliste de la glibc (variable d'environnement MALLOC_CHECK_=1) ou (mieux) Valgrind. Valgrind permet de tracer les erreurs d'accès à la mémoire au bit près. Son gros défaut est qu'il va ralentir l'exécution du programme d'environ 20x... On se retrouve dans le principe de l'incertitude : le fait de déboguer un programme avec Valgrind va en modifier son comportement (ralentir son exécution). Néanmois, on peut faire des adaptions mineures au programmeur pour éviter ça.

Dichotomie subversion

Pour corriger une regression (bug ajouté dans un projet qui fonctionnait), revenez en arrière pour retrouver une version qui fonctionne :

svn up -r4530            # version subversion numéro 4530
svn up -r'{2008-03-01}'  # retour au 1er mars 2008

Lorsque vous avez trouvé l'ancienne version qui n'a pas le bug, réduisez l'intervalle pour retrouver le commit ou les commits fautifs.

Lorsque l'intervalle est suffisamment petit, relisez les commit pour rechercher manuellement le bug :

svn diff -r4540:4541   # changements du commit 4541
svn diff -r4540:4570   # changements des commits 4541 à 4570