GNU/Linux >> Tutoriales Linux >  >> Linux

Un tutorial práctico para usar GNU Project Debugger

Si es un programador y quiere poner una funcionalidad determinada en su software, empiece por pensar en formas de implementarla, como escribir un método, definir una clase o crear nuevos tipos de datos. Luego, escribe la implementación en un lenguaje que el compilador o el intérprete puedan entender. Pero, ¿qué sucede si el compilador o el intérprete no entiende las instrucciones tal como las tenía en mente, aunque esté seguro de haber hecho todo bien? ¿Qué pasa si el software funciona bien la mayor parte del tiempo pero causa errores en ciertas circunstancias? En estos casos, debe saber cómo usar un depurador correctamente para encontrar la fuente de sus problemas.

GNU Project Debugger (GDB) es una poderosa herramienta para encontrar errores en los programas. Le ayuda a descubrir el motivo de un error o falla al rastrear lo que sucede dentro del programa durante la ejecución.

Este artículo es un tutorial práctico sobre el uso básico de GDB. Para seguir con los ejemplos, abra la línea de comando y clone este repositorio:

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

Accesos directos

Más recursos de Linux

  • Hoja de trucos de los comandos de Linux
  • Hoja de trucos de comandos avanzados de Linux
  • Curso en línea gratuito:Descripción general técnica de RHEL
  • Hoja de trucos de red de Linux
  • Hoja de trucos de SELinux
  • Hoja de trucos de los comandos comunes de Linux
  • ¿Qué son los contenedores de Linux?
  • Nuestros últimos artículos sobre Linux

Cada comando en GDB se puede acortar. Por ejemplo, info break , que muestra los puntos de interrupción establecidos, se puede acortar a i break . Es posible que vea esas abreviaturas en otros lugares, pero en este artículo, escribiré el comando completo para que quede claro qué función se usa.

Parámetros de la línea de comandos

