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:
- Supongo que está trabajando con una versión reciente de Fedora
- Invocar volcado de núcleo con el modificador c1:
coredump -c1
- Cargue el archivo de volcado más reciente con GDB:
coredumpctl debug
- 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/
f
u
>
Parámetros opcionales:
n
:el recuento de repeticiones (predeterminado:1) se refiere al tamaño de la unidadf
:Especificador de formato, como en printfu
:Tamaño de la unidadb
:bytesh
: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:
- El puntero base (
rbp
) de la función de llamada se almacena en la pila - 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.