Aller au contenu
Topos Uranos

Résumé

Un programme qui donne un résultat erroné ne se corrige pas en changeant des lignes au hasard, mais en enquêtant. Cette leçon présente le débogage comme une méthode expérimentale : reproduire la défaillance, formuler une hypothèse sur sa cause, concevoir une expérience qui la confirme ou l’écarte, et circonscrire l’emplacement du défaut ; la dichotomie le trouve en un nombre d’étapes qui croît comme le logarithme de la taille du programme. Elle développe l’instrument (la compilation avec -g et sans optimisation, la configuration de Visual Studio Code sur les trois systèmes et une session réelle de GDB qui montre ce que l’éditeur fait en coulisse), les points d’arrêt simples et conditionnels, les pas, la pile d’appels et l’inspection de l’état, avec un exemple central : la médiane qui échoue seulement avec un nombre pair de données. Elle se termine par les stratégies qui valent plus que l’outil (réduire le cas, chercher le dernier point correct, vérifier des invariants avec assert, lire le rapport du détecteur) et par trois cas : une erreur de décalage d’une unité, une récursivité qui ne termine pas et une référence invalidée.

Objectifs d’apprentissage

  1. Configurer la compilation avec des informations de débogage (-g, sans optimisation) et lancer le débogueur depuis Visual Studio Code sur son propre système.
  2. Utiliser des points d’arrêt simples et conditionnels, l’exécution pas à pas (entrer, passer, sortir) et l’inspection des variables, des expressions et de la pile d’appels.
  3. Appliquer une stratégie systématique (reproduire, réduire, procéder par dichotomie, formuler et mettre à l’épreuve des hypothèses) pour localiser un défaut donné, en documentant chaque hypothèse.
  4. Combiner le débogueur avec les détecteurs et avec les tests pour localiser une erreur de mémoire à partir de son rapport.

Déboguer, c’est enquêter

Le défaut et la défaillance

La défaillance est ce que l’on observe : un résultat erroné, un programme qui ne termine pas, un rapport du détecteur (sanitizer). Le défaut (ou erreur) est la cause : une ligne, une condition ou une décision de conception qui ne correspond pas à la spécification. La défaillance se voit ; le défaut, il faut le trouver, et il peut être loin d’elle. Il s’ensuit que corriger là où se voit la défaillance, par exemple en retranchant 2 au résultat parce qu’« il sort toujours 2 de trop », corrige le symptôme et laisse la cause, qui reviendra avec d’autres données.

S’il en est ainsi, déboguer est une enquête sur la cause, et sa méthode est celle des sciences expérimentales :

  1. Observer et reproduire. Décrire avec précision la défaillance (avec quelle entrée, ce qu’on attendait, ce qu’on a obtenu) et obtenir qu’elle se produise chaque fois qu’on le veut. Un défaut qui ne se reproduit pas ne peut pas être étudié, et l’on ne peut pas non plus vérifier qu’il a été corrigé.
  2. Formuler une hypothèse. Une affirmation sur la cause qui puisse se révéler fausse : « le tri ne trie pas », « l’indice de la seconde donnée centrale a un de trop ».
  3. Concevoir une expérience. Choisir un point du programme et prédire quel état on devrait y voir si l’hypothèse est vraie et lequel si elle est fausse.
  4. Conclure et circonscrire. Écarter l’hypothèse ou la confirmer ; dans les deux cas, l’intervalle suspect se réduit. On recommence jusqu’à ce que l’intervalle soit une ligne.

Or la méthode exige de la discipline : changer une seule chose à la fois, noter chaque hypothèse avec son résultat, et ne pas toucher au code tant qu’on ne sait pas ce qu’on cherche. Changer des lignes au hasard jusqu’à ce que la sortie paraisse bonne peut masquer la défaillance sans supprimer le défaut.

ExempleLa médiane avec un nombre pair de données

La médiane d’une liste de données est la donnée centrale de la liste triée si leur nombre est impair, et la moyenne des deux données centrales s’il est pair. La leçon Tester a montré que les tests révèlent quand une fonction échoue, mais non où se trouve son défaut. La fonction suivante passe le test avec cinq données et non avec quatre.

