1. Introduction

Le débogage est une tâche complexe. Comme une grande partie du travail des développeurs consiste à déboguer des programmes, il est important de se familiariser avec les outils disponibles.

Sous Linux, le débogueur maison est GDB. D'aspect rebutant et primitif, il s'utilise en ligne de commande. Nombreux sont les développeurs, surtout ceux qui viennent du monde Windows et sont familiers avec Visual Studio, qui ne tentent même pas d'utiliser GDB.

GDB utilise des commandes en ligne. Il est donc indispensable de bien connaître ces commandes. Bien sûr, ça ressemble au débogage tel qu'on le pratiquait dans les années quatre-vingt, mais il y a des choses que l'on ne peut faire qu'avec GDB.

On utilisera dans la suite de ce tutoriel le code suivant :

app.c
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
#include<stdio.h>

#define addnum(a,b) ((a)+(b))
#define max(a,b) (a)>(b)?(addnum(a,100)):(addnum(b,50))

int a1=10,a2=20;

void f2()
{
    printf("X");
}

void print_dbg()
{
    char buf[15];
    printf("a1=%d a2=%d \n",a1,a2);
    
    strcpy(buf, "hello world");
    
}

int add(int a, int b)
{
    int c;
    c= max(a+90,b+20);
    c+=b;
    return c;
}

void f1()
{
    int i;
           int *p=NULL;
    print_dbg();
    i=add(80,90);
    for(i=0;i<100;i++)
    {
        if(i % 20 == 0)
            a1++;
        f2();
    }
        *p=100;
}
int arr[1000];

void main()
{
  arr[0]=1;
  f1();
  printf("hello %d\n",arr[0]);
}

2. Appel de fonctions à partir du débogueur

Quand GDB est bloqué sur un point d'arrêt ou en mode pas à pas, on peut appeler une fonction en lui passant ou non des paramètres :

watch points
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
(gdb) b add
Breakpoint 1 at 0x400654: file ./a.c, line 21.
(gdb) r
Starting program: myapp 
a1=10 a2=20 

Breakpoint 1, add (a=80, b=90) at ./a.c:21
21        int c= a+90;
(gdb) call print_dbg()
a1=10 a2=20 
(gdb) delete breakpoints 
Delete all breakpoints? (y or n) y
(gdb) b a.c:34
Breakpoint 2 at 0x4006bd: file ./a.c, line 34.
(gdb) c
Continuing.

Breakpoint 2, f1 () at ./a.c:34
34                a1++;
(gdb) call add(2,3)
$1 = 95

3. Points de surveillance

L'une des fonctionnalités remarquables de GDB est son aptitude à suspendre un programme lorsque quelque chose est modifié, que ce soit une variable, une adresse ou un registre.

3-A. Surveillance d'une variable

watch variable
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
(gdb) watch a1
Hardware watchpoint 1: a1
(gdb) r
Starting program: /home/developer/examples/debuggingExamples/debugtests/app 
a1=10 a2=20 

Hardware watchpoint 1: a1

Old value = 10
New value = 11
f1 () at ./a.c:35
35            f2();

3-B. Surveillance d'une adresse

On commence par afficher l'adresse du tableau :

get address
Sélectionnez
1.
2.
(gdb) p &arr
$1 = (int (*)[1000]) 0x601080 <arr>

puis on demande à GDB de s'arrêter dès que les données situées à cette adresse sont modifiées :

watch point
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
(gdb) watch *0x601080
Hardware watchpoint 1: *0x601080
(gdb) r
Starting program: /home/developer/examples/debuggingExamples/debugtests/app 

Hardware watchpoint 1: *0x601080

Old value = 0
New value = 1
main () at ./a.c:43
43    f1();

3-C. Surveillance d'un registre

La même approche est possible avec un registre du processeur :

register watch
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
(gdb) watch $rax
Watchpoint 1: $rax

(gdb) c
Continuing.

Watchpoint 1: $rax

Old value = 4196067
New value = 0
0x00000000004006f6 in main () at ./a.c:43
43    f1();

4. Débogage de macros du préprocesseur C

Déboguer les macros est une tâche complexe. Quand c'est possible, il est préférable de travailler avec des fonctions en ligne. Sinon, lors de la compilation, on peut utiliser l'option « ‑E » pour visualiser la sortie du préprocesseur. On peut aussi demander à GDB de convertir une macro en code final pour voir comment elle est interprétée lors de l'exécution :

