En la nota anterior te mostré cómo controlar el flujo de ejecución utilizando breakpoints condicionales y asociando comandos a ellos. Con estas herramientas ganamos bastante flexibilidad a la hora de examinar la ejecución de nuestros programas, sin embargo, para algunas situaciones no es suficiente con averiguar las circunstancias presentes al momento de reproducir un bug, también es necesario saber cómo fue que llegamos a tales circunstancias; es decir: cuál fue el camino que siguió nuestro programa para llegar al punto de falla.
El camino recorrido
GDB proporciona un comando para mostrar el recorrido que ha dado la ejecución del programa hasta el punto de quiebre: backtrace
. Considera el siguiente programa:
void foo(int f); void bar(int b); void var(); int main() { int n; n = 5; var(); foo(2 * n); return 0; } void foo(int f) { int x; x = f + f; bar(x); } void bar(int b) { int y; y = b - 1; } void var() { return; }
En realidad el programa no hace nada útil, pero es suficiente para ilustrar el comportamiento del comando. Para practicar, guarda el código anterior en un archivo llamado foo.c
y luego construye el binario correspondiente foo
con símbolos de depuración. Con todo listo, inicia una sesión de GDB con ese binario.
➜ gdb foo ... Reading symbols from foo...done.
Una vez dentro del depurador, crea un punto de quiebre en bar()
y ejecuta el programa. A partir de ese momento puedes explorar el estado del proceso que estás depurando:
(gdb) break bar Breakpoint 1 at 0x4004fe: file foo.c, line 26. (gdb) run Starting program: /home/user/foo
En este punto ejecuta el comando backtrace
o su abreviatura bt
y observa su salida. En ella encontrarás una lista de las funciones que han sido invocadas a partir del inicio de la ejecución del proceso que estás examinando, en este caso, main()
.
(gdb) backtrace #0 bar (b=20) at foo.c:26 #1 0x00000000004004ff in foo (f=10) at foo.c:20 #2 0x00000000004004db in main () at foo.c:11 (gdb)
La salida de este comando representa el estado actual de la pila de llamadas al momento de alcanzar el punto de quiebre. Es por ello que la primera línea #0
indica la posición actual y nivel más profundo en la ejecución del programa. Mientras que la última línea indica la ubicación del nivel más superficial, que en este caso, corresponde a main()
. Cada línea intermedia indica el lugar desde donde fue llamada la función de la línea anterior. De todo esto, podemos observar que el camino recorrido por el proceso hasta llegar al punto de quiebre que indicamos es main() -> foo() -> bar()
. Nota que var()
está fuera de esta ruta debido a que la llamada a esta función ya no se encuentra en la pila de llamadas cuando bar()
es invocada. Gráficamente esta situación luce así:
Además de poder observar cómo luce la pila de llamadas, también podemos explorarla. Desde la misma salida de backtrace
podemos leer los valores actuales de todos los argumentos de cada una de las funciones en la pila. También podemos desplegar los valores almacenados en las variables de una sola vez utilizando la opción full
de este comando.
(gdb) backtrace full #0 bar (b=20) at foo.c:26 y = 32767 #1 0x00000000004004ff in foo (f=10) at foo.c:20 x = 20 #2 0x00000000004004db in main () at foo.c:11 n = 5 (gdb)
Y no solo eso, también podemos cambiar la vista hacia cualquier función en la pila mediante el comando frame
, pasándole como argumento el número indicado al inicio de la línea:
(gdb) frame 1 #1 0x00000000004004ff in foo (f=10) at foo.c:20 20 bar(x); (gdb) list 15 16 void foo(int f) 17 { 18 int x; 19 x = f + f; 20 bar(x); 21 } 22 23 void bar(int b) 24 { (gdb)
Para volver al nivel más profundo en la ejecución y seguir examinando el programa, basta con ejecutar frame 0
. Fácil, ¿no?
Me parece que esto es suficiente por ahora. Con las herramientas que he explicado hasta el momento ya puedes utilizar GDB para encontrar bichos cada vez más escurridizos en tus programas. Sigue practicando y espera la siguiente entrega de esta serie. ¡Hasta entonces!