Les Bases de GDB

Dans cet article nous allons présenter l’utilisation basique du débogueur GDB. Nous commencerons par illustrer les différents modes de lancement avant de présenter les différentes commandes de base.

À la fin de ce tutoriel vous saurez:

  • Lancer GDB sur votre programme
  • Vous attacher à un programme en cours d’éxécution
  • Explorer l’état d’un programme via l’invite de commande GDB

Installer GDB

GDB est une commande de base pour tout développment en C/C++/Fortran. Il devrait donc être sur votre système. Si c’est le cas, passez cette section.

Pour l’installer sous Centos 7:

yum install gdb

Pour l’installer sous Ubuntu:

apt-get install gdb

Une fois installé vous devriez pouvoir le lancer:

gdb

Et obtenir une sortie analogue à celle-ci:

GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-94.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
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-redhat-linux-gnu ».
For bug reporting instructions, please see:
http://www.gnu.org/software/gdb/bugs.
(gdb)

Lancer GDB

GDB se lance avec le programme cible en argument. Pour illutrer cela, considérons le programme erroné suivant:

int main(int argc, char *argv[])
{
	int *a = NULL;

	*a = 9;

	return 0;
}

Compilons le avec les symboles de débug (considérant qu’il s’appelle t.c) :

gcc -g t.c -o test

L’option « -g » , demande à GCC d’ajouter des informations supplémentaires au binaire, en particulier,le binaire contiendra les informations permettant de faire correspondre une fonction à ses sources. Il est indispensable d’ajouter cette option pour déboguer dans de bonnes conditions. Dans le cas contraire, certaines informatiosn pourraient être seulement partiellement visibles.

Si nous lançons le programme:

./test

Nous obtenons la sortie suivante:

[1] 10446 segmentation fault ./test

Cela s’iginifie que le programme a été interrompu car il a fait un accès mémoire incorrect, provoquant une erreur se segmentation. La pluspart du temps, cela s’ignifie qu’il à lu ou écrit à une adresse incorrecte. Dans la majorité des cas impliquant ce type d’erreur, il faut utiliser un débogueur pour extraire l’état du programme menant à cette erreur et donc pour la corriger. Pour langer avec GDB, il faut utiliser simplement la commande suivante:

gdb ./test

Notez que si ./test prennait des arguments il faudrait utiliser :

gdb --args ./test ARG1 ARG2

Dans le cas contraire les arguments ne seraient pas transmis au programme.

Pour quitter GDB on peut soit utiliser la commande quit dans l’invite GDB:

(gdb) quit

Ou bien de manière plus succinte faire CTRL+D (pour émettre un EOF, end-of-file).

Attacher GDB

Il est également possible de s’attacher à un programme en cours d’éxécution. Cela peut être utile par exemple pour voir où le programme en est et pour diagnostiquer des inter-blocages. Dans cet exemple nous allons diagnostiquer un inter-blocage:


int bar()
{
    while(1){}
}

int main(int argc, char *argv[])
{
	bar();
	return 0;
}

Il est possible de voir que ce programme ne se terminera jamais. Compilons le:

gcc -g t.c -o testddlk

Et lançons le:

./testddlk

Le programme est comme prévu bloqué. Maintenant comment le suivre dans GDB ? en particulier car nous ne l’avons pas lancé préfixé par celui-ci.

La première étape est de récupérer le PID du programme cible. Le plus simple est de reposer sur la commande :

ps ux

On a par exemple la sortie suivante:

USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
jbbesna+ 13506 0.0 0.0 145020 2296 ? S 09:32 0:00 sshd: jbbesnard@pts/0
jbbesna+ 13507 0.0 0.0 145828 6940 pts/0 Ss 09:32 0:00 -zsh
jbbesna+ 13591 0.0 0.0 145020 2300 ? S 09:32 0:00 sshd: jbbesnard@pts/1
jbbesna+ 13592 0.0 0.0 142792 5796 pts/1 Ss 09:32 0:00 -zsh
jbbesna+ 13753 89.0 0.0 4156 344 pts/0 R+ 09:34 0:03 ./testddlk
jbbesna+ 13758 0.0 0.0 151056 1816 pts/1 R+ 09:34 0:00 ps ux

