Aprendizaje de depuración de GDB (1): ejecución de un solo paso y seguimiento de llamadas a funciones

prefacio

Además de los errores que son claros de un vistazo, se requieren ciertos métodos de depuración para analizar qué es lo que está mal. Hasta ahora solo tenemos un método de depuración: asumir la causa del error en función del fenómeno de error durante la ejecución del programa, luego insertar printf en una posición apropiada en el código, ejecutar el programa y analizar el resultado de la impresión, si el resultado es el esperado , básicamente prueba Si asume la causa del error usted mismo, puede corregir el error usted mismo.Si el resultado es diferente del esperado, haga suposiciones y análisis adicionales basados ​​​​en el resultado.

En este artículo, presentamos una herramienta de depuración muy poderosa, gdb, que puede controlar completamente la ejecución del programa, haciendo que el programa sea como un juguete en su mano, puede decirle que continúe, decirle que se detenga y puede ver todo los programas en el programa en cualquier momento Estado interno, como el valor de cada variable, los parámetros pasados ​​a la función, la línea de código que se está ejecutando actualmente, etc. Después de dominar el uso de gdb, los métodos de depuración son más abundantes.

Pero cabe señalar que incluso si se enriquecen los métodos de depuración, la idea básica de la depuración sigue siendo un ciclo de "analizar el fenómeno → asumir la causa del error → generar un nuevo fenómeno para verificar la hipótesis". Nuevos fenómenos verificar las hipótesis requiere un análisis y un pensamiento muy rigurosos. Si abusa de las herramientas poderosas e ignora el proceso de análisis porque tiene una herramienta poderosa en la mano, a menudo corregirá los síntomas pero no corregirá el error localmente, lo que resultará en un fenómeno de error que desaparecerá pero el error Todavía existe , e incluso cuanto más se cambia el programa, más mal se vuelve.

Este artículo explica cómo usar gdb para depurar programas a través de varios ejemplos de errores que los principiantes son propensos a cometer y resume algunos comandos gdb de uso común después de cada ejemplo.

1 Llamadas de función de rastreo y paso a paso

      #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 función add_range se agrega de menor a mayor. En la función principal, primero sume de 1 a 10, guarde el resultado, luego sume de 1 a 100 y luego guarde el resultado. Los dos últimos resultados impresos son:

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

El primer resultado es correcto, pero el segundo resultado obviamente es incorrecto [ilustración]. Escuchamos la historia de Gauss cuando era un niño en la escuela primaria. Sumar del 1 al 100 debería ser 5050.

Un fragmento de código, el resultado de la primera ejecución es correcto, pero la segunda ejecución es incorrecta. Este es un tipo de fenómeno de error muy común. En este caso, por un lado, se debe sospechar del código y, por el otro. Por otro lado, los datos deben ser sospechosos: la primera y la misma pieza de código se ejecuta la segunda vez.Si el código es incorrecto, ¿por qué el resultado de la primera vez puede ser correcto?

Es probable que los datos de estado relevantes sean incorrectos durante la segunda ejecución, y los datos incorrectos conducen a resultados incorrectos. Antes de depurar, los lectores deben tratar de ver si pueden ver la causa del error simplemente mirando el código. Siempre que hayan aprendido una base sólida en los capítulos anteriores, deberían poder verlo.

La opción -g se debe agregar al compilar, y el archivo ejecutable generado se puede depurar en el nivel del código fuente con 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 función de la opción -g es agregar información del código fuente al archivo ejecutable , como qué instrucción de máquina en el archivo ejecutable corresponde a la línea del código fuente, pero no incrusta todo el archivo fuente en el archivo ejecutable, por lo tanto, al depurar, debe asegurarse de que gdb pueda encontrar el archivo fuente.