Puede adjuntar GDB a cada ejecutable. Navegue hasta el repositorio que clonó y compílelo ejecutando make . Ahora debería tener un ejecutable llamado coredump . (Consulte mi artículo sobre Creación y depuración de archivos de volcado de Linux para más información..

Para adjuntar GDB al ejecutable, escriba:gdb coredump .

Su salida debería verse así:

Dice que no se encontraron símbolos de depuración.

La información de depuración es parte del archivo de objeto (el ejecutable) e incluye tipos de datos, firmas de funciones y la relación entre el código fuente y el código de operación. En este punto, tienes dos opciones:

  • Continúe con la depuración del ensamblaje (consulte "Depuración sin símbolos" a continuación)
  • Compile con información de depuración usando la información en la siguiente sección

Compilar con información de depuración

Para incluir información de depuración en el archivo binario, debe volver a compilarlo. Abre el Makefile y elimina el hashtag (# ) de la línea 9:

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

El g La opción le dice al compilador que incluya la información de depuración. Ejecute make clean seguido de make e invoque GDB nuevamente. Debería obtener este resultado y puede comenzar a depurar el código:

La información de depuración adicional aumentará el tamaño del ejecutable. En este caso, aumenta el ejecutable en 2,5 veces (de 26.088 bytes a 65.480 bytes).

Inicie el programa con -c1 cambie escribiendo run -c1 . El programa se iniciará y fallará cuando llegue a State_4 :

Puede recuperar información adicional sobre el programa. El comando info source proporciona información sobre el archivo actual:

  • 101 líneas
  • Idioma:C++
  • Compilador (versión, ajuste, arquitectura, indicador de depuración, estándar de idioma)
  • Formato de depuración:DWARF 2
  • No hay información de macros de preprocesador disponible (cuando se compilan con GCC, las macros solo están disponibles cuando se compilan con -g3 bandera).

El comando info shared imprime una lista de bibliotecas dinámicas con sus direcciones en el espacio de direcciones virtuales que se cargó al inicio para que el programa se ejecute:

Si desea obtener información sobre el manejo de bibliotecas en Linux, consulte mi artículo Cómo manejar bibliotecas dinámicas y estáticas en Linux .

Depurar el programa

Es posible que haya notado que puede iniciar el programa dentro de GDB con run dominio. El run El comando acepta argumentos de línea de comandos como los que usaría para iniciar el programa desde la consola. El -c1 switch hará que el programa se cuelgue en la etapa 4. Para ejecutar el programa desde el principio, no tiene que salir de GDB; simplemente use el run comando de nuevo. Sin el -c1 interruptor, el programa ejecuta un bucle infinito. Tendrías que detenerlo con Ctrl+C .

También puede ejecutar un programa paso a paso. En C/C++, el punto de entrada es main función. Usa el comando list main para abrir la parte del código fuente que muestra el main función:

El main la función está en la línea 33, así que agregue un punto de interrupción allí escribiendo break 33 :

Ejecute el programa escribiendo run . Como era de esperar, el programa se detiene en el main función. Escriba layout src para mostrar el código fuente en paralelo:

Ahora está en el modo de interfaz de usuario de texto (TUI) de GDB. Utilice las teclas de flecha hacia arriba y hacia abajo para desplazarse por el código fuente.

GDB resalta la línea a ejecutar. Al escribir next (n), puede ejecutar los comandos línea por línea. GBD ejecuta el último comando si no especifica uno nuevo. Para recorrer el código, simplemente presione Enter clave.

De vez en cuando, notará que la salida de TUI se corrompe un poco:

Si esto sucede, presione Ctrl+L para restablecer la pantalla.

Usa Ctrl+X+A para entrar y salir del modo TUI a voluntad. Puede encontrar otras combinaciones de teclas en el manual.

Para salir de GDB, simplemente escriba quit .

Puntos de observación

El corazón de este programa de ejemplo consiste en una máquina de estado que se ejecuta en un bucle infinito. La variable n_state es una enumeración simple que determina el estado actual:

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

Desea detener el programa cuando n_state se establece en el valor State_5 . Para hacerlo, detenga el programa en main función y establecer un punto de observación para n_state :

watch n_state == State_5

La configuración de puntos de observación con el nombre de la variable solo funciona si la variable deseada está disponible en el contexto actual.

Cuando continúa la ejecución del programa escribiendo continue , debería obtener un resultado como:

Si continúa la ejecución, GDB se detendrá cuando la expresión del punto de observación se evalúe como false :

Puede especificar puntos de control para cambios de valores generales, valores específicos y acceso de lectura o escritura.

Alteración de puntos de interrupción y puntos de observación

Escriba info watchpoints para imprimir una lista de puntos de observación establecidos previamente:

Eliminar puntos de interrupción y puntos de observación

Como puede ver, los puntos de observación son números. Para eliminar un punto de observación específico, escriba delete seguido del número del punto de vigilancia. Por ejemplo, mi punto de observación tiene el número 2; para eliminar este punto de observación, ingresa delete 2 .

Precaución: Si usa delete sin especificar un número, todos se eliminarán los puntos de observación y los puntos de interrupción.

Lo mismo se aplica a los puntos de interrupción. En la captura de pantalla a continuación, agregué varios puntos de interrupción e imprimí una lista de ellos escribiendo info breakpoint :

Para eliminar un solo punto de interrupción, escriba delete seguido de su número. Como alternativa, puede eliminar un punto de interrupción especificando su número de línea. Por ejemplo, el comando clear 78 eliminará el punto de interrupción número 7, que se establece en la línea 78.

Deshabilitar o habilitar puntos de interrupción y puntos de observación

En lugar de eliminar un punto de interrupción o un punto de observación, puede desactivarlo escribiendo disable seguido de su número. A continuación, los puntos de interrupción 3 y 4 están deshabilitados y marcados con un signo menos en la ventana de código:

También es posible modificar un rango de puntos de interrupción o puntos de observación escribiendo algo como disable 2 - 4 . Si desea reactivar los puntos, escriba enable seguido de sus números.

Puntos de interrupción condicionales

Primero, elimine todos los puntos de interrupción y puntos de observación escribiendo delete . Todavía desea que el programa se detenga en el main función, pero en lugar de especificar un número de línea, agregue un punto de interrupción nombrando la función directamente. Escribe break main para agregar un punto de interrupción en el main función.

Escribe run para comenzar la ejecución desde el principio, y el programa se detendrá en el main función.

El main la función incluye la variable n_state_3_count , que se incrementa cuando la máquina de estado alcanza el estado 3.

Para agregar un punto de interrupción condicional basado en el valor de n_state_3_count tipo:

break 54 if n_state_3_count == 3

Continuar la ejecución. El programa ejecutará la máquina de estados tres veces antes de detenerse en la línea 54. Para verificar el valor de n_state_3_count , escriba:

print n_state_3_count

Hacer puntos de interrupción condicionales

También es posible condicionar un punto de interrupción existente. Elimine el punto de interrupción agregado recientemente con clear 54 y agregue un punto de interrupción simple escribiendo break 54 . Puede hacer que este punto de interrupción sea condicional escribiendo:

condition 3 n_state_3_count == 9

El 3 se refiere al número de punto de interrupción.

Establecer puntos de interrupción en otros archivos fuente

Si tiene un programa que consta de varios archivos fuente, puede establecer puntos de interrupción especificando el nombre del archivo antes del número de línea, por ejemplo, break main.cpp:54 .

Puntos de captura

Además de puntos de interrupción y puntos de observación, también puede establecer puntos de captura. Los puntos de captura se aplican a los eventos del programa, como realizar llamadas al sistema, cargar bibliotecas compartidas o generar excepciones.

Para capturar el write syscall, que se usa para escribir en STDOUT, ingrese:

catch syscall write

Cada vez que el programa escribe en la salida de la consola, GDB interrumpirá la ejecución.

En el manual, puede encontrar un capítulo completo que cubre los puntos de interrupción, vigilancia y captura.

Evaluar y manipular símbolos

La impresión de los valores de las variables se realiza con print dominio. La sintaxis general es print <expression> <value> . El valor de una variable se puede modificar escribiendo:

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

En la captura de pantalla a continuación, di la variable n_state_3_count el valor 123 .

El /x expresión imprime el valor en hexadecimal; con el & operador, puede imprimir la dirección dentro del espacio de direcciones virtuales.

Si no está seguro del tipo de datos de un determinado símbolo, puede encontrarlo con whatis :

Si desea enumerar todas las variables que están disponibles en el ámbito de main función, escriba info scope main :

El DW_OP_fbreg los valores se refieren al desplazamiento de pila basado en la subrutina actual.

Alternativamente, si ya está dentro de una función y desea enumerar todas las variables en el marco de pila actual, puede usar info locals :

Consulte el manual para obtener más información sobre el examen de símbolos.

Adjuntar a un proceso en ejecución

El comando gdb attach <process-id> le permite conectarse a un proceso que ya se está ejecutando especificando el ID del proceso (PID). Afortunadamente, el coredump El programa imprime su PID actual en la pantalla, por lo que no tiene que encontrarlo manualmente con ps o top.

Inicie una instancia de la aplicación de volcado de memoria:

./coredump

El sistema operativo da el PID 2849 . Abra una ventana de consola separada, muévase al directorio de origen de la aplicación de volcado de memoria y adjunte GDB:

gdb attach 2849

GDB detiene inmediatamente la ejecución cuando lo adjunta. Escriba layout src y backtrace para examinar la pila de llamadas:

La salida muestra el proceso interrumpido mientras se ejecuta std::this_thread::sleep_for<...>(...) función que se llamó en la línea 92 de main.cpp .

Tan pronto como salga de GDB, el proceso continuará ejecutándose.

Puede encontrar más información sobre cómo adjuntar a un proceso en ejecución en el manual de GDB.

Moverse por la pila

Regrese al programa usando up dos veces para subir en la pila a main.cpp :

Por lo general, el compilador creará una subrutina para cada función o método. Cada subrutina tiene su propio marco de pila, por lo que moverse hacia arriba en el marco de pila significa moverse hacia arriba en la pila de llamadas.

Puede obtener más información sobre la evaluación de la pila en el manual.

Especifique los archivos fuente

Cuando se adjunta a un proceso que ya se está ejecutando, GDB buscará los archivos de origen en el directorio de trabajo actual. Alternativamente, puede especificar los directorios de origen manualmente con el directory comando.

Evaluar archivos de volcado

Lea Creación y depuración de archivos de volcado de Linux para obtener información sobre este tema.

TL;DR:

  1. Supongo que está trabajando con una versión reciente de Fedora
  2. Invocar volcado de núcleo con el modificador c1:coredump -c1

  3. Cargue el archivo de volcado más reciente con GDB:coredumpctl debug
  4. Abra el modo TUI e ingrese layout src

La salida de backtrace muestra que el bloqueo ocurrió a cinco marcos de pila de distancia de main.cpp . Intro para saltar directamente a la línea de código defectuosa en main.cpp :

Una mirada al código fuente muestra que el programa intentó liberar un puntero que no fue devuelto por una función de administración de memoria. Esto da como resultado un comportamiento indefinido y causó el SIGABRT .

Depuración sin símbolos

Si no hay fuentes disponibles, las cosas se ponen muy difíciles. Tuve mi primera experiencia con esto cuando intentaba resolver desafíos de ingeniería inversa. También es útil tener algún conocimiento del lenguaje ensamblador.

Mira cómo funciona con este ejemplo.

Vaya al directorio de origen, abra el Makefile y edite la línea 9 así:

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

Para recompilar el programa, ejecute make clean seguido de make e inicie GDB. El programa ya no tiene ningún símbolo de depuración para mostrar el código fuente.

El comando info file revela las áreas de memoria y el punto de entrada del binario:

El punto de entrada se corresponde con el comienzo del .text área, que contiene el código de operación real. Para agregar un punto de interrupción en el punto de entrada, escriba break *0x401110 luego comience la ejecución escribiendo run :

Para configurar un punto de interrupción en una dirección determinada, especifíquelo con el operador de desreferenciación * .

Elija el tipo de desensamblador

Antes de profundizar en el ensamblaje, puede elegir qué tipo de ensamblaje usar. El valor predeterminado de GDB es AT&T, pero prefiero la sintaxis de Intel. Cámbialo con:

set disassembly-flavor intel

Ahora abra el ensamblaje y registre la ventana escribiendo layout asm y layout reg . Ahora debería ver un resultado como este:

Guardar archivos de configuración

Aunque ya ha ingresado muchos comandos, en realidad no ha comenzado a depurar. Si está depurando mucho una aplicación o tratando de resolver un desafío de ingeniería inversa, puede ser útil guardar la configuración específica de GDB en un archivo.

El archivo de configuración gdbinit en el repositorio de GitHub de este proyecto contiene los comandos usados ​​recientemente:

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

El set write on El comando le permite modificar el binario durante la ejecución.

Salga de GDB y vuelva a abrirlo con el archivo de configuración: gdb -x gdbinit coredump .

Leer instrucciones

Con el c2 interruptor aplicado, el programa se bloqueará. El programa se detiene en la función de entrada, por lo que debe escribir continue para continuar con la ejecución:

El idiv instrucción realiza una división entera con el dividendo en el RAX registro y el divisor especificado como argumento. El cociente se carga en el RAX registrarse, y el resto se carga en RDX .

Desde la descripción general del registro, puede ver el RAX contiene 5 , por lo que debe averiguar qué valor está almacenado en la pila en la posición RBP-0x4 .

Leer memoria

Para leer contenido de memoria sin procesar, debe especificar algunos parámetros más que para leer símbolos. Cuando se desplaza un poco hacia arriba en la salida del ensamblaje, puede ver la división de la pila:

Lo que más le interesa es el valor de rbp-0x4 porque esta es la posición donde el argumento para idiv está almacenado. En la captura de pantalla, puede ver que la siguiente variable se encuentra en rbp-0x8 , por lo que la variable en rbp-0x4 tiene 4 bytes de ancho.

En GDB, puede usar el x comando para examinar cualquier contenido de memoria:

x/ n f u> addr>

Parámetros opcionales:

  • n :el recuento de repeticiones (predeterminado:1) se refiere al tamaño de la unidad
  • f :Especificador de formato, como en printf
  • u :Tamaño de la unidad
    • b :bytes
    • h :medias palabras (2 bytes)
    • w :palabra (4 bytes) (predeterminado)
    • g :palabra gigante (8 bytes)

Para imprimir el valor en rbp-0x4 , escribe x/u $rbp-4 :

Si tiene en cuenta este patrón, es sencillo examinar la memoria. Consulte la sección de examen de la memoria en el manual.

Manipular el ensamblaje

La excepción aritmética ocurrió en la subrutina zeroDivide() . Cuando se desplaza un poco hacia arriba con la tecla de flecha hacia arriba, puede encontrar este patrón:

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

Esto se llama prólogo de la función:

  1. El puntero base (rbp ) de la función de llamada se almacena en la pila
  2. El valor del puntero de pila (rsp ) se carga en el puntero base (rbp )

Omita esta subrutina por completo. Puede verificar la pila de llamadas con backtrace . Solo está un marco de pila por delante de su main función, para que pueda volver a main con un solo up :

En tu main función, puede encontrar este patrón:

0x401431 <main+497>     cmp    BYTE PTR [rbp-0x12],0x0
0x401435 <main+501>     je     0x40145f <main+543>
0x401437 <main+503>     call   0x401211<_Z10zeroDividev>

La subrutina zeroDivide() se ingresa solo cuando jump equal (je) se evalúa como true . Puede reemplazar esto fácilmente con un jump-not-equal (jne) instrucción, que tiene el código de operación 0x75 (siempre que esté en una arquitectura x86/64; los códigos de operación son diferentes en otras arquitecturas). Reinicie el programa escribiendo run . Cuando el programa se detenga en la función de entrada, manipule el código de operación escribiendo:

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

Finalmente, escribe continue . El programa omitirá la subrutina zeroDivide() y no se bloqueará más.

Conclusión

Puede encontrar GDB trabajando en segundo plano en muchos entornos de desarrollo integrados (IDE), incluidos Qt Creator y la extensión Native Debug para VSCodium.

Es útil saber cómo aprovechar la funcionalidad de GDB. Por lo general, no todas las funciones de GDB se pueden usar desde el IDE, por lo que se beneficiará de tener experiencia en el uso de GDB desde la línea de comandos.


Linux
  1. 7 trucos útiles para usar el comando wget de Linux

  2. Consejos de Linux para usar GNU Screen

  3. 8 consejos para la línea de comandos de Linux

  4. 5 consejos para el depurador GNU

  5. Kali en el subsistema de Windows para Linux

Tutorial de comandos ss de Linux para principiantes (8 ejemplos)

GalliumOS:la distribución de Linux para Chromebooks

La guía completa para usar ffmpeg en Linux

Tutorial sobre el uso del comando Timeout en Linux

Tutorial sobre el uso del último comando en la terminal de Linux

Los 20 mejores depuradores de Linux para ingenieros de software modernos