On trouve alors facilement la commande ./testddlk et son PID : 13753. On peut donc attacher GDB à ce programme:

# gdb  PROG      PID
  gdb ./testddlk 13753

Une approche alternative est de lancer GDB:

gdb

Et d’utiliser la commande « attach PID » dans l’invite de commande :

(gdb) attach 13753

Dans les deux cas vous obtiendrez la sortie suivante :

Reading symbols from /home/jbbesnard/gdb/testddlk…done.
Attaching to program: /home/jbbesnard/gdb/./testddlk, process 13753
Reading symbols from /lib64/libc.so.6…(no debugging symbols found)…done.
Loaded symbols for /lib64/libc.so.6
Reading symbols from /lib64/ld-linux-x86-64.so.2…(no debugging symbols found)…done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
bar () at ./t.c:3
3 while(1){}
Missing separate debuginfos, use: debuginfo-install glibc-2.17-157.el7_3.1.x86_64

Notez que le programme cible est interrompu, et qu’il se troube bien à la ligne du deadlock. Si vous quitez GDB (CTRL+D) le programme va reprendre son éxécution normale. Une fois attaché dans GDB il est possible de se détacher sans relancer GDB (si par exemple vous suivez de multiples processus).

Pour ce faire on utilise la commande suivante dans l’invite de commande de GDB:

(gdb) detach

Explorer l’état d’un programme

Dans les exemples suivants nous allons considérer le code suivant:

#include <omp.h>
#include <stdio.h>

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

int mul(int a, int b)
{
	return a*b;
}

int foo( int a , int b )
{
	return add( a, b ) + mul( a , b );
}

int main(int argc, char *argv[])
{
	int a, b;
	a = 8;
	b = 9;
	#pragma omp parallel
	{
		printf("foo : %d\n", foo( a , b ) );
	}

	return 0;
}

Ce code est un code OpenMP, si vous n’êtes pas trop familier avec ce modèle de programmation, sachez juste que dans la section avec des acolades de multiples threads seront créés. Cela s’ignifie que plusieurs fils d’éxécution vont traiter le code du prinf et donc écrire sur la sortie standard.

Pour compiler ce code, nous mettons les symboles de débug -g et ajoutons le support OpenMP -fopenmp:

gcc -g -fopenmp p.c -o par

Pour vérifier nos dires lançons ce code:

foo : 89
foo : 89
foo : 89
foo : 89
foo : 89
foo : 89
foo : 89
foo : 89

On retrouve huit fois la sortie, cela correspond aux huit coeurs de notre machine de test.

Les points d’arrêt

Notez avant toute chose que GDB s’arrête si le programme rencontre un signal (par exemple une erreur de segmentation). Il est donc possible de l’éxécuter directement pour se rendre au lieu de l’erreur sans avoir recours aux breakpoints.

Maintenant, considérons que nous voulions explorer l’état du programme dans la fonction add. Pour ce faire, il faut placer un breakpoint ou point d’arrêt. Il forcera le programme a s’interrompre quant il entrera dans la fonction add.

On lance le programme avec GDB:

gdb ./par

On place alors un point d’arrêt sur add :

(gdb) break add
Breakpoint 1 at 0x4006c7: file p.c, line 6.

On remarque alors que GDB confirme la position de ce point d’arrêt, indiquant même la ligne source.

Il est maintenant temps de lancer le programme avec la commande run aussi abrégeable en r

(gdb) run

GDB comme attendu va alors arrêter le programme dans add:

[Switching to Thread 0x7ffff45d3700 (LWP 14196)]
Breakpoint 1, add (a=8, b=9) at p.c:6
6 return a + b;
(gdb)

Le programme cible est alors interrompu au point indiqué, p.c à la ligne 6 (noté p.c:6) dans la fonction add. GDB affiche également la ligne source.

Il est possible de lister les breakpoint actif avec la commande suivante:

(gdb) info breakpoint
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004006c7 in add at p.c:6
	breakpoint already hit 1 time

