Skip to content

Instantly share code, notes, and snippets.

@gnoirzox
Last active December 17, 2015 02:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save gnoirzox/5536259 to your computer and use it in GitHub Desktop.
Save gnoirzox/5536259 to your computer and use it in GitHub Desktop.
Article beta : Utilisation du débogueur GDB

Title: Utilisation du débogueur GDB Author: Simon ROUGER Date: 2013-05-09 Tags: gdb, gnu, débogueur, c

Introduction

GDB est un logiciel permettant de débusquer les bogues pouvant apparaitre lors de l'exécution d'un programme. Ce débogueur peut s'exécuter sur la plupart des systèmes de type Unix. Il est également compatible avec un grand nombre de langages tels que C, C++, Java. Il s'exécute aussi sur un ensemble d'architectures système (ARM, X86, X64, MIPS, Power-PC, SPARC, ect.) et est intégré au sein d'un grand nombre d'environnements de développement sous forme d'interface graphique (Code::Blocks, Xcode, Qt Creator ou encore Visual Studio).

Nous allons nous intéresser dans cet article à son utilisation sous forme textuelle. Nous verrons d'abord comment démarrer une première session, ensuite nous verrons les points d'arrêt (breakpoint, watchdog), nous parlerons également de la modification des variables, des registres et de l'utilisation de la pile.

Première session

