GNU/Linux >> Tutoriels Linux >  >> Linux

Un tutoriel pratique pour utiliser le débogueur de projet GNU

Si vous êtes programmeur et que vous souhaitez intégrer une certaine fonctionnalité dans votre logiciel, vous commencez par réfléchir à des moyens de l'implémenter, comme écrire une méthode, définir une classe ou créer de nouveaux types de données. Ensuite, vous écrivez l'implémentation dans un langage que le compilateur ou l'interpréteur peut comprendre. Mais que se passe-t-il si le compilateur ou l'interpréteur ne comprend pas les instructions telles que vous les aviez en tête, même si vous êtes sûr d'avoir tout fait correctement ? Que se passe-t-il si le logiciel fonctionne bien la plupart du temps mais provoque des bogues dans certaines circonstances ? Dans ces cas là, il faut savoir utiliser correctement un débogueur pour trouver la source de vos soucis.

Le débogueur de projet GNU (GDB) est un outil puissant pour trouver des bogues dans les programmes. Il vous aide à découvrir la raison d'une erreur ou d'un plantage en suivant ce qui se passe à l'intérieur du programme pendant l'exécution.

Cet article est un tutoriel pratique sur l'utilisation de base de GDB. Pour suivre les exemples, ouvrez la ligne de commande et clonez ce référentiel :

git clone https://github.com/hANSIc99/core_dump_example.git 

Raccourcis

Plus de ressources Linux

  • Aide-mémoire des commandes Linux
  • Aide-mémoire des commandes Linux avancées
  • Cours en ligne gratuit :Présentation technique de RHEL
  • Aide-mémoire sur le réseau Linux
  • Aide-mémoire SELinux
  • Aide-mémoire sur les commandes courantes de Linux
  • Que sont les conteneurs Linux ?
  • Nos derniers articles Linux

Chaque commande dans GDB peut être raccourcie. Par exemple, info break , qui affiche les points d'arrêt définis, peut être raccourci en i break . Vous verrez peut-être ces abréviations ailleurs, mais dans cet article, j'écrirai la commande entière afin qu'il soit clair quelle fonction est utilisée.

Paramètres de ligne de commande

