Debugging

François Trahay

Debugging

But: comprendre l’exécution d’un programme

Selon une étude1, un développeur passe 50 % de son temps à debugger des programmes. Apprendre à debugger efficacement est donc nécessaire si vous souhaitez réduire la durée de cette activité pénible.

1 T. Britton et al. Reversible debugging software. University of Cambridge-Judge Business School, 2013, Technical Report.


Debugging “manuel”


Utilisation d’un Debugger


Exemple d’utilisation de GDB

$ ./sigsegv 
Debut du programme
Erreur de segmentation

$ gdb ./sigsegv 
[...]
(gdb) run
Starting program: ./sigsegv
Debut du programme

Program received signal SIGSEGV, Segmentation fault.
0x000000000040050b in main (argc=1, argv=0x7fffffffdd68) at sigsegv.c:7
7     *ptr=5;
(gdb) print ptr
$1 = (int *) 0x0

Utiliser GDB

Vous trouverez sur https://www-inf.telecom-sudparis.eu/COURS/CSC4103/Supports/?page=annexe-gdb un récapitulatif des principales commandes gdb.


Examiner l’état du programme

La commande bt (ou backtrace)

  (gdb) bt
#0  baz (a=2) at backtrace.c:7
#1  0x0000000000400581 in bar (n=5, m=3) at backtrace.c:15
#2  0x00000000004005ae in foo (n=4) at backtrace.c:21
#3  0x0000000000400559 in baz (a=5) at backtrace.c:9
[...]

La backtrace permet d’examiner l’état actuel du processus, ainsi que l’enchaînement d’appels de fonctions qui a mené à cet état.

  (gdb) bt
#0  baz (a=2) at backtrace.c:7
#1  0x0000000000400581 in bar (n=5, m=3) at backtrace.c:15
#2  0x00000000004005ae in foo (n=4) at backtrace.c:21
#3  0x0000000000400559 in baz (a=5) at backtrace.c:9
[...]
(gdb) frame
#0  baz (a=2) at backtrace.c:7
7     if(a<=2)
(gdb) print a
$1 = 2
(gdb) frame 1
#1  0x0000000000400581 in bar (n=5, m=3) at backtrace.c:15
15    return baz(m-1);
(gdb) print m
$2 = 3