gdb proporciona un entorno de línea de comando similar a un shell, lo anterior (gdb) es el indicador, ingrese ayuda en este indicador para ver la categoría del comando:

      (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.

También puede verificar qué comandos están en una determinada categoría, por ejemplo, verificar qué comandos están disponibles en la categoría de archivos:

      (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
      ...

Ahora intente usar el comando de lista para enumerar el código fuente a partir de la primera línea:

      (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

Solo se enumeran 10 líneas a la vez. Si desea continuar enumerando el código fuente de la línea 11, puede ingresarlo nuevamente

      (gdb) list

También puede presionar Enter sin escribir nada, gdb proporciona una función muy conveniente, presione directamente Enter en el indicador para repetir el comando anterior.

      (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     }

Muchos comandos de gdb que se usan comúnmente tienen formas abreviadas. Por ejemplo, el comando list se puede escribir como l. Para enumerar el código fuente de una función, el nombre de la función también se puede usar como parámetro:

      (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

Ahora salga del entorno gdb:

      (gdb) quit

Hagamos un experimento, cambiemos el nombre del código fuente o muévalo a otro lugar y luego usemos gdb para depurar, de modo que el código fuente no aparezca en la lista:

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

Se puede ver que la opción -g de gcc no incrusta el código fuente en el archivo ejecutable, y el archivo fuente también se requiere durante la depuración. Ahora restaura el código fuente a su estado original y continuamos con la depuración. Primero inicie el programa con el comando de inicio:

      $ 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 se detiene en la primera instrucción después de la definición de la variable en la función principal y espera a que emitamos un comando. La instrucción enumerada por gdb es la siguiente instrucción que se ejecutará. Podemos usar el siguiente comando (abreviado como n) para controlar la ejecución de estas declaraciones una por una:

      (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;

Use el comando n para ejecutar dos líneas de declaraciones de asignación y una línea de declaraciones de impresión en secuencia. Cuando se ejecuta la declaración de impresión, el resultado se imprimirá inmediatamente, y luego se detendrá antes de la declaración de retorno y esperará a que emitamos un comando. a pesar de

Aunque tenemos control total sobre la ejecución del programa, todavía no podemos ver qué está mal, porque el error no está en la función principal sino en la función add_range.Ahora usa el comando start para comenzar de nuevo, esta vez usa el paso Comando (abreviado como s) para profundizar en Para realizar un seguimiento de la ejecución en la función 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++)

Esta vez se detuvo en la primera declaración después de la definición de la variable en la función add_range.

Hay varias formas de ver el estado de una función. El comando backtrace (abreviado como bt) puede ver el marco de la pila de la llamada a la función:

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

Se puede ver que la función principal llama a la función add_range actual, y los parámetros pasados ​​por main son bajo = 1, alto = 10.

El número de marco de pila de la función principal es 1 y el número de marco de pila de add_range es 0. Ahora puede usar el comando info (abreviado como i) para ver el valor de la variable local de la función add_range:

      (gdb) i locals
      i = 0
      sum = 0

Si desea ver el valor de la variable local actual de la función principal, también puede hacerlo. Primero use el comando de cuadro (abreviado como f) para seleccionar el cuadro de pila No. 1 y luego ver la variable local:

      (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,
      ...

Tenga en cuenta que muchos elementos en la matriz de resultados tienen valores aleatorios y sabemos que las variables locales no inicializadas tienen valores indeterminados.

Todo funciona bien hasta ahora. Use s o n para bajar unos pocos pasos y luego use el comando de impresión (abreviado como p) para imprimir el valor de la suma 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

El primer ciclo i es 1, el segundo ciclo i es 2, suma 3, sí.

1 aquí significa que gdb guarda estos resultados intermedios, 1 significa que gdb guarda estos resultados intermedios,1 significa que g d b guarda estos resultados intermedios, y los números posteriores aumentarán automáticamente. En el comando, puede usar $1, $2, $3 y otros números para reemplazar los valores correspondientes.

Como ya sabemos que el resultado de la primera llamada es correcto, no tiene sentido hacer un seguimiento.Podemos usar el comando finish para mantener el programa en ejecución hasta que regrese de la función actual:

      (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

El valor devuelto es 55 y actualmente se está preparando para ejecutar la operación de asignación. Use el comando s para asignar un valor y luego vea la matriz de resultados:

      (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,
      ...

De hecho, el primer valor 55 se asigna al elemento 0 de la matriz de resultados. Luego, use el comando s para ingresar la segunda llamada add_range.Después de ingresar, primero verifique los parámetros y las 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

Dado que las variables locales i y sum no se inicializan, tienen valores inciertos, y debido a que las dos llamadas están una al lado de la otra, i y sum simplemente toman el valor de la última llamada.

Es solo que el ejemplo que di esta vez logró que el valor inicial de la variable local sum sea 0 cuando se llama por primera vez y no sea 0 cuando se llama por segunda vez. No importa si el valor inicial de i es incierto, en el bucle for, primero se le asignará un valor de bajo, pero si el valor inicial de sum no es 0, el resultado acumulado será incorrecto.

Bien, hemos encontrado la causa del error, podemos salir de gdb y modificar el código fuente. Si no queremos desperdiciar esta oportunidad de depuración, podemos cambiar inmediatamente el valor inicial de sum a 0 en gdb y continuar ejecutando para ver si hay otros errores después de este cambio:

      (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;

El resultado es correcto.

Además de usar el comando set para modificar el valor de una variable, también puede usar el comando imprimir, porque el comando imprimir va seguido de una expresión, y sabemos que las asignaciones y las llamadas a funciones también son expresiones, por lo que también puede usar el comando de impresión para modificar el valor de la variable o Funciones de llamada:

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

Como dijimos, el valor de retorno de printf indica el número de caracteres realmente impresos, por lo que el resultado de $6 es 13. Resuma los comandos gdb utilizados en esta sección, como se muestra en la tabla.

inserte la descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/weixin_45264425/article/details/132289641
Recomendado
Clasificación