Vous pouvez attacher GDB à chaque exécutable. Accédez au référentiel que vous avez cloné et compilez-le en exécutant make . Vous devriez maintenant avoir un exécutable appelé coredump . (Voir mon article sur Créer et déboguer des fichiers de vidage Linux pour plus d'informations..

Pour attacher GDB à l'exécutable, tapez :gdb coredump .

Votre sortie devrait ressembler à ceci :

Il indique qu'aucun symbole de débogage n'a été trouvé.

Les informations de débogage font partie du fichier objet (l'exécutable) et incluent les types de données, les signatures de fonction et la relation entre le code source et l'opcode. À ce stade, vous avez deux options :

  • Poursuivre le débogage de l'assembly (voir "Débogage sans symboles" ci-dessous)
  • Compiler avec les informations de débogage en utilisant les informations de la section suivante

Compiler avec les informations de débogage

Pour inclure des informations de débogage dans le fichier binaire, vous devez le recompiler. Ouvrez le Makefile et supprimez le hashtag (# ) à partir de la ligne 9 :

CFLAGS =-Wall -Werror -std=c++11 -g 

Le g L'option indique au compilateur d'inclure les informations de débogage. Exécutez make clean suivi de make et invoquez à nouveau GDB. Vous devriez obtenir ce résultat et vous pouvez commencer à déboguer le code :

Les informations de débogage supplémentaires augmenteront la taille de l'exécutable. Dans ce cas, il augmente l'exécutable de 2,5 fois (de 26 088 octets à 65 480 octets).

Démarrez le programme avec le -c1 basculez en tapant run -c1 . Le programme démarrera et plantera lorsqu'il atteindra State_4 :

Vous pouvez récupérer des informations supplémentaires sur le programme. La commande info source fournit des informations sur le fichier en cours :

  • 101 lignes
  • Langage :C++
  • Compilateur (version, réglage, architecture, indicateur de débogage, norme de langage)
  • Format de débogage :DWARF 2
  • Aucune information de macro de préprocesseur disponible (lorsqu'elles sont compilées avec GCC, les macros ne sont disponibles que lorsqu'elles sont compilées avec -g3 drapeau).

La commande info shared imprime une liste de bibliothèques dynamiques avec leurs adresses dans l'espace d'adressage virtuel chargé au démarrage afin que le programme s'exécute :

Si vous voulez en savoir plus sur la gestion des bibliothèques sous Linux, consultez mon article Comment gérer les bibliothèques dynamiques et statiques sous Linux .

Déboguer le programme

Vous avez peut-être remarqué que vous pouvez démarrer le programme dans GDB avec le run commande. Le run La commande accepte les arguments de ligne de commande comme vous le feriez pour démarrer le programme à partir de la console. Le -c1 switch fera planter le programme à l'étape 4. Pour exécuter le programme depuis le début, vous n'avez pas besoin de quitter GDB; utilisez simplement le run commander à nouveau. Sans le -c1 commutateur, le programme exécute une boucle infinie. Il faudrait l'arrêter avec Ctrl+C .

Vous pouvez également exécuter un programme pas à pas. En C/C++, le point d'entrée est le main une fonction. Utilisez la commande list main pour ouvrir la partie du code source qui affiche le main fonction :

Le main la fonction est à la ligne 33, alors ajoutez-y un point d'arrêt en tapant break 33 :

Exécutez le programme en tapant run . Comme prévu, le programme s'arrête au main une fonction. Tapez layout src pour afficher le code source en parallèle :

Vous êtes maintenant en mode interface utilisateur texte (TUI) de GDB. Utilisez les touches fléchées Haut et Bas pour faire défiler le code source.

GDB met en surbrillance la ligne à exécuter. En tapant next (n), vous pouvez exécuter les commandes ligne par ligne. GBD exécute la dernière commande si vous n'en spécifiez pas une nouvelle. Pour parcourir le code, appuyez simplement sur Entrée clé.

De temps en temps, vous remarquerez que la sortie de TUI est un peu corrompue :

Si cela se produit, appuyez sur Ctrl+L pour réinitialiser l'écran.

Utilisez Ctrl+X+A pour entrer et sortir du mode TUI à volonté. Vous pouvez trouver d'autres raccourcis clavier dans le manuel.

Pour quitter GDB, tapez simplement quit .

Points de surveillance

Le cœur de cet exemple de programme consiste en une machine à états fonctionnant dans une boucle infinie. La variable n_state est une simple énumération qui détermine l'état actuel :

while(true){
        switch(n_state){
        case State_1 :
                std::cout <<"State_1 atteint" <                n_state =State_2 ;
                break;
        case State_2:
                std::cout <<"State_2 atteint" <                n_state =State_3;
         ;
       
        (.....)
       
        }
}

Vous voulez arrêter le programme lorsque n_state est mis à la valeur State_5 . Pour ce faire, arrêtez le programme au main fonction et définissez un point de surveillance pour n_state :

watch n_state == State_5 

La définition de points de surveillance avec le nom de la variable ne fonctionne que si la variable souhaitée est disponible dans le contexte actuel.

Lorsque vous continuez l'exécution du programme en tapant continue , vous devriez obtenir une sortie comme :

Si vous continuez l'exécution, GDB s'arrêtera lorsque l'expression du point de surveillance sera évaluée à false :

Vous pouvez spécifier des points de surveillance pour les modifications de valeurs générales, des valeurs spécifiques et un accès en lecture ou en écriture.

Modification des points d'arrêt et des points de surveillance

Tapez info watchpoints pour imprimer une liste des points de surveillance précédemment définis :

Supprimer les points d'arrêt et les points de surveillance

Comme vous pouvez le voir, les points de surveillance sont des nombres. Pour supprimer un point de vue spécifique, tapez delete suivi du numéro du point de surveillance. Par exemple, mon point de vue a le numéro 2; pour supprimer ce point de surveillance, saisissez delete 2 .

Attention : Si vous utilisez delete sans spécifier de nombre, tous les points de surveillance et les points d'arrêt seront supprimés.

Il en va de même pour les points d'arrêt. Dans la capture d'écran ci-dessous, j'ai ajouté plusieurs points d'arrêt et en ai imprimé une liste en tapant info breakpoint :

Pour supprimer un seul point d'arrêt, tapez delete suivi de son numéro. Vous pouvez également supprimer un point d'arrêt en spécifiant son numéro de ligne. Par exemple, la commande clear 78 supprimera le point d'arrêt numéro 7, qui est défini à la ligne 78.

Désactiver ou activer les points d'arrêt et les points de surveillance

Au lieu de supprimer un point d'arrêt ou un point de surveillance, vous pouvez le désactiver en tapant disable suivi de son numéro. Dans ce qui suit, les points d'arrêt 3 et 4 sont désactivés et sont marqués d'un signe moins dans la fenêtre de code :

Il est également possible de modifier une plage de points d'arrêt ou de points de surveillance en tapant quelque chose comme disable 2 - 4 . Si vous souhaitez réactiver les points, tapez enable suivis de leurs numéros.

Points d'arrêt conditionnels

Tout d'abord, supprimez tous les points d'arrêt et points de surveillance en tapant delete . Vous voulez toujours que le programme s'arrête au main fonction, mais au lieu de spécifier un numéro de ligne, ajoutez un point d'arrêt en nommant directement la fonction. Tapez break main pour ajouter un point d'arrêt au main fonction.

Tapez run pour démarrer l'exécution depuis le début, et le programme s'arrêtera au main fonction.

Le main la fonction inclut la variable n_state_3_count , qui est incrémenté lorsque la machine d'état atteint l'état 3.

Pour ajouter un point d'arrêt conditionnel basé sur la valeur de n_state_3_count saisissez :

break 54 if n_state_3_count == 3 

Continuez l'exécution. Le programme exécutera la machine d'état trois fois avant de s'arrêter à la ligne 54. Pour vérifier la valeur de n_state_3_count , saisissez :

print n_state_3_count 

Rendre les points d'arrêt conditionnels

Il est également possible de rendre conditionnel un point d'arrêt existant. Supprimez le point d'arrêt récemment ajouté avec clear 54 , et ajoutez un simple point d'arrêt en tapant break 54 . Vous pouvez rendre ce point d'arrêt conditionnel en tapant :

condition 3 n_state_3_count == 9 

Le 3 fait référence au numéro du point d'arrêt.

Définir des points d'arrêt dans d'autres fichiers source

Si vous avez un programme composé de plusieurs fichiers source, vous pouvez définir des points d'arrêt en spécifiant le nom du fichier avant le numéro de ligne, par exemple, break main.cpp:54 .

Points de capture

En plus des points d'arrêt et des points de surveillance, vous pouvez également définir des points d'interception. Les points d'arrêt s'appliquent aux événements du programme tels que l'exécution d'appels système, le chargement de bibliothèques partagées ou la génération d'exceptions.

Pour attraper le write syscall, qui est utilisé pour écrire dans STDOUT, entrez :

catch syscall write 

Chaque fois que le programme écrit dans la sortie de la console, GDB interrompt l'exécution.

Dans le manuel, vous pouvez trouver un chapitre entier couvrant les points de rupture, de surveillance et d'accroche.

Évaluer et manipuler des symboles

L'impression des valeurs des variables se fait avec le print commande. La syntaxe générale est print <expression> <value> . La valeur d'une variable peut être modifiée en tapant :

set variable <variable-name> <new-value>. 

Dans la capture d'écran ci-dessous, j'ai donné la variable n_state_3_count la valeur 123 .

Le /x expression imprime la valeur en hexadécimal ; avec le & opérateur, vous pouvez imprimer l'adresse dans l'espace d'adressage virtuel.

Si vous n'êtes pas sûr du type de données d'un certain symbole, vous pouvez le trouver avec whatis :

Si vous voulez lister toutes les variables qui sont disponibles dans la portée du main fonction, tapez info scope main :

Le DW_OP_fbreg les valeurs font référence au décalage de pile basé sur la sous-routine actuelle.

Alternativement, si vous êtes déjà dans une fonction et que vous souhaitez lister toutes les variables sur le cadre de pile actuel, vous pouvez utiliser info locals :

Consultez le manuel pour en savoir plus sur l'examen des symboles.

Attacher à un processus en cours d'exécution

La commande gdb attach <process-id> vous permet de vous attacher à un processus déjà en cours d'exécution en spécifiant l'ID de processus (PID). Heureusement, le coredump programme imprime son PID actuel à l'écran, vous n'avez donc pas à le trouver manuellement avec ps ou top.

Démarrez une instance de l'application coredump :

./coredump 

Le système d'exploitation donne le PID 2849 . Ouvrez une fenêtre de console séparée, accédez au répertoire source de l'application coredump et attachez GDB :

gdb attach 2849 

GDB arrête immédiatement l'exécution lorsque vous l'attachez. Tapez layout src et backtrace pour examiner la pile des appels :

La sortie montre le processus interrompu lors de l'exécution de std::this_thread::sleep_for<...>(...) fonction appelée à la ligne 92 de main.cpp .

Dès que vous quittez GDB, le processus se poursuit.

Vous pouvez trouver plus d'informations sur l'attachement à un processus en cours d'exécution dans le manuel GDB.

Parcourir la pile

Revenir au programme en utilisant up deux fois pour monter dans la pile vers main.cpp :

Habituellement, le compilateur créera un sous-programme pour chaque fonction ou méthode. Chaque sous-programme a son propre cadre de pile, donc se déplacer vers le haut dans le cadre de la pile signifie se déplacer vers le haut dans la pile des appels.

Vous pouvez en savoir plus sur l'évaluation de la pile dans le manuel.

Spécifier les fichiers sources

Lors de l'attachement à un processus déjà en cours d'exécution, GDB recherchera les fichiers source dans le répertoire de travail actuel. Alternativement, vous pouvez spécifier les répertoires source manuellement avec le directory commande.

Évaluer les fichiers de vidage

Lisez Créer et déboguer des fichiers de vidage Linux pour plus d'informations sur ce sujet.

TL; DR :

  1. Je suppose que vous travaillez avec une version récente de Fedora
  2. Appelez coredump avec le commutateur c1 :coredump -c1

  3. Chargez le dernier fichier de vidage avec GDB :coredumpctl debug
  4. Ouvrez le mode TUI et saisissez layout src

La sortie de backtrace montre que le plantage s'est produit à cinq cadres de pile de main.cpp . Entrez pour accéder directement à la ligne de code défectueuse dans main.cpp :

Un regard sur le code source montre que le programme a tenté de libérer un pointeur qui n'a pas été renvoyé par une fonction de gestion de la mémoire. Cela entraîne un comportement indéfini et a provoqué le SIGABRT .

Déboguer sans symboles

S'il n'y a pas de sources disponibles, les choses deviennent très difficiles. J'ai eu ma première expérience avec cela en essayant de résoudre des problèmes de rétro-ingénierie. Il est également utile d'avoir une certaine connaissance du langage d'assemblage.

Découvrez comment cela fonctionne avec cet exemple.

Allez dans le répertoire source, ouvrez le Makefile , et modifiez la ligne 9 comme suit :

CFLAGS =-Wall -Werror -std=c++11 #-g 

Pour recompiler le programme, exécutez make clean suivi de make et démarrez GDB. Le programme n'a plus de symboles de débogage pour guider le code source.

La commande info file révèle les zones mémoire et le point d'entrée du binaire :

Le point d'entrée correspond au début du .text zone, qui contient l'opcode réel. Pour ajouter un point d'arrêt au point d'entrée, tapez break *0x401110 puis lancez l'exécution en tapant run :

Pour mettre en place un point d'arrêt à une certaine adresse, précisez-le avec l'opérateur de déréférencement * .

Choisir la saveur du désassembleur

Avant de creuser plus profondément dans l'assemblage, vous pouvez choisir la saveur d'assemblage à utiliser. La valeur par défaut de GDB est AT&T, mais je préfère la syntaxe Intel. Changez-le avec :

set disassembly-flavor intel 

Ouvrez maintenant l'assemblage et enregistrez la fenêtre en tapant layout asm et layout reg . Vous devriez maintenant voir une sortie comme celle-ci :

Enregistrer les fichiers de configuration

Bien que vous ayez déjà entré de nombreuses commandes, vous n'avez pas réellement commencé le débogage. Si vous déboguez fortement une application ou essayez de résoudre un problème de rétro-ingénierie, il peut être utile d'enregistrer vos paramètres spécifiques à GDB dans un fichier.

Le fichier de configuration gdbinit dans le référentiel GitHub de ce projet contient les commandes récemment utilisées :

set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg

Le set write on La commande vous permet de modifier le binaire lors de l'exécution.

Quittez GDB et rouvrez-le avec le fichier de configuration : gdb -x gdbinit coredump .

Lire les instructions

Avec le c2 commutateur appliqué, le programme plantera. Le programme s'arrête à la fonction d'entrée, vous devez donc écrire continue pour procéder à l'exécution :

L'idiv l'instruction effectue une division entière avec le dividende dans le RAX registre et le diviseur spécifié en argument. Le quotient est chargé dans le RAX registre, et le reste est chargé dans RDX .

Dans l'aperçu du registre, vous pouvez voir le RAX contient 5 , vous devez donc savoir quelle valeur est stockée sur la pile à la position RBP-0x4 .

Lire la mémoire

Pour lire le contenu de la mémoire brute, vous devez spécifier quelques paramètres de plus que pour lire les symboles. Lorsque vous faites défiler un peu vers le haut dans la sortie de l'assemblage, vous pouvez voir la division de la pile :

Vous êtes le plus intéressé par la valeur de rbp-0x4 car c'est la position où l'argument pour idiv est stocké. À partir de la capture d'écran, vous pouvez voir que la prochaine variable est située à rbp-0x8 , donc la variable à rbp-0x4 a une largeur de 4 octets.

Dans GDB, vous pouvez utiliser le x commande pour examiner tout contenu de la mémoire :

x/ n f u> addr>

Paramètres facultatifs :

  • n  :Le nombre de répétitions (par défaut :1) fait référence à la taille de l'unité
  • f :spécificateur de format, comme dans printf
  • u :Taille de l'unité
    • b :octets
    • h :demi-mots (2 octets)
    • w :mot (4 octets)(par défaut)
    • g :mot géant (8 octets)

Pour imprimer la valeur à rbp-0x4 , tapez x/u $rbp-4 :

Si vous gardez ce modèle à l'esprit, il est simple d'examiner la mémoire. Consultez la section de la mémoire d'examen dans le manuel.

Manipuler l'assemblage

L'exception arithmétique s'est produite dans la sous-routine zeroDivide() . Lorsque vous faites défiler un peu vers le haut avec la touche fléchée vers le haut, vous pouvez trouver ce modèle :

0x401211 <_Z10zeroDividev>              pousser   rbp
0x401212 <_Z10zeroDividev+1>            mov    rbp,rsp  

C'est ce qu'on appelle le prologue de la fonction :

  1. Le pointeur de base (rbp ) de la fonction appelante est stocké sur la pile
  2. La valeur du pointeur de pile (rsp ) est chargé dans le pointeur de base (rbp )

Ignorez complètement ce sous-programme. Vous pouvez vérifier la pile d'appels avec backtrace . Vous n'êtes qu'à un frame de pile devant votre main fonction, vous pouvez donc revenir à main avec un seul up :

Dans votre main fonction, vous pouvez trouver ce modèle :

0x401431      cmp    BYTE PTR [rbp-0x12],0x0
0x401435     je     0x40145f
0x401437     appeler   0x4010zeroDividev<_Z1>

Le sous-programme zeroDivide() est saisi uniquement lorsque jump equal (je) évalue à true . Vous pouvez facilement le remplacer par un jump-not-equal (jne) instruction, qui a l'opcode 0x75 (à condition d'être sur une architecture x86/64; les opcodes sont différents sur les autres architectures). Redémarrez le programme en tapant run . Lorsque le programme s'arrête à la fonction d'entrée, manipulez l'opcode en tapant :

set *(unsigned char*)0x401435 = 0x75 

Enfin, tapez continue . Le programme ignorera la sous-routine zeroDivide() et ne plantera plus.

Conclusion

Vous pouvez trouver GDB fonctionnant en arrière-plan dans de nombreux environnements de développement intégrés (IDE), y compris Qt Creator et l'extension Native Debug pour VSCodium.

Il est utile de savoir comment tirer parti des fonctionnalités de GDB. Habituellement, toutes les fonctions de GDB ne peuvent pas être utilisées à partir de l'EDI, vous bénéficiez donc d'une expérience dans l'utilisation de GDB à partir de la ligne de commande.


Linux
  1. 7 astuces pratiques pour utiliser la commande Linux wget

  2. Conseils Linux pour l'utilisation de GNU Screen

  3. 8 conseils pour la ligne de commande Linux

  4. Dépannage à l'aide du système de fichiers proc sous Linux

  5. 5 conseils pour le débogueur GNU

Tutoriel de commande Linux ss pour les débutants (8 exemples)

GalliumOS :la distribution Linux pour les Chromebooks

Le guide complet d'utilisation de ffmpeg sous Linux

Tutoriel sur l'utilisation de la commande Timeout sous Linux

Tutoriel sur l'utilisation de la dernière commande dans le terminal Linux

Les 20 meilleurs débogueurs Linux pour les ingénieurs logiciels modernes