Ici, gdb nous indique que le programme est arrêté dans la fonction baz, à la ligne 7 du fichier backtrace.c. Cette fonction a été appelée (frame #1) par la fonction bar à la ligne 15. La fonction bar a été appelée par foo à la ligne 31 (cf la frame #2).

En sélectionnant une frame, on peut examiner l’état des variables locales au site d’appel.


Etat des variables d’un processus

Il est possible de choisir le format d’affichage:

gdb peut également afficher la valeur d’un registre. Par exemple p $eax affiche la valeur du registre eax.


Exécution pas à pas

Une fois le programme lancé, possibilité d’exécuter les instructions une par une:


Points d’arrêt

Après avoir définis les points d’arrêt, on laisse le programme s’exécuter (avec la commande continue). Lorsque le programme atteint un des points d’arrêt, le débugger le met en pause et donne la main au développeur afin qu’il puisse examiner l’état du programme.

Par exemple:

$ gdb ./programme
[...]
(gdb) b bar
Breakpoint 1 at 0x400569: file programme.c, line 13.
(gdb) b backtrace.c:9
Breakpoint 2 at 0x40054c: file programme.c, line 9.
(gdb) r
Starting program: programme
Debut du programme

Breakpoint 1, bar (n=11, m=9) at backtrace.c:13
13    if(m<2)
(gdb) p n
$1 = 11
(gdb) p m
$2 = 9
(gdb) c
Continuing.

Breakpoint 2, baz (a=8) at backtrace.c:9
9     return foo(a-1);
(gdb) p a
$3 = 8
(gdb) c
Continuing.

Breakpoint 1, bar (n=8, m=6) at backtrace.c:13
13    if(m<2)
(gdb)

Il est également possible de définir des points d’arrêt conditionnels. Par exemple la commande

(gdb) b bar if n == 0

n’arrêtera l’exécution du programme en entrant dans la fonction bar que si n est égal à 0.


Surveiller une variable

Voici un exemple d’utilisation de la commande watch

$ gdb ./watch
  [...]
(gdb) watch n
 Hardware watchpoint 2: n
(gdb) c
Continuing.

Hardware watchpoint 2: n

Old value = 0
New value = 1
main (argc=1, argv=0x7fffffffdd68) at watch.c:7
7     for(i=0; i<1000; i++) {
(gdb) c
Continuing.

Hardware watchpoint 2: n

Old value = 1
New value = 2
main (argc=1, argv=0x7fffffffdd68) at watch.c:7
7     for(i=0; i<1000; i++) {
(gdb) p i
$1 = 17
[...]

Valgrind

Valgrind peut détecter l’utilisation de variables non initialisées. Par exemple, la non initialisation de n dans instructions suivantes est détectée par valgrind:

int n;
printf("%d$\n", n);
  $ valgrind ./exemple_valgrind 
==1148== Memcheck, a memory error detector
==1148== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==1148== Using Valgrind-3.12.0.SVN and LibVEX; rerun with -h for copyright info
==1148== Command: ./exemple_valgrind
==1148== 
==1148== Conditional jump or move depends on uninitialised value(s)
==1148==    at 0x4E7F2D3: vfprintf (vfprintf.c:1631)
==1148==    by 0x4E86AC8: printf (printf.c:33)
==1148==    by 0x400504: foo (exemple_valgrind.c:6)
==1148==    by 0x400529: main (exemple_valgrind.c:13)
==1148== 
==1148== Use of uninitialised value of size 8
==1148==    at 0x4E7C06B: _itoa_word (_itoa.c:179)
==1148==    by 0x4E7F87C: vfprintf (vfprintf.c:1631)
==1148==    by 0x4E86AC8: printf (printf.c:33)
==1148==    by 0x400504: foo (exemple_valgrind.c:6)
==1148==    by 0x400529: main (exemple_valgrind.c:13)
==1148== 
[...]
==1148== 
5
==1148== 
==1148== HEAP SUMMARY:
==1148==     in use at exit: 0 bytes in 0 blocks
==1148==   total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==1148== 
==1148== All heap blocks were freed -- no leaks are possible
==1148== 
==1148== For counts of detected and suppressed errors, rerun with: -v
==1148== Use --track-origins=yes to see where uninitialised values come from
==1148== ERROR SUMMARY: 8 errors from 8 contexts (suppressed: 0 from 0)

Valgrind détecte également les fuites mémoire. Lorsqu’une zone mémoire allouée avec malloc() n’est pas libérée (avec free()), la zone mémoire peut être ``perdue’’. L’effet peut être grave si la fuite mémoire survient fréquemment. Par exemple, un serveur web qui perdrait quelques octets lors du traitement d’une requête web, pourrait perdre plusieurs gigaoctets de mémoire après le traitement de millions de requêtes.

Valgrind détecte ce type de fuites mémoire. Pour obtenir des informations sur l’origine de la fuite, on peut utiliser l’option --leak-check=full :

$ valgrind  --leak-check=full ./exemple_valgrind2 
==1572== Memcheck, a memory error detector
==1572== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==1572== Using Valgrind-3.12.0.SVN and LibVEX; rerun with -h for copyright info
==1572== Command: ./exemple_valgrind2
==1572== 
85823552
==1572== 
==1572== HEAP SUMMARY:
==1572==     in use at exit: 1,024 bytes in 1 blocks
==1572==   total heap usage: 2 allocs, 1 frees, 2,048 bytes allocated
==1572== 
==1572== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==1572==    at 0x4C2BBCF: malloc (vg_replace_malloc.c:299)
==1572==    by 0x40054E: main (exemple_valgrind2.c:7)
==1572== 
==1572== LEAK SUMMARY:
==1572==    definitely lost: 1,024 bytes in 1 blocks
==1572==    indirectly lost: 0 bytes in 0 blocks
==1572==      possibly lost: 0 bytes in 0 blocks
==1572==    still reachable: 0 bytes in 0 blocks
==1572==         suppressed: 0 bytes in 0 blocks
==1572== 
==1572== For counts of detected and suppressed errors, rerun with: -v
==1572== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Pointeurs de fonction

1 Oui, c’est sans rapport avec le debugging, mais pour équilibrer les séances, nous avons préféré ne pas aborder cette notion lors du cours sur les pointeurs :-)

Exemple:

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

double add(double a, double b) {
  return a+b;
}

double substract(double a, double b) {
  return a-b;
}

int main(int argc, char**argv) {
  double n, m;
  scanf("%lf", &n);
  scanf("%lf", &m);
  // declare a function pointer named "operation"
  double (*operation)(double, double) = NULL;

  if(n < m) {
    /* operation points to the add function */
    operation = add;
  } else {
    /* operation points to the substract function */
    operation = substract;
  }

  /* call the function pointed to by operation */
  double result = operation(n, m);

  printf("Result of the operation: %lf\n", result);

  return EXIT_SUCCESS;
}

On peut définir un type (à l’aide du mot-clé typedef) correspondant à un pointeur de fonction. Par exemple:

typedef double (*op_function)(double, double);

définit le type op_function. On peut donc ensuite déclarer un pointeur de fonction de ce type en faisant:

op_function operation;

Un plugin implémentant ce service allouera la structure et désignera ses fonctions comme callback pour le service.