macro expansion
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
(gdb) b add
Breakpoint 1 at 0x400654: file ./a.c, line 28.
(gdb) r
Starting program: /home/developer/examples/debuggingExamples/debugtests/app 
a1=10 a2=20 

Breakpoint 1, add (a=80, b=90) at ./a.c:28
28        c= max(a+90,b+20);
(gdb) macro expand max(a+90,b+20)
expands to: (a+90)>(b+20)?(((a+90)+(100))):(((b+20)+(50)))

5. Inspection de code sans exécution

Si vous avez mis en place des gestionnaires d'erreurs dans votre code et que le programme se crashe, l'adresse fautive est affichée dans le journal :

error log
Sélectionnez
1.
2.
3.
signal 11 received 
EIP address = 0x400702
Segmentation fault (core dumped)

Vous pouvez charger le programme sans l'exécuter avec GDB :

 
Sélectionnez
1.
# gdb ./app

puis demander à GDB d'afficher la ligne de l'adresse fautive :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
(gdb) l *0x400702
0x400702 is in f1 (./a.c:45).
40        {
41            if(i % 20 == 0)
42                a1++;
43            f2();
44        }
45        *p=100;
46    }
47    int arr[1000];
48    
49    void main()

C'est très utile si le programme s'exécute sur une autre plate-forme, par exemple un système embarqué. On peut dans ce cas utiliser un débogueur croisé (arm-linux-gdb).

6. Analyse de fichier core

Quand un programme s'interrompt suite à une erreur, le système peut sauvegarder son état dans un fichier core si on a exécuté préalablement :

 
Sélectionnez
1.
# ulimit -c unlimited

Lancez le programme d'exemple, il s'arrête en erreur et un fichier core est créé.

 
Sélectionnez
1.
2.
3.
# ./app
a1=10 a2=20 
Segmentation fault (core dumped)