Pour notre premier lancement du débogueur GDB, nous allons lancer une session de celui-ci. Pour lancer une session, nous devons avoir un programme à déboguer. Nous allons utiliser le programme ci-dessous (ce code source provient de l'article Linux software debugging with GDB) :

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

int wib(int n01, int n02) {
	int result,
		diff;
	
	diff 	= n01 - n02;
	result 	= n01 / diff;

	return result;
}

int main(int argc, char* argv[]) {
	int value,
		div,
		result,
		i,
		total;

	value 	= 10;
	div 	= 6;
	total	= 0;

	for(i = 0; i < 10; i++) {
		result = wib(value, div);
		total += result;

		div++;
		value--;
	}

	printf("%d wibed by %d equals %d\n", value, div, total);

	return 0;
}

Ce programme est censé calculer une valeur cumulative grâce à la fonction wib() au sein d'une boucle et imprimer le résultat.

Après avoir compilé ce code source avec l'argument ''-g'' pour permettre d'obtenir des informations de débogage utiles pour GDB, nous devons lancer le programme GDB.

$ gcc -g wib.c -o wib

Nous avons deux façons de lancer GDB avec un programme :

$ gdb wib

$ gdb
 (gdb) file wib

Après l'avoir lancé, nous pouvons démarrer la session de notre programme.

(gdb) run

Notre session est lancée et nous recevons une erreur de type SIGFPE :

Program received signal SIGFPE, Arithmetic exception.
0x80483ea in wib (no1=8, no2=8) at wib.c:9
7         result = no1 / diff;
(gdb) 

En regardant les signaux de type POSIX, nous voyons que SIGFPE correspond à une erreur de calcul arithmetique exécuté par le programme. De plus, le message spécifie la ligne où s'est produite l'erreur ainsi que les variables concernées. Ainsi, nous voulons savoir les valeurs des variables présentes sur la ligne 9 :

(gdb) print n01
$5 = 8
(gdb) print diff
$2 = 0

Nous comprenons alors que l'erreur arithmétique est due à une bête division par zéro exécutée par notre programme.

Maintenant, pour arrêter notre session GDB, nous pouvons appuyer sur les touches controle + d.

Nous venons de déboguer notre programme de manière simple au sein d'une session GDB. Dans d'autres cas, nous pouvons avoir recours à des points d'arrêt pour faciliter la résolution de plusieurs bogues au sein d'un même programme.

Les points d'arrêt

Le problème en exécutant directement le programme au sein de GDB est que le programme est lui même exécuté jusqu'à ce qu'il soit arrété. Ainsi, dans beaucoup de cas, ceci rend difficile la résolution d'un bogue offusqué, c'est pourquoi les points d'arrêt existent. Il y'a deux types de points d'arrêt: les breakpoints et les watchdogs. Nous allons voir ces deux types et dans quel but ils sont utiles.

Les breakpoints

Les breakpoints servent à stopper un programme à une instruction précise quand celui-ci est exécuté. Ceci a pour but de verifier l'état du programme à un instant T. La définition d'un breakpoint est très simple.

(gdb) break wib.c:25

On utilise la commande ''break'' suivie du fichier source et de la ligne où mettre le breakpoint (dans un programme simple comme le notre, ayant un seul fichier source, nous pouvons également définir seulement la ligne).

(gdb) break 25

Ainsi, en relançant la commande ''run'' au sein de GDB, notre programme a un point d'arrêt à l'instruction correspondant à la ligne 25 de notre code source.

Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:25
25          result = wib(value, div);

On peut donc mettre plusieurs breakpoints au sein de notre programme et décomposer l'exécution de celui-ci en étape. En utilisant la commande ''next'', on peut exécuter le programme en mode instruction par instruction (pas à pas). Ceci peut s'avérer particulièrement utile pour des programmes plus complexes que le notre. On peut alors vérifier de manière plus précise l'endroit dans notre code source qui est à l'origine d'un bogue qui peut s'avérer, à première vue, obscur.

Nous pouvons alors continuer l'exécution du programme grâce à la commande ''continue'' ou ''step'' pour s'arrêter au prochain breakpoint.

Il existe également un autre type de point d'arrêt, le conditional breakpoint. Ce type de breakpoint peut être plus utile dans des cas précis où nous avons besoin d'arrêter notre programme dans un cas conditionel. Nous pouvons avoir un conditional breakpoint de ce type :

(gdb) condition 1 value==div

Nous avons défini un conditional breakpoint à la place du breakpoint 1 défini précèdemment qui arrète le programme quand les variables div et value sont égales. Il existe une autre manière de mettre en place un conditional breakpoint, voici un exemple :

(gdb) break 25 if value==div

Pour finir sur les breakpoints, la commande ''info'' nous permet de lister l'ensemble des breakpoints mis au sein du programme exécuté.

(gdb) info breakpoint
Num Type           Disp Enb Address            What
1   breakpoint     keep y   0x0000000100000e7c in main at gdb.c:25
        stop only if value == di

Si vous voulez supprimer un breakpoint, il suffit d'utiliser la commande ''delete''.

(gdb) delete 1

Les watchdogs

Les watchdogs sont un autre type de breakpoint. Ils sont utilisés pour arrêter le programme quand une variable spécifiée est changée ou lue. Il existe trois types de watchdogs :

  • les ''watch'' qui arrètent le programme quand la variable spécifiée est changée ;
  • les ''rwatch'' qui arrètent le programme quand la variable spécifiée est lue ;
  • les ''awatch'' qui arrètent le programme quand la variable spécifiée est lue et/ou changée.

Ceci peuvent être donc très pratique pour la résolution de bogue, bien qu'ils engendrent une exécution plus lente du programme à déboguer.

Voici un exemple d'utilisation d'un watchdog de type ''watch'' :

(gdb) break main
Note: breakpoint 2 also set at pc 0x100000e3f.
Breakpoint 5 at 0x100000e3f: file gdb.c, line 18.

Nous mettons d'abord un breakpoint sur la zone d'exécution à inspecter.

(gdb) watch div == value
Hardware watchpoint 6: div == value

Nous mettons notre watchdog sur l'équivalence entre div et value.

(gdb) continue
Continuing.
Hardware watchpoint 2: div == value
Old value = 0
New value = 1
main (argc=1, argv=0xbffff954) at eg1.c:25
25        for(i = 0; i < 10; i++)

En continuant l'exécution de notre programme, nous voyons que les valeurs ont changés. Ceci s'avère utile dans d'autres cas plus complexes.

Aller plus loin

Nous venons de voir l'utilisation basique de GDB pour déboguer un programme de manière simple. Nous allons maintenant aborder d'autres sujets tels que la manipulation des registres et de la pile.

Les registres

Les registres sont des emplacements de mémoire au sein du processeur de notre ordinateur. ce sont les zones mémoire les plus performantes pour l'accès direct; ainsi, ce sont les zones privilégiées pour la manipulation de données. Il existe différents types de registres, ceux dédiés au stockage de données (stockage de nombres entiers ou de nombres flottants), stockage d'adresse mémoire et d'autres types existent mais ils ne seront pas abordés ici.

Grâce à GDB, nous pouvons visualiser les données et les adresses mémoires occupées par notre programme présentes au sein des registres.

Pour visualiser l'ensemble des registres et leur contenu, nous pouvons utiliser cette commande :

(gdb) info registers
rax            0x8      8
rbx            0x0      0
rcx            0x0      0
rdx            0x0      0
rsi            0x8      8
rdi            0x8      8
rbp            0x7fff5fbff9f0   0x7fff5fbff9f0
rsp            0x7fff5fbff9f0   0x7fff5fbff9f0
r8             0x0      0
r9             0x50     80
r10            0x7fff5fbfeca0   140734799801504
r11            0x246    582
r12            0x0      0
r13            0x0      0
r14            0x0      0
r15            0x0      0
rip            0x100000e1c      0x100000e1c <wib+28>
eflags         0x10246  66118
cs             0x2b     43
ss             0x0      0
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0

Nous pouvons donc voir les valeurs stockées au sein des différents registres, nous avons des entiers et des adresses mémoires. Ici les registres dédiés au stockage de nombres flottants ne sont pas affichés. Pour les afficher nous avons la commande suivante :

(gdb) info all-registers

il est également possible d'afficher seulement le contenu d'un registre spécifique sous forme d'entier, hexadécimale ou flottante :

(gdb) $eflags 
$11 = 66118
(gdb) print/x $eflags
$12 = 0x10246
(gdb) print/f $eflags
$13 = 9.26510519e-41

Pour finir sur les registres, il est également possible de modifier temporairement la valeur d'un registre.

(gdb) set $rbx = 8

Comme vous pouvez le voir, la manipulation des registres au sein de GDB peut s'avérer utile pour détecter et résoudre des bogues tels que des fuites mémoire.

La pile

La pile est une structure de données basée sur le principe du "dernier entré, premier servi" (appelé également LIFO), on peut donner comme exemple une pile de livres. La pile est implémentée nativement dans la plupart des architectures de processeur, c'est un registre qui enregistre l'adresse du dernier élément stocké en mémoire. Dans un langage comme le C, les algorithmes récursifs utilisent une pile appelée la pile d'exécution.

GDB se sert d'une pile pour stocker les informations à propos des paramètres et variables locales de la dernière fonction appelée au sein de notre programme. Ainsi, quand notre programme s'interrompt, nous pouvons avoir accès à ses informations grâce à la pile.

La pile de GDB est divisée en pièces appelées frames. Chaque frame est associée avec l'appel d'une fonction précise. Ainsi, à chaque appel de fonction au sein de notre programme exécuté, une frame est ajoutée au sein de la pile et un numéro lui est attribué par le débogueur. La sélection d'une frame se fait avec la commande du même nom :

(gdb) frame 1
#1  0x0000000100000e6c in main (argc=1, argv=0x7fff5fbffa60) at wib.c:23
23              result = wib(value, div);
(gdb) frame 0
#0  0x0000000100000e1c in wib (no1=8, no2=8) at wib.c:7
7           result  = no1/diff; 

Ceci nous permet donc de voir les étapes suivies pour les différents appels de fonction successifs avec l'état de chaque fonction grâce aux valeurs de leur paramètres et de leurs variables locales. Il est également possible de spécifier l'adresse mémoire de la frame plutôt que son numéro avec la commande ''frame''; pour sélectionner la frame suivante nous utilison la commande ''up'', et ''down'' pour la frame précèdente.

Pour avoir plus d'informations sur la frame séléctionnée, nous utilisons la commande ''info'' :

(gdb) info frame
Stack level 1, frame at 0x7fff5fbffa50:
 rip = 0x100000e6c in main (gdb.c:23); saved rip 0x7fff86b547e1
 caller of frame at 0x7fff5fbffa00
 source language c.
 Arglist at 0x7fff5fbffa48, args: argc=1, argv=0x7fff5fbffa60
 Locals at 0x7fff5fbffa48, Previous frame's sp is 0x7fff5fbffa50
 Saved registers:
  rsi at 0x7fff5fbffa30, rdi at 0x7fff5fbffa3c, rbp at 0x7fff5fbffa40, rip at 0x7fff5fbffa48

Nous pouvons voir différentes informations telles que les adresses pour les paramètres de fonction, les variables locales, l'adresse de la prochaine frame, celle de la précèdente ou encore les registres utilisés.

Il est également possible d'obtenir plus d'informations sur les arguments (paramètres de fonction), les variables locales ou encore les exceptions levées au sein de la frame séléctionnée grâce à ces différentes commandes :

(gdb) info args 
argc = 1
argv = (char **) 0x7fff5fbffa60

(gdb) info locals
value = 8
div = 8
i = 2
result = 4
total = 6

(gdb) info catch
Info catch not supported with this target/compiler combination.

Pour finir sur la pile, pour décomposer l'appel successif des différentes fonctions au sein de notre programme, nous utilisons une commande qui s'avère très utile dans des codes sources de beaucoup plus grande envergure que le notre. C'est la commande ''backtrace'' qui permet de tracer les étapes qui suivent l'appel d'une frame à une autre :

(gdb) backtrace
#0  0x0000000100000e1c in wib (no1=8, no2=8) at wib.c:7
#1  0x0000000100000e6c in main (argc=1, argv=0x7fff5fbffa60) at wib.c:23

Conclusion

Nous venons de voir l'utilisation du débogueur GDB de manière basique avec le lancement d'une session, la mise en place de points d'arrêt tel que les breakpoints ou les watchdogs, à une utilisation plus poussée telle que la manipulation des registres et de la pile au sein de l'environnement de GDB. D'autres éléments auraient pu être abordés tel que les tracepoints, la manipulation du code source avec le desassemblage du code machine exécuté, l'utilisation de GDB avec un autre langage que le C ou encore son utilisation à distance.

Pour pouvoir aborder ses sujets, la documentation officielle est disponible. Il existe également une 'cheatsheet' regroupant la plupart des commandes utiles de GDB.

Il existe d'autres débogueurs qui ont été influencés par GDB, nous avons par exemple LLDB qui est le débogueur de la suite LLVM qui est particulièrement utile en débogage de programmes concurrents, ou encore Valgrind qui est spécialisé pour déboguer les fuites mémoire.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment