Apprentissage du débogage GDB (1) : exécution en une seule étape et suivi des appels de fonction

avant-propos

En plus des bogues qui sont clairs en un coup d'œil, certaines méthodes de débogage sont nécessaires pour analyser ce qui ne va pas. Jusqu'à présent, nous n'avons qu'une seule méthode de débogage : assumez la cause de l'erreur en fonction du phénomène d'erreur lors de l'exécution du programme, puis insérez printf à une position appropriée dans le code, exécutez le programme et analysez le résultat d'impression, si le résultat est comme prévu. , cela prouve essentiellement Si vous assumez vous-même la cause de l'erreur, vous pouvez corriger le bogue vous-même. Si le résultat est différent de celui attendu, faites d'autres hypothèses et analysez en fonction du résultat.

Dans cet article, nous présentons un outil de débogage très puissant gdb, qui peut contrôler complètement le fonctionnement du programme, faisant du programme un jouet dans votre main, vous pouvez lui dire de partir, lui dire de s'arrêter, et vous pouvez tout voir les programmes dans le programme à tout moment. Etat interne, tel que la valeur de chaque variable, les paramètres passés à la fonction, la ligne de code en cours d'exécution, etc. Après avoir maîtrisé l'utilisation de gdb, les méthodes de débogage sont plus abondantes.

Mais il faut noter que même si les méthodes de débogage s'enrichissent, l'idée de base du débogage reste un cycle « d'analyse du phénomène → supposer la cause de l'erreur → générer un nouveau phénomène pour vérifier l'hypothèse ». vérifier des hypothèses nécessite une analyse et une réflexion très rigoureuses. Si vous abusez d'outils puissants et ignorez le processus d'analyse parce que vous avez un outil puissant en main, vous corrigerez souvent les symptômes mais ne corrigerez pas le bogue localement, ce qui entraînera la disparition d'un phénomène d'erreur mais le bug Il existe , et même plus le programme est modifié, plus il devient faux.

Cet article explique comment utiliser gdb pour déboguer des programmes à travers plusieurs exemples d'erreurs que les débutants sont susceptibles de commettre, et résume certaines commandes gdb couramment utilisées après chaque exemple.

1 Appels de fonction pas à pas et de traçage

      #include <stdio.h>
      int add_range(int low, int high)
      {
              int i, sum;
              for (i = low; i <= high; i++)
                    sum = sum + i;
              return sum;
      }
      int main(void)
      {
              int result[1000];
              result[0] = add_range(1, 10);
              result[1] = add_range(1, 100);
              printf("result[0]=%d\nresult[1]=%d\n", result[0],
              result[1]);
              return 0;
      }

La fonction add_range est additionnée de bas en haut. Dans la fonction principale, ajoutez d'abord de 1 à 10, enregistrez le résultat, puis ajoutez de 1 à 100, puis enregistrez le résultat. Les deux derniers résultats imprimés sont :

      result[0]=55
      result[1]=5105

Le premier résultat est correct, mais le deuxième résultat est évidemment incorrect [illustration]. Nous avons entendu l'histoire de Gauss lorsqu'il était enfant à l'école primaire. L'addition de 1 à 100 devrait donner 5050.

Un morceau de code, le résultat de la première exécution est correct, mais la seconde exécution est erronée. C'est un type de phénomène d'erreur très courant. Dans ce cas, d'une part, le code doit être suspecté, et d'autre part D'autre part, les données doivent être suspectées : la première fois et Le même morceau de code est exécuté la deuxième fois Si le code est faux, pourquoi le résultat de la première fois peut-il être juste ?

Il est probable que les données d'état pertinentes soient erronées lors de la deuxième exécution, et les données erronées conduisent à des résultats erronés. Avant de déboguer, les lecteurs devraient essayer de voir s'ils peuvent voir la cause de l'erreur en regardant simplement le code. Tant qu'ils ont appris une base solide dans les chapitres précédents, ils devraient être capables de le voir.

L'option -g doit être ajoutée lors de la compilation, et le fichier exécutable généré peut être débogué au niveau du code source avec gdb :

      $ gcc -g main.c -o main
      $ gdb main
      GNU gdb (GDB) 7.1-ubuntu
      Copyright (C) 2010 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 "i486-linux-gnu".
      For bug reporting instructions, please see:
      <http://www.gnu.org/software/gdb/bugs/>...
      Reading symbols from /home/akaedu/main...done.
      (gdb)

La fonction de l'option -g est d'ajouter des informations de code source au fichier exécutable , telles que quelle instruction machine dans le fichier exécutable correspond à la ligne du code source, mais elle n'intègre pas l'intégralité du fichier source dans le fichier exécutable, Ainsi, lors du débogage, vous devez vous assurer que gdb peut trouver le fichier source.

gdb fournit un environnement de ligne de commande de type shell, le ci-dessus (gdb) est l'invite, entrez help à cette invite pour afficher la catégorie de la commande :

      (gdb) help
      List of classes of commands:
      aliases -- Aliases of other commands
      breakpoints -- Making program stop at certain points
      data -- Examining data
      files -- Specifying and examining files
      internals -- Maintenance commands
      obscure -- Obscure features
      running -- Running the program
      stack -- Examining the stack
      status -- Status inquiries
      support -- Support facilities
      tracepoints -- Tracing of program execution without stopping the program
      user-defined -- User-defined commands
      Type "help" followed by a class name for a list of commands in that class.
      Type "help all" for the list of all commands.
      Type "help" followed by command name for full documentation.
      Type "apropos word" to search for commands related to "word".
      Command name abbreviations are allowed if unambiguous.

Vous pouvez également vérifier plus en détail quelles commandes appartiennent à une certaine catégorie, par exemple, vérifier quelles commandes sont disponibles dans la catégorie des fichiers :

      (gdb) help files
      Specifying and examining files.
      List of commands:
      add-symbol-file -- Load symbols from FILE
      add-symbol-file-from-memory -- Load the symbols out of memory from a dynamically loaded object file
      cd -- Set working directory to DIR for debugger and program being debugged
      core-file--Use FILE as core dump for examining memory and registers
      directory--Add directory DIR to beginning of search path for source files
      edit -- Edit specified file or function
      exec-file -- Use FILE as program for getting contents of pure memory
      file -- Use FILE as program to be debugged
      forward-search -- Search for regular expression (see regex(3)) from last line listed
      generate-core-file -- Save a core file with the current state of the debugged process
      list -- List specified function or line
      ...

Essayez maintenant d'utiliser la commande list pour lister le code source à partir de la première ligne :

      (gdb) list 1
      1      #include <stdio.h>
      2
      3      int add_range(int low, int high)
      4      {
      5                int i, sum;
      6                for (i = low; i <= high; i++)
      7                       sum = sum + i;
      8                return sum;
      9      }
      10

Seules 10 lignes sont répertoriées à la fois. Si vous souhaitez continuer à répertorier le code source à partir de la ligne 11, vous pouvez le saisir à nouveau.

      (gdb) list

Vous pouvez également appuyer sur Entrée sans rien taper, gdb fournit une fonction très pratique, appuyez directement sur Entrée à l'invite pour répéter la commande précédente.

      (gdb)(直接回车)
      11     int main(void)
      12     {
      13             int result[1000];
      14             result[0] = add_range(1, 10);
      15             result[1] = add_range(1, 100);
      16             printf("result[0]=%d\nresult[1]=%d\n", result[0],
                    result[1]);
      17             return 0;
      18     }

De nombreuses commandes couramment utilisées de gdb ont des formes abrégées. Par exemple, la commande list peut être écrite comme l. Pour lister le code source d'une fonction, le nom de la fonction peut également être utilisé comme paramètre :

      (gdb) l add_range
      1       #include <stdio.h>
      2
      3       int add_range(int low, int high)
      4   {
      5                int i, sum;
      6                for (i = low; i <= high; i++)
      7                       sum = sum + i;
      8                return sum;
      9       }
      10

Quittez maintenant l'environnement gdb :

      (gdb) quit

Faisons une expérience, renommez le code source ou déplacez-le à un autre endroit, puis utilisez gdb pour déboguer, afin que le code source ne soit pas répertorié :

      $ mv main.c mian.c
      $ gdb main
      ...
      (gdb) l
      5      main.c: No such file or directory.
            in main.c

On peut voir que l'option -g de gcc n'intègre pas le code source dans le fichier exécutable, et le fichier source est également requis lors du débogage. Restaurez maintenant le code source à son état d'origine, et nous continuons à déboguer. Lancez d'abord le programme avec la commande start :

      $ gdb main
      ...
      (gdb) start
      Temporary breakpoint 1 at 0x8048415: file main.c, line 14.
      Starting program: /home/akaedu/main
      Temporary breakpoint 1, main () at main.c:14
      14              result[0] = add_range(1, 10);
      (gdb)

gdb s'arrête à la première instruction après la définition de la variable dans la fonction main et attend que nous émettions une commande. L'instruction listée par gdb est la prochaine instruction à exécuter. Nous pouvons utiliser la commande suivante (abrégé en n) pour contrôler l'exécution de ces instructions une par une :

      (gdb) n
      15              result[1] = add_range(1, 100);
      (gdb)(直接回车)
      16              printf("result[0]=%d\nresult[1]=%d\n",result[0],
                      result[1]);
      (gdb)(直接回车)
      result[0]=55
      result[1]=5105
      17              return 0;

Utilisez la commande n pour exécuter deux lignes d'instructions d'affectation et une ligne d'instructions d'impression dans l'ordre. Lorsque l'instruction d'impression est exécutée, le résultat sera imprimé immédiatement, puis il s'arrêtera avant l'instruction de retour et attendra que nous émettions une commande. bien que

Bien que nous ayons un contrôle total sur l'exécution du programme, nous ne pouvons toujours pas voir ce qui ne va pas, car l'erreur n'est pas dans la fonction principale mais dans la fonction add_range. Maintenant, utilisez la commande start pour recommencer, cette fois utilisez l'étape commande (abrégé en s) pour explorer Pour suivre l'exécution dans la fonction add_range :

      (gdb) start
      The program being debugged has been started already.
      Start it from the beginning? (y or n) y
      Temporary breakpoint 2 at 0x8048415: file main.c, line 14.
      Starting program: /home/akaedu/main
      Temporary breakpoint 2, main () at main.c:14
      14              result[0] = add_range(1, 10);
      (gdb) s
      add_range (low=1, high=10) at main.c:6
      6               for (i = low; i <= high; i++)

Cette fois, il s'est arrêté à la première instruction après la définition de la variable dans la fonction add_range.

Il existe plusieurs manières d'afficher l'état d'une fonction. La commande backtrace (abrégé en bt) peut afficher le cadre de pile de l'appel de fonction :

      (gdb) bt
      #0  add_range (low=1, high=10) at main.c:6
      #1  0x08048429 in main () at main.c:14

On peut voir que la fonction add_range actuelle est appelée par la fonction main et que les paramètres passés par main sont low=1, high=10.

Le numéro de cadre de pile de la fonction principale est 1 et le numéro de cadre de pile de add_range est 0. Vous pouvez maintenant utiliser la commande info (abrégé en i) pour afficher la valeur de la variable locale de la fonction add_range :

      (gdb) i locals
      i = 0
      sum = 0