Chargez ensuite le programme et le fichier core associé avec GDB :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
# gdb ./app ./core
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./app...done.
[New LWP 34577]
Core was generated by `./app'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000400706 in f1 () at ./a.c:45
45        *p=100;
(gdb)

On peut voir le type d'erreur (erreur de segmentation) et connaître l'état du programme au moment du crash :

 
Sélectionnez
1.
2.
3.
(gdb) bt
#0  0x0000000000400706 in f1 () at ./a.c:45
#1  0x0000000000400727 in main () at ./a.c:52

6-A. Registres

Le contenu des registres du processeur peut être visualisé :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
(gdb) info registers 
rax            0x0    0
rbx            0x0    0
rcx            0xfbad2a84    4222429828
rdx            0x58    88
rsi            0x7fce21600780    140523299932032
rdi            0x7fce215ff620    140523299927584
rbp            0x7fff73ecdd90    0x7fff73ecdd90
rsp            0x7fff73ecdd80    0x7fff73ecdd80
r8             0x7fce21600780    140523299932032
r9             0x7fce21807700    140523302057728
r10            0x1d6    470
r11            0x7fce212ab290    140523296436880
r12            0x4004e0    4195552
r13            0x7fff73ecde80    140735138291328
r14            0x0    0
r15            0x0    0
rip            0x400706    0x400706 <f1+132>
eflags         0x10202    [ IF RF ]
cs             0x33    51
ss             0x2b    43
ds             0x0    0
es             0x0    0
fs             0x0    0
gs             0x0

6-B. Pile

On affiche vingt mots à partir de l'adresse du pointeur de pile :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
(gdb) x/20w $rsp
0x7fff73ecdd80:    4196176    100    0    0
0x7fff73ecdd90:    1944903072    32767    4196135    0
0x7fff73ecdda0:    4196176    0    556116016    32718
0x7fff73ecddb0:    0    0    1944903304    32767
0x7fff73ecddc0:    562207904    1    4196111    0

7. Automatisation

GDB permet d'automatiser des tâches. On peut, par exemple, lui demander de faire quelque chose de particulier lorsqu'un point d'arrêt est atteint :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
(gdb) b add
Breakpoint 1 at 0x400654: file ./a.c, line 28.
(gdb) commands 
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>print a1
>call print_dbg()
>end
(gdb) r
Starting program: /home/developer/examples/debuggingExamples/debugtests/app 
a1=10 a2=20 

Breakpoint 1, add (a=80, b=90) at ./a.c:28
28        c= max(a+90,b+20);
$1 = 10
a1=10 a2=20

Ceci peut être défini dans le fichier « .gdbinit » (situé dans le répertoire de débogage) que GDB chargera et exécutera automatiquement.

Fichier « .gdbinit » :

.gdbinit
Sélectionnez
1.
2.
3.
4.
5.
6.
echo "hi"

b add
commands
print a1
call print_dbg()

Quand on lance le débogueur, le fichier « .gdbinit » est sourcé :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
# gdb ./app 
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./app...done.
"hi"Breakpoint 1 at 0x400654: file ./a.c, line 28.

8. Macro commandes GDB

À l'aide du fichier « .gdbinit », on peut aussi définir de nouvelles commandes (des macros) utilisables dans ses sessions de débogage. On peut par exemple écrire les définitions suivantes :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
define irg
    info registers
    info threads
end

define pdb
    print a1
    print a2
    call print_dbg()
end

Ce fichier définit deux nouvelles macros réutilisables, « pdb » et « irg ».

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
(gdb) pdb
$2 = 10
$3 = 20
a1=10 a2=20 
(gdb) irg
rax            0x0    0
rbx            0x0    0
rcx            0x6f77206f6c6c6568    8031924123371070824
rdx            0x7ffff7dd3780    140737351858048
rsi            0x5a    90
rdi            0x50    80
rbp            0x7fffffffdb50    0x7fffffffdb50
rsp            0x7fffffffdb50    0x7fffffffdb50
r8             0x0    0
r9             0xd    13
r10            0x0    0
r11            0x246    582
r12            0x4004e0    4195552
r13            0x7fffffffdc60    140737488346208
r14            0x0    0
r15            0x0    0
rip            0x400654    0x400654 <add+10>
eflags         0x246    [ PF ZF IF ]
cs             0x33    51
ss             0x2b    43
ds             0x0    0
es             0x0    0
fs             0x0    0
gs             0x0    0
  Id   Target Id         Frame 
* 1    process 35330 "app" add (a=80, b=90) at ./a.c:28

9. Définition de variables GDB

Vous pouvez définir des variables dans la session de débogage puis les utiliser avec d'autres commandes.

 
Sélectionnez
1.
2.
3.
(gdb) set $myvar = 100
(gdb) call add($myvar,20)
$4 = 310

10. Astuces pour déboguer simultanément plusieurs processus et threads

Quand vous déboguez un processus qui utilise l'appel système fork(2), vous pouvez choisir de continuer dans le processus enfant ou dans le processus parent :

 
Sélectionnez
1.
2.
(gdb) set follow-fork-mode child 
(gdb) set detach-on-fork on

Quand vous déboguez une application multithreadée, par défaut, lorsqu'un point d'arrêt est atteint, le débogueur gèle tous les threads. Vous pouvez demander au débogueur de laisser les autres threads continuer à s'exécuter à l'aide cette commande :

 
Sélectionnez
1.
(gdb) set non-stop on

11. Intégration avec les environnements de développement intégrés

Une dernière bonne raison d'utiliser GDB est son aptitude à s'intégrer avec d'autres programmes. En premier lieu, vous pouvez utiliser GDB avec l'option « -tui » qui permet de visualiser lors de l'exécution le code source et d'autres fenêtres, le tout en mode texte.

 
Sélectionnez
1.
# gdb -tui ./app

Vous pouvez aussi trouver des frontaux qui donnent accès aux commandes en ligne de GDB. Vous disposez ainsi des deux méthodes en parallèle (ligne de commande et interface graphique). Vous en trouverez une liste ici.

Une dernière note

En tant que développeurs, on utilise le clavier en permanence. Apprendre les commandes de GDB ne prend qu'une demi-heure et en se servant d'un aide mémoire, on finit vite par ne plus avoir besoin de documentation.

12. Notes de la rédaction

La rédaction remercie Liran B.H pour l'autorisation de traduction de son article :

« 10 Things You Can Only Do With GDB »

La rédaction remercie également :

jlliagre pour sa traduction ;

Chrtophe pour sa relecture technique ;

Claude Leloup pour sa relecture orthographique.