Hoy toca hablar sobre GDB el depurador estándar del compilador GNU. Normalmente los IDEs suelen integrar una interfaz gráfica para interactuar con los debuggers de cada lenguaje y no suele ser necesario aprender a usar un debugger por consola. Pero como siempre pasa, cuando necesitas ese conocimiento suele ser demasiado tarde y te arrepientes de tu dependencia a los entornos de desarrollo modernos. En cualquier caso aprender a usar GDB por consola te permite poder integrarlo en prácticamente en cualquier entorno de desarrollo.
Normalmente los casos de usos de un depurador suele ser para:
- Abrir un core y hacer un backtrace para ver las últimas llamadas a funciones antes del crash.
- Lanzar un programa a ir yendo paso a paso por el progama para poder ir imprimiendo el valor de las variables y las funciones por las que pasa.
Opciones del compilador
Para poder usar GDB es necesario compilar el software habilitando las símbolos de depuración. En GDB suele ser con el flag -g. También seria conveniente deshabilitar las optimizaciones del compilador para que no omita ciertas variables y no se puendan mostrar en GSB, no obstante esto último puede ser opcional.
También merece la pena agregar -g3 para indicar al compilador que incluya la información de depuración para las macros.
GDB básico
Vamos con un ejemplo básico:
#include <iostream>
using namespace std;
int add(int a, int b)
{
return a + b;
}
int main(void)
{
std::cout << add(1, 4) << std::endl;
return 0;
}
y compilamos:
g++ -g -Wall -std=c++20 -g3 -O0 main.cc -o main.out
Para lanzar gdb
gdb <executable>
GDB con argumentos
Normalmente la mayoría de aplicaciones requieren una serie de argumentos que se le pasan por consola. Para poder depurar aplicaciones con argumentos se lanza gdb con el flag –args. Modifiquemos el programa anterior para aceptar argumentos por cli:
#include <iostream>
#include <string>
using namespace std;
int add(int a, int b)
{
return a + b;
}
int main(int argc, char* argv[])
{
if (argc < 3) {
std::cout << "Usage " << argv[0]
<< " num1 num2" << std::endl;
return 1;
}
int a = stoi(argv[1]);
int b = stoi(argv[2]);
std::cout << "add(" << a << "," << b << ")="
<< add(a, b) << std::endl;
return 0;
}
gdb --args <executable> <arg1> <arg2> ... <argn>
gdb --args main 1 2
Abrir un core
Cuando un software termina con un crash por defecto en Linux no se genera el fichero de core. Para poder habilitarlo hay que lanzar el comando ulimit. Recordar esto para entornos de producción porque si se produce un core no vamos a poder contar con esa valiosa información.
El fichero de core tiene el formato de: core.8721. El número que sigue a core es el pid del ejecutable.
Tanto el formato del fichero del core como habilitar que se vuelque por defecto se puede configurar escribiendo ciertos ficheros del kernel.
Por ejemplo, usemos el programa anterior abortando en la función add:
int add(int a, int b)
{
abort();
return a + b;
}
$ gdb main core.123
$ run
program received signal SIGABRT, Aborted.
__pthread_kill_implementation (threadid=<optimized out>, signo=signo@entry=6, no_tid=no_tid@entry=0) at pthread_kill.c:44
Downloading source file /usr/src/debug/glibc/glibc/nptl/pthread_kill.c
44 return INTERNAL_SYSCALL_ERROR_P (ret) ? INTERNAL_SYSCALL_ERRNO (ret) : 0;
$ bt
(gdb) bt
#0 __pthread_kill_implementation (threadid=<optimized out>, signo=signo@entry=6, no_tid=no_tid@entry=0) at pthread_kill.c:44
#1 0x00007ffff7aa0953 in __pthread_kill_internal (signo=6, threadid=<optimized out>) at pthread_kill.c:78
#2 0x00007ffff7a51ea8 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#3 0x00007ffff7a3b53d in __GI_abort () at abort.c:79
#4 0x00005555555562bc in add (a=1, b=2) at main.cc:7
#5 0x000055555555646b in main (argc=3, argv=0x7fffffffdd48) at main.cc:21
Asi podemos abrir el core y conocer las últimas funciones que se ejecutaron.
GDB contra un proceso en ejecución
Otro uso interesante de GDB es que te permite poder acoplar el depurador en una aplicación que está corriendo mediante el PID del proceso.
gdb --pid <PID>
Depuración remota
También existe la posibilidad de depurar código de forma remota. Es decir, se puede depurar código que está corriendo en una máquina A y desde una máquina B depurar el código de forma remota.
Máquina remota
gdbserver :<PORT> <executable> <args>
Máquina de trabajo
$ gdb
target remote <IP>:<port>
Habilitar interfaz de terminal (tui)
Cuando estemos frente a una sesión de depuración tener un interfaz de texto exclusivamente se puede hacer un poco tedioso porque tenemos que ir lanzando el comando list para mostrar el código fuente. Gdb posee una interfaz de terminal que divide la pantalla entre una cli para comandos gdb y el código fuente. Para habilitarlo:
gdb --tui <executable>
Scripting con gdb
Otro de las usos en los que me he encontrado es el de poder hacer scripting con gdb. El caso en particular consistía en abrir una lista de ficheros de cores e imprimir el resultado por pantalla para poder analizarlos, ordenarlos, etc.
Los flags para poder hacer scripting son:
- –batch: Suprime texto de salida y sale con status 0 al terminar
- –ex
: Ejecuta el comando de GDB desado - -x
: Especifica un fichero con una lista de comandos GDB a lanzar.
En mi caso en particular la solución fue algo así:
ls core.* | xargs -I {} gdb --batch -ex bt {}
Para los curiosos, la solución fue algo más compleja porque este comando pertenecía a un script en python que obtenia el backtrace del core y con esa salida se hacía un análisis para descartar aquellos cores que tenían un backtrace con las misma funciones
Links
https://developers.redhat.com/blog/2015/04/28/remote-debugging-with-gdb