Noter que cette commande peut être racourcie comme de nombreuse commandes GDB:

(gdb) i b

On peut supprimer un breakpoint avec delete:

(gdb) delete 1
(gdb) i b
No breakpoints or watchpoints.

On peut temporairement désactiver un breakpoint (remarquer le changement de Enb) :

(gdb) info b
Num     Type           Disp Enb Address            What
3       breakpoint     keep y   0x00000000004006c7 in add at p.c:6
(gdb) disable 3
(gdb) info b
Num     Type           Disp Enb Address            What
3       breakpoint     keep n   0x00000000004006c7 in add at p.c:6

Et par symétrie, le réactiver:

(gdb) enable 3
(gdb) i b
Num     Type           Disp Enb Address            What
3       breakpoint     keep y   0x00000000004006c7 in add at p.c:6

Enfin, disable et enable sans paramètre désactivent/activent tout les points d’arrêt.

Enfin on peut continuer l’éxécution normale du programme avec :

(gdb) continue
Continuing.

À partir de maintenant, utilisez:

(gdb) c
Continuing.

La pile d’appel

Une fois arrêtés dans add, il est possible d’afficher la pile d’appel (les fonction appelées) qui nous ont menées à cet appel à add. C’est l’une des command les plus importante pour le débogage.

Générons alors la pile d’appel:

(gdb) backtrace
#0  add (a=8, b=9) at p.c:6
#1  0x0000000000400702 in foo (a=8, b=9) at p.c:18
#2  0x00000000004007a1 in main._omp_fn.0 () at p.c:30
#3  0x00007ffff7bcd435 in ?? () from /lib64/libgomp.so.1
#4  0x00007ffff79a2dc5 in start_thread () from /lib64/libpthread.so.0
#5  0x00007ffff76d173d in clone () from /lib64/libc.so.6

À l’avenir utilisez la version courte de cet appel:

(gdb) bt

On observe que add a été appelé de la fonction foo qui a elle même été appelée depuis le main (avec la complexité supplémentaire d’un contexte OpenMP).

On retrouve pour chaque fonction de la pile d’appel les informations suivantes:

  • #1 : Le numéro de frame
  • 0x0000000000400702 : Addresse du pointeur programme
  • foo : le nom de la fonction
  • (a=8, b=9) : les paramètres de la fonction
  • at p.c:18 : la position de la fonction dans le code
  • from /lib64/libpthread.so.0 : la bibliothèque contenant le symbole (quand applicable)

La frame

Une frame est une instance d’appel de fonction dans la pile d’appel, est est décrite par un identifiant dans le backtrace. Par exmple foo dans l’exemple précédent est à la frame #1. Il est possible de monter dans la pile d’appel avec la commande suivante:

(gdb) up
#1  0x0000000000400702 in foo (a=8, b=9) at p.c:18
18		return add( a, b ) + mul( a , b );

Et bien sûr de descendre:

(gdb) down
#0  add (a=8, b=9) at p.c:6
6		return a + b;

Ces commandes étant racourcissable en up et do.

Il est également possible de se rendre directement à une frame avec son numéro:

(gdb) frame 2
#2  0x00000000004007a1 in main._omp_fn.0 () at p.c:30
30			printf("foo : %d\n", foo( a , b ) );

À partir de maintenant vous utiliserez fr en lieu et place de frame

Il est possible de récupérer les informations de la frame courante avec :

(gdb) info frame
Stack level 0, frame at 0x7ffff45d2e20:
 rip = 0x4006cf in add (p.c:7); saved rip 0x400702
 called by frame at 0x7ffff45d2e40
 source language c.
 Arglist at 0x7ffff45d2e10, args: a=8, b=9
 Locals at 0x7ffff45d2e10, Previous frame's sp is 0x7ffff45d2e20
 Saved registers:
  rbp at 0x7ffff45d2e10, rip at 0x7ffff45d2e18

On trouve dans cette sortie de nombreuses informations, avec par exemple les arguments de la fonction. Notez que l’on peut lister les arguments avec :

