Aller au contenu

Débogage avancé/Travail pratique/Observation à la loupe

Leçons de niveau 17
Une page de Wikiversité, la communauté pédagogique libre.
Début de la boite de navigation du travail pratique
Observation à la loupe
Image logo représentative de la faculté
T.P. no 6
Leçon : Débogage avancé

TP de niveau 17.

Précédent :Débogage à la volée
Suivant :Allocation dans la pile
En raison de limitations techniques, la typographie souhaitable du titre, « Travail pratique : Observation à la loupe
Débogage avancé/Travail pratique/Observation à la loupe
 », n'a pu être restituée correctement ci-dessus.


Un des bugs classiques en C et C++ est de lire ou d'écrire en dehors d'une zone préalablement allouée. Suivant les cas les comportements à l'exécution sont un peu différents. Cela va permettre d'illustrer différentes techniques sur des bugs très similaires.

Les trois codes à déboguer

[modifier | modifier le wikicode]

Lire un peu avant la zone allouée

[modifier | modifier le wikicode]

Créer un fichier bug_overreadheap_tiny.c contenant le code suivant.

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const uint32_t NB = 100;

void fibon(const uint32_t n, uint32_t p[static n]) {
  // C11 array args, p is guaranteed by programmer to be at least n
  for (uint32_t i = 0; i < n; i++)
    if (i < 1) // BUG: i < 2
      p[i] = i;
    else
      p[i] = p[i - 1] + p[i - 2]; // BUG: read p[-1]
}

int main() {
  assert(NB > 2);
  uint32_t *p = malloc(sizeof(uint32_t[NB]));
  assert(p != NULL);
  memset(p, 0, sizeof(uint32_t[NB]));

  fibon(NB, p);

  free(p);
  return EXIT_SUCCESS;
}

Écrire un peu après la zone allouée

[modifier | modifier le wikicode]

Créer un fichier bug_overwriteheap_tiny.c contenant le code suivant.

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

const unsigned int NB = 10000;

void fibon(unsigned int size, unsigned int p[size]) {
  for (unsigned int i = 0; i <= size; i++) // BUG: i < size
    if (i < 2)
      p[i] = i;
    else
      p[i] = p[i - 1] + p[i - 2]; // BUG: write p[NB]
}

int main() {
  assert(NB > 2);

  unsigned int *p = malloc(sizeof(int[NB]));
  assert(p != NULL);

  fibon(NB, p);

  free(p);
  return EXIT_SUCCESS;
}

Écrire loin au-delà de la zone allouée

[modifier | modifier le wikicode]

Créer un fichier bug_overwriteheap_large.c contenant le code suivant.

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

const unsigned int SIZE = 10000; // size may be a misleading name

/**
   fibon overwrite the array 4 times the length allocated by main

   Thanks to the C11 array size notation, gcc-12+ detects the bug and
   warns you ! Read the warnings ! Solve them !

   Use as recent as possible compiler. Even for quite stable language
   as C, it is helpful.

   Note also that with the old classical notation, aka.
     unsigned int size, unsigned int *p
   or if you do not indicate the size of the array in the argument,
   the compiler may not help you.

   @param size size of the array
   @param p array of size "size"
 */
void fibon(unsigned int size, unsigned int p[static size]) {
  for (unsigned int i = 0; i <= size; i++)
    if (i < 2)
      p[i] = i;
    else
      p[i] = p[i - 1] + p[i - 2];
}

int main() {
  assert(SIZE > 2);

  unsigned int *p = malloc(SIZE); // BUG: 4x smaller than correct sizeof( int[SIZE] ));
  assert(p != NULL);

  fibon(SIZE, p);

  free(p);
  return 0;
}


Lire un peu avant la zone allouée

[modifier | modifier le wikicode]

Le code bug_overreadheap_tiny.c lit une valeur juste un peu devant l'allocation. Comme c'est la première allocation, elle est en bordure de page et cela provoque instantanément un SEGFAULT. Il est donc relativement aisé de trouver la source du problème avec gdb en demandant simplement les valeurs des variables.

  1. Compiler le fichier bug_overreadheap_tiny.c. Un compilateur récent devrait détecter le bug.
  2. Lancer le programme bug_overreadheap_tiny. Vous devriez obtenir le SEGFAULT attendu
  3. Lancer le programme avec Valgrind qui devrait vous indiquer la ligne du code avec l'erreur.
  4. Lancer le programme dans gdb. Afficher le contenu des 20 premières cases du tableau en décimal.
  5. Demander l'affichage de p[i-2]
  6. Recompiler le programme avec ASan. Lancer le programme. ASan détecte bien le SEGFAULT mais ne vous indique rien sur sa cause.

Écrire un peu après la zone allouée

[modifier | modifier le wikicode]

Le code bug_overwriteheap_tiny.c écrit une valeur juste un peu après l'allocation (un entier de 4 octets, 0 octet après, donc juste après). Lors d'une exécution normale, il ne se passe rien ! Il se termine parfaitement normalement. Mais le bug est bien là. Valgrind est capable de le détecter.


  1. Compiler le fichier bug_overwriteheap_tiny.c. Il est fort possible que votre compilateur ne remonte aucun problème.
  2. Lancer le programme. Il ne devrait y avoir aucun problème non plus, car la détection matérielle ne fonctionne qu'à la granularité d'une page (typiquement 4Kio).
  3. Lancer le programme avec Valgrind qui devrait vous indiquer la ligne du code avec l'erreur.
  4. Recompiler le programme avec ASan. Lancer le programme. L'erreur devrait aussi être détectée à l'exécution.

Écrire loin au-delà de la zone allouée

[modifier | modifier le wikicode]

Dans le code bug_overwriteheap_large.c l'allocation est 4 fois trop petite. Beaucoup d'écritures débordent. Néanmoins, la détection n'a lieu qu'au moment du free. Valgrind sait détecter le problème. Mais si nous soupçonnons un débordement en écriture, nous pouvons demander à gdb de surveiller explicitement des modifications juste après notre allocation.

Cette technique est particulièrement utile lorsque quelque chose a modifié une de vos structures de données: vous ne savez pas quoi ou quand, mais vous savez où.

  1. Compiler le fichier bug_overwriteheap_large.c. Le compilateur devrait détecter le problème.
  2. Lancer le programme. La détection du problème a lieu au moment du free.
  3. Lancer le programme avec Valgrind qui devrait vous indiquer les lignes du code avec l'erreur.
  4. Charger le programme dans GDB. Mettre un breakpoint au niveau du malloc. Après l'exécution du malloc, mettre un watchpoint sur la mémoire juste après le malloc (attention à l'arithmétique de pointeurs !). Continuer.
  5. Recompiler le programme avec ASan. Lancer le programme. L'erreur devrait aussi être détectée à l'exécution.