But: comprendre l’exécution d’un programme
12
? Ca devrait être 17, non
?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.
printf("entrée dans foo(n=%d)\n", n);
dans le
code source$ ./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
-g
gdb
:
gdb ./programme
r
(ou run
)Ctrl+C
q
(ou quit
)Vous trouverez sur https://www-inf.telecom-sudparis.eu/COURS/CSC4103/Supports/?page=annexe-gdb
un récapitulatif des principales commandes gdb
.
La commande bt
(ou backtrace
)
#x
avec la
commande frame [x]
. (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.
p [var]
(ou print [var]
)
[var]
display [var]
[var]
à chaque arrêt du
programme[var]
peut être une variable (eg.
p n
)[var]
peut être une expression (eg.
p ptr->next->data.n
)Il est possible de choisir le format d’affichage:
p/d [var]
affiche la valeur décimale de
[var]
p/u [var]
affiche la valeur décimale (non signée) de
[var]
p/x [var]
affiche la valeur hexadécimale de
[var]
p/o [var]
affiche la valeur octale de
[var]
p/t [var]
affiche la valeur binaire de
[var]
p/a [var]
affiche la valeur [var]
sous
forme d’adressep/c [var]
affiche la valeur [var]
sous
forme de caractèrep/f [var]
affiche la valeur [var]
sous
forme de flottantgdb
peut également afficher la valeur d’un registre. Par
exemple p $eax
affiche la valeur du registre
eax
.
Une fois le programme lancé, possibilité d’exécuter les instructions une par une:
n
(ou next
) : exécute la prochaine
instruction, puis arrête le programme.s
(ou step
) : idem. Si l’instruction est
un appel de fonction, ne descend pas dans la fonction.c
(ou continue
) : continue l’exécution du
programme (arrête le mode pas à pas)b fichier.c:123
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
.
watch x
ptr
ne devrait pas être NULL
!
Qui a fait ça ?”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 [programme]
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;
("%d$\n", n); printf
$ 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)
foo
désigne l’adresse de la fonction
int foo(int a, char b);
int (*function_ptr)(int a, char b) = foo;
function_ptr(12, 'f'); // appelle la fonction foo
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 :-)
(*nom_pointeur)
NULL
int
qui ne peut pointer que
sur une valeur de type int
, un pointeur sur
int (*f)(double, char, int)
ne peut pointer que sur une
fonction dont le prototype est
int f(double, char, int);
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;
("%lf", &n);
scanf("%lf", &m);
scanf// declare a function pointer named "operation"
double (*operation)(double, double) = NULL;
if(n < m) {
/* operation points to the add function */
= add;
operation } else {
/* operation points to the substract function */
= substract;
operation }
/* call the function pointed to by operation */
double result = operation(n, m);
("Result of the operation: %lf\n", result);
printf
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;
struct component {
char* plugin_name;
void (*init_value)(struct value*v);
void (*compute_value)(struct value*v);
void (*print_value)(struct value*v);
};
Un plugin implémentant ce service allouera la structure et désignera ses fonctions comme callback pour le service.