Débogage de code C++ : comment arrêter d'avoir peur de la segmentation fault et aimer GDB
Avez-vous déjà passé trois heures à chercher un bug qui s'avérait être un break oublié dans un switch ou un ; superflu après un if ? Si oui — bienvenue au club. Le débogage C++ est un art qui frôle le masochisme. Mais comme tout art, il peut être maîtrisé.
Dans cet article, nous n'allons pas simplement voir « comment lancer GDB », mais des techniques réelles qui transforment le débogage d'une divination dans le marc de café en un travail de détective à la Sherlock Holmes. Nous parlerons des sanitizers modernes, du débogage réversible et même de la façon de traquer un bug qui ne se reproduit « que le vendredi après-midi ».
1. Les classiques : GDB et LLDB
Commençons par les fondamentaux. GNU Debugger (GDB) et son concurrent LLDB sont les couteaux suisses du débogueur. Mais la plupart des gens les utilisent à 10% de leurs capacités. Corrigeons cela.
Points d'arrêt conditionnels
Vous placez un point d'arrêt dans une boucle de 10 000 itérations et appuyez sur continue 9999 fois ? Arrêtez. Utilisez des points d'arrêt conditionnels :
(gdb) break main.cpp:42 if i == 5000Cela vous fera gagner des heures de vie. Dans LLDB, la syntaxe est similaire :
(lldb) breakpoint set --file main.cpp --line 42 --condition 'i == 5000'Watchpoints — attraper les modifications de variables
Imaginez : vous avez une variable qui change sans que vous sachiez où. Placez un watchpoint :
(gdb) watch my_variable(gdb) rwatch my_variable # se déclenche à la lecture(gdb) awatch my_variable # se déclenche à la lecture et à l'écritureMaintenant, le programme s'arrêtera exactement au moment où quelqu'un touchera votre variable. Magique, non ?
2. Reverse Debugging : voyage dans le temps
Le pire cauchemar d'un programmeur : le programme plante, mais vous ne savez pas comment il en est arrivé là. Le débogage réversible (reverse debugging) permet de « rembobiner » l'exécution.
Dans GDB, cela nécessite le support de record :
(gdb) record(gdb) continue... le programme plante ...(gdb) reverse-step # pas en arrière(gdb) reverse-continue # continuer en sens inverseDans les outils commerciaux (comme UndoDB ou rr de Mozilla), cela fonctionne encore mieux. rr (Record and Replay) est un outil gratuit qui enregistre l'exécution d'un programme, puis permet de la rejouer encore et encore :
$ rr record ./my_program$ rr replay(gdb) continue... on trouve l'endroit du plantage ...(gdb) reverse-stepConseil : utilisez rr pour rechercher les courses de données (data races) — il reproduit chaque exécution de manière déterministe.
3. Sanitizers : vaccin contre les comportements indéfinis
Le C++ moderne (à partir de Clang et GCC 4.8+) dispose d'outils intégrés que nos pères n'auraient jamais imaginés. AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan) et ThreadSanitizer (TSan) sont des incontournables.
Exemple : attraper une fuite mémoire
// compile with: g++ -fsanitize=address -g main.cpp -o main#include <iostream>
void leak() { int* p = new int(42); // oubli du delete}
int main() { leak(); return 0;}On lance — et on voit :
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from: #0 0x7f... in operator new(unsigned long) #1 0x400... in leak() main.cpp:5 #2 0x400... in main main.cpp:10ASan montrera la pile d'appels exacte, y compris le numéro de ligne où l'allocation a eu lieu. Fini les « il y a une fuite mémoire quelque part ».
UBSan contre les comportements indéfinis
// compile with: g++ -fsanitize=undefined -g main.cppint main() { int x = INT_M