(gdb) info args
a = 8
b = 9

Et l’ensemble des variables locales avec:

(gdb) fr 2
#2  0x00000000004007a1 in main._omp_fn.0 () at p.c:30
30			printf("foo : %d\n", foo( a , b ) );
(gdb) info locals
a = 8
b = 9

Faire avancer le programme

Quand vous lancez GDB, il faut faire « run » (ou « r ») pour que le programme commence. Si vous ne plaçez aucun breakpoint, le programme va se terminer. Faire « run » à nouveau le lancera encore une fois. Ensuite, quand vous souhaitez quitter un breakpoint, faites « continues » ou « c ». Le programme continue alors son éxécution.

Il est également possible d’avancer ligne de code par ligne de code avec « step » ou « s », et enfin d’une seule instruction avec « si ».

Afficher les sources

Dans GDB il est possible d’afficher les lignes sources proche du point d’arrêt. Pour cela on utilise:

(gdb) list
1	#include <omp.h>
2	#include <stdio.h>
3
4	int add( int a ,  int b )
5	{
6		return a + b;
7	}
8
9
10	int mul(int a, int b)

Chaque nouvel appel à list affichera la suite du code.

Pour « remonter » utilisez:

(gdb) list -

Inspecter les threads

Notre petit exemple crée de multiple threads, il y a donc plusieurs instance de pile dans le programme que nous avons débogué jusqu’à maintenant. Les commandes précédentes se sont donc appliquées à un seul thread (flot d’éxécution), le choix du thread étant celui du premier entrant dans le breakpoint.

Il est possible de lister les threads avec:

(gdb) info thread
  Id   Target Id         Frame
  8    Thread 0x7ffff45d3700 (LWP 16038) "par" add (a=8, b=9) at p.c:6
* 7    Thread 0x7ffff4dd4700 (LWP 16037) "par" add (a=8, b=9) at p.c:6
  6    Thread 0x7ffff55d5700 (LWP 16036) "par" add (a=8, b=9) at p.c:6
  5    Thread 0x7ffff5dd6700 (LWP 16035) "par" add (a=8, b=9) at p.c:6
  4    Thread 0x7ffff65d7700 (LWP 16034) "par" add (a=8, b=9) at p.c:6
  3    Thread 0x7ffff6dd8700 (LWP 16033) "par" add (a=8, b=9) at p.c:6
  2    Thread 0x7ffff75d9700 (LWP 16032) "par" add (a=8, b=9) at p.c:6
  1    Thread 0x7ffff7fe27c0 (LWP 16031) "par" add (a=8, b=9) at p.c:6

Notez comment le thread actuellement sélectionné est indiqué par une astérisque (*). De plus il est important d’observer que la fonction courante, ses arguments et sa position sont indiquée dans cet appel. Cela le rend très pratique car il permet de voir en une commande la situation de touts les threads. Ici, ils sont tous dans la fonction add.

Tout comme les frames, il est possible de changer de thread :

(gdb) thread 1
[Switching to thread 1 (Thread 0x7ffff7fe27c0 (LWP 16031))]
#0  add (a=8, b=9) at p.c:6
6		return a + b;

Vous pouvez également utiliser la syntaxe courte thr. Une fois ce changement effectué, vous pouvez utilisez l’ensemble des commandes pour inspecter son contexte.

Il est possible de nommer les threads en utilisant :

(gdb) thread name BUG
(gdb) info thread
  Id   Target Id         Frame
  8    Thread 0x7ffff45d3700 (LWP 30206) "par" add (a=8, b=9) at p.c:6
  7    Thread 0x7ffff4dd4700 (LWP 30205) "par" add (a=8, b=9) at p.c:6
  6    Thread 0x7ffff55d5700 (LWP 30204) "par" add (a=8, b=9) at p.c:6