Si vous souhaitez afficher la valeur de la variable locale courante de la fonction principale, vous pouvez également le faire. Utilisez d'abord la commande frame (abrégé en f) pour sélectionner le cadre de pile n° 1, puis affichez la variable locale :

      (gdb) f 1
      #1  0x08048429 in main () at main.c:14
      14      result[0] = add_range(1, 10);
      (gdb) i locals
      result={0<repeats 517 times>,1180510,0,0,0,0,0,0,-1207961512,-1073746824, 1228788, -1073746376,
      ...

Notez que de nombreux éléments du tableau de résultats ont des valeurs aléatoires et nous savons que les variables locales non initialisées ont des valeurs indéterminées.

Tout fonctionne bien jusqu'à présent. Utilisez s ou n pour descendre quelques étapes, puis utilisez la commande d'impression (abrégé en p) pour imprimer la valeur de la somme variable :

      (gdb) s
      7                       sum = sum + i;
      (gdb)(直接回车)
      6               for (i = low; i <= high; i++)
      (gdb)(直接回车)
      7                       sum = sum + i;
      (gdb)(直接回车)
      6               for (i = low; i <= high; i++)
      (gdb) p sum
      $1 = 3

Le premier cycle i est 1, le deuxième cycle i est 2, additionnez jusqu'à 3, oui.

1 signifie ici que gdb enregistre ces résultats intermédiaires, 1 signifie que gdb enregistre ces résultats intermédiaires,1 signifie que g d b enregistre ces résultats intermédiaires et les nombres suivants augmenteront automatiquement. Dans la commande, vous pouvez utiliser $1, $2, $3 et d'autres nombres pour remplacer les valeurs correspondantes.

Puisque nous savons déjà que le résultat du premier appel est correct, cela n'a aucun sens de poursuivre. Nous pouvons utiliser la commande finish pour continuer à exécuter le programme jusqu'à ce qu'il revienne de la fonction en cours :

      (gdb) finish
      Run till exit from #0  add_range (low=1, high=10) at main.c:6
      0x08048429 in main () at main.c:14
      14              result[0] = add_range(1, 10);
      Value returned is $2 = 55

La valeur de retour est 55 et il se prépare actuellement à exécuter l'opération d'affectation. Utilisez la commande s pour affecter une valeur, puis affichez le tableau de résultats :

      (gdb) s
      15              result[1] = add_range(1, 100);
      (gdb) p result
      $3={55,0<repeats 516 times>,1180510,0,0,0,0,0,0,-1207961512,
      -1073746824, 1228788, -1073746376,
      ...

La première valeur 55 est bien affectée au 0ème élément du tableau résultat. Ensuite, utilisez la commande s pour entrer le deuxième appel add_range. Après avoir entré, vérifiez d'abord les paramètres et les variables locales :

      (gdb) s
      add_range (low=1, high=100) at main.c:6
      6       for (i = low; i <= high; i++)
      (gdb) bt
      #0  add_range (low=1, high=100) at main.c:6
      #1  0x08048441 in main () at main.c:15
      (gdb) i locals
      i = 11
      sum = 55

Comme les variables locales i et sum ne sont pas initialisées, elles ont des valeurs incertaines, et comme les deux appels sont côte à côte, i et sum prennent simplement la valeur du dernier appel.

C'est juste que l'exemple que j'ai donné cette fois a réussi à faire en sorte que la valeur initiale de la variable locale sum soit 0 lorsqu'elle est appelée pour la première fois et non 0 lorsqu'elle est appelée pour la deuxième fois. Peu importe si la valeur initiale de i est incertaine. Dans la boucle for, je recevrai d'abord une valeur low, mais si la valeur initiale de sum n'est pas 0, le résultat cumulé sera faux.

Eh bien, nous avons trouvé la cause de l'erreur, nous pouvons quitter gdb et modifier le code source. Si nous ne voulons pas gâcher cette opportunité de débogage, nous pouvons immédiatement changer la valeur initiale de sum à 0 dans gdb et continuer à courir pour voir s'il y a d'autres bogues après ce changement :

      (gdb) set var sum=0
      (gdb) finish
      Run till exit from #0  add_range (low=1, high=100) at main.c:6
      0x08048441 in main () at main.c:15
      15              result[1] = add_range(1, 100);
      Value returned is $4 = 5050
      (gdb) n
      16              printf("result[0]=%d\nresult[1]=%d\n",result[0],
                      result[1]);
      (gdb)(直接回车)
      result[0]=55
      result[1]=5050
      17              return 0;

Le résultat est juste.

En plus d'utiliser la commande set pour modifier la valeur d'une variable, vous pouvez également utiliser la commande print, car la commande print est suivie d'une expression, et nous savons que les affectations et les appels de fonction sont également des expressions, vous pouvez donc également utiliser la commande print pour modifier la valeur de la variable ou Appelez les fonctions :

      (gdb) p result[2]=33
      $5 = 33
      (gdb) p printf("result[2]=%d\n", result[2])
      result[2]=33
      $6 = 13

Comme nous l'avons dit, la valeur de retour de printf indique le nombre de caractères réellement imprimés, donc le résultat de $6 est 13. Résumez les commandes gdb utilisées dans cette section, comme indiqué dans le tableau.

insérez la description de l'image ici

Je suppose que tu aimes

Origine blog.csdn.net/weixin_45264425/article/details/132289641
conseillé
Classement