Programme en C++
#include <algorithm>#include <cstddef>#include <print>#include <vector>​// Médiane d’une liste de données.// Précondition : values n’est pas vide.// Postcondition : renvoie la donnée centrale de la liste triée, ou la moyenne des deux centrales.double median(std::vector<double> values){    std::ranges::sort(values);    const std::size_t n{values.size()};    const std::size_t middle{n / 2};    if (n % 2 == 1) {        return values[middle];    }    return (values[middle] + values[middle + 1]) / 2.0;}​int main(){    const std::vector<double> odd{7.0, 1.0, 5.0, 3.0, 9.0};    const std::vector<double> even{7.0, 1.0, 5.0, 3.0};    std::println("Médiane de {} : {}", odd, median(odd));    std::println("Médiane de {} : {}", even, median(even));}
Sortie
Médiane de [7, 1, 5, 3, 9] : 5Médiane de [7, 1, 5, 3] : 6

L’observation est précise : avec {7,1,5,3}\{7, 1, 5, 3\}, triées {1,3,5,7}\{1, 3, 5, 7\}, on attendait (3+5)/2=4(3 + 5)/2 = 4 et on a obtenu 6 ; avec cinq données, le résultat est correct. Il y a au moins trois hypothèses : (H1) le tri ne trie pas ; (H2) les indices des deux données centrales sont faux ; (H3) la somme ou la division sont mal écrites. Chacune prédit quelque chose de différent à la ligne 17, avant de l’exécuter : si H1 est vraie, values n’est pas trié ; si H2 l’est, values[middle] et values[middle + 1] ne valent pas 3 et 5 ; si H3 l’est, les deux données valent 3 et 5 et pourtant le calcul donne 6. Que le cas impair fonctionne rend H1 improbable, mais « improbable » n’est pas « écartée » ; l’expérience, avec le débogueur, consiste à arrêter le programme à la ligne 17 et à regarder.

La dichotomie

Quand il n’y a pas d’hypothèse claire, il reste une méthode qui n’échoue pas : la dichotomie. On suppose que l’état du programme est correct au début et erroné à la fin, et qu’en chaque point intermédiaire on peut décider s’il est correct (en le comparant à une valeur calculée à la main, ou en vérifiant un invariant). On examine l’état au milieu de l’intervalle suspect : s’il est correct, le défaut est dans la seconde moitié ; sinon, dans la première. C’est la recherche dichotomique de la leçon Rechercher et trier, appliquée aux lignes d’un programme (ou aux itérations d’une boucle, ou aux versions d’un fichier).

La figure suit la dichotomie de seize lignes avec le défaut à la ligne 11 ; chaque trait est une expérience sur l’état après cette ligne : correct (✓) ou erroné (✗).

Démonstration

  1. Au début, les seize lignes sont suspectes : on sait que l’état est correct avant la ligne 1 et erroné après la ligne 16.

  2. Après la ligne 8, l’état est correct : les lignes 1 à 8 sont disculpées.

  3. Après la ligne 12, l’état est déjà incorrect : le défaut est entre la ligne 9 et la ligne 12.

  4. Après la ligne 10, correct : il reste les lignes 11 et 12.

  5. Après la ligne 11, incorrect : le dernier point correct et le premier incorrect sont voisins, et le défaut est à la ligne 11.

Le décompte est celui de la recherche dichotomique : chaque expérience divise l’intervalle par deux, de sorte que kk expériences suffisent pour 2k2^k lignes. Pour un programme de mille lignes, dix suffisent, car 210=10242^{10} = 1024. D’où l’avantage de la méthode : son coût croît comme le logarithme de la taille, et elle n’exige pas de comprendre tout le programme, mais de savoir décider, en un point, si l’état est celui qu’il doit être. Or cette décision est la partie difficile : les spécifications et les invariants sont le critère avec lequel on juge chaque état intermédiaire.