* 5    Thread 0x7ffff5dd6700 (LWP 30203) "BUG" add (a=8, b=9) at p.c:6
  4    Thread 0x7ffff65d7700 (LWP 30202) "par" add (a=8, b=9) at p.c:6
  3    Thread 0x7ffff6dd8700 (LWP 30201) "par" add (a=8, b=9) at p.c:6
  2    Thread 0x7ffff75d9700 (LWP 30200) "par" add (a=8, b=9) at p.c:6
  1    Thread 0x7ffff7fe27c0 (LWP 30196) "par" add (a=8, b=9) at p.c:6

Ce qui est très pratique pour s’y retrouver parfois. Enfin il est possible d’appliquer une commande à plusieurs threads à la fois :

(gdb) thread apply 1-4 print a

Thread 1 (Thread 0x7ffff7fe27c0 (LWP 30196)):
$3 = 8

Thread 2 (Thread 0x7ffff75d9700 (LWP 30200)):
$4 = 8

Thread 3 (Thread 0x7ffff6dd8700 (LWP 30201)):
$5 = 8

Thread 4 (Thread 0x7ffff65d7700 (LWP 30202)):
$6 = 8

De plus passer « all » permet d’invoquer une commande sur tous les threads, par exemple :

(gdb) thread apply all bt

Inspecter les variables

A tout moment dans GDB il est possible d’explorer le contenu de la mémoire. Pour cela, on utilise généralement la fonction print. Cette fonction peut s’utilisez très simplement:

(gdb) print a
$1 = 8

À partir de maintenant utilisez la version courte p. Print peut effectuer de nombreuses manipulation sur la sortie, de plus les variables passées en paramètre peuvent être altérées tout comme en C. Pour illustrer cela trouvons l’addresse de a:

(gdb) print &a
$2 = (int *) 0x7fffffffe11c

Et maintenant affichons le contenu à cette addresse comme un int:

(gdb) p *((int*)0x7fffffffe11c)
$3 = 8

Il est de plus tout à fait possible de faire de l’arithmétique de pointeur. Par exemple en considérant la manière dont les variables sont rangées sur la pile, b est stocké juste avant a, vérifions le:

p ((int*)0x7fffffffe11c)[-1]
$10 = 9

On a bien &b = &a – 4 , avec 4 la taille en octed d’un entier:

(gdb) p &b
$11 = (int *) 0x7fffffffe118

Si nous considérons un code trivial avec un tableau:

#include <stdio.h>

int main(int argc, char *argv[])
{
	int *a = NULL;
	int array[4] = {1,2,3,4};
	*a = 9;
	return 0;
}

On peut afficher le contenu de ce tableau dans GDB:

Directement:

(gdb) p array
$1 = {1, 2, 3, 4}

En utilisant l’opérateur de longueur:

(gdb) p array[0]@4
$2 = {1, 2, 3, 4}

Ou bien via un cast:

(gdb) p *((int[4]*) &array[0])
$9 = {1, 2, 3, 4}

Il y a également des modifieurs de format, par exemple « x » pour hexadécimal (voir ici pour la liste):

(gdb) p/x array[0]@4
$7 = {0x1, 0x2, 0x3, 0x4}

Inspecter la mémoire

En reprenant notre exemple précédent, il est possible d’explorer la mémoire, par exemple pour afficher la mémoire à l’endroit du tableau sur 4 entiers :

(gdb) x/4x &array[0]
0x7fffffffe170:	0x00000001	0x00000002	0x00000003	0x00000004

Les arguments de format étant pris en compte:

(gdb) x/4d &array[0]
0x7fffffffe170:	1	2	3	4

Par défaut, l’offset est de 4 octets, un entier, si vous voullez changer pour un byte tout en affichant maintenant 16 bytes (ou 4 * 4 entiers de 4 bytes) :

(gdb) x/16db &array[0]
0x7fffffffe170:	1	0	0	0	2	0	0	0
0x7fffffffe178:	3	0	0	0	4	0	0	0

Voir ici pour les différentes options.

Conclusion

Dans ce tutorial vous avez apris à utliser GDB, à le lancer ou bien à vous attacher au processus cible. Ensuite, nous avons couvert les différentes commandes de base. Dans un tuturial plus avancé nous couvrirons plus en détails les watchpoints et la possibilité de placer des commandes sur les breakpoints.