Uso del debugger GDB

El debugger gdb es una herramienta poderosísima, que habitualmente está disponible en los mismos sistemas que el compilador g++.

El uso principal que podemos darle es detectar rápidamente la línea donde ocurre el runtime error en un programa que falla en tiempo de ejecución. Esto es especialmente valioso si el programa es muy largo y puede fallar en muchos lugares.

Supongamos que tenemos el siguiente programa, que lee una lista de números de entrada, y muestra dos valores: el máximo número de la entrada, y el total que resulta de sumar todos los productos de dos números consecutivos en la lista.

#include <iostream>
#include <vector>
 
using namespace std;
 
int main()
{
    int n;
    cin >> n;
    vector<int> v(n);
    for (int i=0;i<n;i++)
        cin >> v[i];
    int suma = 0;
    for (int i=0;i<n;i++)
        suma += v[i] * v[i+1];
    int maxi = -1000000000;
    for (int i=0;i<n;i++)
        maxi = max(maxi, v[i]);
    cout << suma << " " << maxi << endl;
    return 0;
}

Este programa puede parecer correcto a primera vista, y de hecho si compilamos sin opciones especiales (que es lo que hacen casi todos los entornos y editores, si no configuramos especialmente las opciones recomendadas para programación competitiva) puede parecer que funciona bien al correr un ejemplo sencillo:

$ g++ -o gdbtest gdbtest.cpp
$ ./gdbtest
4
1 10 20 30
810 30

Sin embargo, si seguimos testeandolo intensivamente, en algún momento vamos a encontrar (casi seguro) que falla:

$ g++ -o gdbtest gdbtest.cpp
$ ./gdbtest
30
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
1829660 30

El resultado de este experimento es que el programa falla, no da el resultado correcto. Si lo hubiéramos enviado a un problema del juez online de la OIA, podríamos recibir un veredicto de respuesta incorrecta.

Veamos qué pasa si corremos el mismo programa, pero compilando con las opciones recomendadas:

$ g++ -std=c++11 -g -O2 -Wconversion -Wshadow -Wall -Wextra -D_GLIBCXX_DEBUG -o gdbtest gdbtest.cpp
$ ./gdbtest 
30
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
/usr/include/c++/7/debug/vector:417:
Error: attempt to subscript container with out-of-bounds index 30, but 
container only holds 30 elements.

Objects involved in the operation:
    sequence "this" @ 0x0x7ffe50c24950 {
      type = std::__debug::vector<int, std::allocator<int> >;
    }
Aborted (core dumped)

La información que obtenemos de esta manera es muchísimo mejor: en lugar de tener una respuesta completamente incorrecta, obtenemos directamente un error en tiempo de ejecución, que nos dice que el problema fue que se accedió fuera de rango, con un índice 30, a un cierto vector. ¿Pero en qué línea de nuestro programa ocurrió este acceso fuera de rango? En un programa largo habrá muchos accesos a vectores, y no será obvio para nada dónde está el error.

Podemos utilizar gdb para obtener esta información.

Iniciamos gdb con el comando

$gdb ./gdbtest

Notemos que ./gdbtest es justamente nuestro archivo ejecutable, el resultado de compilar nuestro programa gdbtest.cpp.

Esto inicia a su vez una consola de gdb, en la cual podemos usar el comando run que inicia y ejecuta nuestro programa. Como nuestro programa lee del teclado, allí mismo queda esperando los datos, y podemos ingresar los valores de entrada para nuestro programa. Al hacer todo esto obtenemos:

$(gdb) run
Starting program: gdbtest
30
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
/usr/include/c++/7/debug/vector:417:
Error: attempt to subscript container with out-of-bounds index 30, but 
container only holds 30 elements.

Objects involved in the operation:
    sequence "this" @ 0x0x7fffffffd8c0 {
      type = std::__debug::vector<int, std::allocator<int> >;
    }

Program received signal SIGABRT, Aborted.
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
51	../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
$ (gdb)

Ahora nuestro programa fue ejecutado por gdb hasta el error de ejecución, y vemos un mensaje muy similar al anterior. La clave es que ahora, aquí mismo, podemos utilizar el comando where para que nos muestre un stack-trace, que indica todas las llamadas que se realizaron en el programa, con sus correspondientes números de línea (solamente si se compiló con la opción -g: es muy importante compilar con esta opción, para poder ver los números de línea en gdb), hasta el punto exacto donde ocurrió el error:

$(gdb) where
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1  0x00007ffff7481921 in __GI_abort () at abort.c:79
#2  0x00007ffff7ad8789 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x000055555555525f in std::__debug::vector<int, std::allocator<int> >::operator[] (__n=<optimized out>, this=0x7fffffffd8c0) at /usr/include/c++/7/debug/vector:417
#4  main () at gdbtest.cpp:15

Si leemos de abajo hacia arriba, el stack trace nos dice que se ingresó a la función main() y en la línea 15 de gdbtest.cpp se llamó a la siguiente función, que viendo la línea justo arriba resulta ser el operator[] (o sea, un acceso a vector) y ya caemos en código interno de la STL. Podemos ver las llamadas internas dentro de la STL, pero eso normalmente no nos interesa.

La clave es que al correr esto, instantáneamente gdb nos dice sin perder tiempo la línea donde ocurrió el error en tiempo de ejecucción: fue en gdbtest.cpp:15, al llamar al operator[] de vector.

En nuestro caso particular la línea 15 era suma += v[i] * v[i+1]; , en el segundo for. Entonces, el error ocurrió al acceder al vector allí, lo que nos da una enorme pista sobre dónde podemos tener el acceso fuera de rango. Mirando con cuidado ese for, vemos que en la línea justo anterior deberíamos ir solamente hasta $n-1$ y no hasta $n$, porque vamos a acceder a $i+1$ y no solamente a $i$.

Corrigiendo eso, nuestro programa funciona. Y gracias a gdb combinado con las opciones de compilación recomendadas (notar que si no, no hubiera ocurrido el error, ¡sería directamente una respuesta errónea!), podemos encontrar sin perder tiempo el lugar del bug.

Finalmente, si queremos correr nuestro programa con un archivo de entrada para no tener que tipearla, lo que normalmente haríamos con:

$./programa <entrada.in

En gdb lo hacemos ejecutando

$gdb ./programa

Pero luego en lugar de directamente run, utilizamos

$(gdb) run <entrada.in