Lihuen
RSSRSS AtomAtom

Cómo usar GDB

Introducción

Aquellos que desarrollan en C, conocen de las dificultades a las que se enfrenta cuando trata de depurar un programa, que por ejemplo, por qué no se agrega un nodo a una lista o por qué no se copia determinado string. GDB (Gnu Project Debugger) es una herramienta que permite entre otras cosas, correr el programa con la posibilidad de detenerlo cuando se cumple cierta condición, avanzar paso a paso, analizar que ha pasado cuando un programa se detiene o cambiar algunas cosas del programa como el valor de las variables.

GDB es una herramienta muy poderosa que nos ayudará a encontrar esos errores difíciles, por ejemplo cuando los punteros no apuntan a donde estamos pensando. Si bien este tutorial está pensado para el lenguaje C, probablemente también sirva para depurar programas en Fortran o C++ con los mismos comandos o similares.

DESCARGO: Este no es un tutorial de programación en C. Muchas de las cosas aquí explicadas, sobre todo en el código de muestra, no necesariamente estén correctas. Solo se escribieron de la forma en que se escribieron a fin de mostrar alguna determinada característica de GDB.

Instalación

GDB no viene en Lihuen por lo que es necesario instalarlo desde los repositorios. Esto se puede hacer utilizando una interfaz gráfica como Synaptic, instalando el paquete gdb o desde la consola como superusuario, ejecutando los siguientes comandos:

~#apt-get update
~#apt-get install gdb

Una vez instalado el programa, ya está listo para funcionar. GDB funciona desde la terminal y si bien existen interfaces gráficas como ddd y xxgdb, el funcionamiento de las mismas no se cubre en este tutorial. Para ejemplificar el uso de GDB utilizaremos algunos sniplets de código C, en particular de una lista genérica.

Preliminares

Los archivos de ejemplo pueden descargarse aquí.

En el primer ejemplo haremos referencia al archivo lista1.c. Para compilar, se puede correr el siguiente comando:

~$ gcc -o lista lista1.c -std=c99 -Wall -DTEST --debug

El parámetro -DTEST define la macro TEST para que se incluya la función main, que permite testear la funcionalidad de la lista. El parámetro --debug agrega símbolos de depuración, importantes a la hora de utilizar GDB.

Para correr el programa simplemente ejecutamos:

~$ ./lista

La salida debería ser algo así:

 Pregunto si la lista sin inicializar es vacía.
No es vacía
No es vacía, así que trato de imprimir el contenido del nodo...
Violación de segmento

El problema que tiene este programa es que utiliza la lista sin crearla, por lo que, tanto l, l->dato y l->sig son diferentes de NULL. El problema es obvio y el error grosero (otros son mucho más sutiles) pero ¿cómo nos ayuda GDB a darnos cuenta?

Los comandos básicos GDB

Desde la terminal, ejecutamos:

~$ gdb lista

Lista es el nombre de nuestro programa. Si el programa recibe argumentos, pueden pasarse aquí o pueden setearse luego. La salida será algo así como:

GNU gdb (GDB) 7.4.1-debian
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
(gdb)_

La línea de comando de GDB ((gdb)_) espera que ingresemos los comandos. Una prueba simple puede ser utilizar el comando list (o la versión abreviada l) para ver el código fuente de la función main.

(gdb) l 41,51
41      #ifdef TEST
42      int main(int argc, char ** argv){
43              Lista l;
44              puts("Pregunto si la lista sin inicializar es vacía.");
45              printf("%s\n", (l_EsVacia(l)?"Es vacía":"No es vacía"));
46              puts("No es vacía, así que trato de imprimir el contenido del nodo...");
47              printf("%s\n", (char*)l->dato);
48              return 0;
49      }
50      #endif

El problema parece estar en la línea 47 por que lo último que se ejecuta correctamente es el puts de la línea 46. Sin embargo, esto puede no ser tan fácil de determinar en un programa que no tenga salida. Lo que generalmente se hace en estos casos es ejecutar el programa en el entorno de GDB, con el comando run:

(gdb) run
Starting program: lista-generica-c/lista 
Pregunto si la lista sin inicializar es vacía.
No es vacía
No es vacía, así que trato de imprimir el contenido del nodo...

Program received signal SIGSEGV, Segmentation fault.
__strlen_ia32 () at ../sysdeps/i386/i686/multiarch/../../i586/strlen.S:99
99      ../sysdeps/i386/i686/multiarch/../../i586/strlen.S: No existe el fichero o el directorio.

Esto no nos dice mucho. Lo importante aquí es que el programa recibió una señal SIGSEGV, es decir una Violación de Segmento.

Program received signal SIGSEGV, Segmentation fault.

Pero la línea a la que hace referencia está en:

99      ../sysdeps/i386/i686/multiarch/../../i586/strlen.S

Es decir, en una librería del sistema operativo. Para determinar en qué línea de qué función falló el programa, se puede utilizar el comando backtrace, inmediatamente después de haber ejecutado run.

(gdb) backtrace
#0  __strlen_ia32 () at ../sysdeps/i386/i686/multiarch/../../i586/strlen.S:99
#1  0xb7ec0ae5 in _IO_puts (str=0x158d7c <Address 0x158d7c out of bounds>) at ioputs.c:37
#2  0x0804850a in main (argc=1, argv=0xbffff324) at lista1.c:47

El error es el del último mensaje, es decir, en la función main, del archivo lista1.c, en la línea 47.

Puntos de parada y ejecución paso a paso

Ahora vamos a ver como ejecutar el programa hasta que falle pero paso a paso. Esto también puede servir para determinar el punto de falla de un programa.

Primero recargamos el archivo. Esto se puede hacer saliendo de GDB con el comando quit y volviendo a ejecutarlo de la misma forma que en la sección de Comandos básicos de GDB o se puede ejecutar el comando file:

(gdb) file lista
A program is being debugged already.
Are you sure you want to change the file? (y or n) y

Load new symbol table from "lista-generica-c/lista"? (y or n) y

Luego ponemos un punto de parada en la primer línea de main con:

(gdb) break main
Breakpoint 1 at 0x80484c0: file lista1.c, line 44.

o

(gdb) break 42

donde 42 es el número de línea donde se desea detener la ejecución del programa. Si ahora ejecutamos el programa con run, la ejecución se detendrá en este punto de parada.

(gdb) run
Starting program: lista-generica-c/lista  

Breakpoint 1, main (argc=1, argv=0xbffff324) at lista1.c:44 
44              puts("Pregunto si la lista sin inicializar es vacía.");

Para avanzar utilizamos el comando next (o su forma abrebiada n):

(gdb) n
Pregunto si la lista sin inicializar es vacía.
45              printf("%s\n", (l_EsVacia(l)?"Es vacía":"No es vacía"));

Si seguimos ingresando next eventualmente llegaremos al punto de falla.

Cabe aclarar que si una de las siguientes lineas a ejecutar es una función, el comando next la ejecuta y muestra el resultado. Si quiere verse la ejecución de la función, puede usarse el comando step

Visualización del contenido de las variables

Es posible durante la ejecución paso a paso, ver e incluso modificar el contenido de algunas variables. Recapitulemos.

$ gdb lista
...
(gdb) b 41
...
(gdb) run
...
(gdb) n
Pregunto si la lista sin inicializar es vacía.
45              printf("%s\n", (l_EsVacia(l)?"Es vacía":"No es vacía"));

Podemos ahora pedirle a GDB que nos muestre si la lista esta vacía o no, es decir, que evalúe l_EsVacia(l) y nos muestre el resultado. Esto se hace con el comando print (o su forma resumida p).

(gdb) print l_EsVacia(l)
$1 = 0

También es posible mostrar los contenidos de una variable, por ejemplo l o l->dato:

(gdb) p l
$2 = (Lista) 0xb7fb8ff4
p (char*)l-> dato
$3 = 0x158d7c <Address 0x158d7c out of bounds>

Que la dirección de l->dato casteada como string diga out of bounds nos da la pauta de que esa cadena no termina en una dirección que no es del programa que estamos ejecutando. Probablemente una llamada a printf o a strlen de ese string de segmentation fault

(gdb) p printf("%s\n",(char*)l-> dato)

Program received signal SIGSEGV, Segmentation fault.

Este printf no forma parte del programa y como dio error, GDB no lo tiene en cuenta en la ejecución del mismo. Sin embargo, debemos correr el comando run y luego next para volver a donde estábamos. Podemos tratar ahora de enmendar el error para que esta variable o de error a la hora de imprimirla utilizando el comando set

(gdb) set l->dato="algo"
(gdb) p (char*)l->dato
$4 = 0x804a008 "algo"

Ahora el programa debería terminar normalmente. Podemos ejecutarlo hasta el final (en realidad hasta el próximo breakpoint) utilizando el comando continue o su forma abreviada c:

(gdb) c
Continuing.
No es vacía
No es vacía, así que trato de imprimir el contenido del nodo...
algo
[Inferior 1 (process 9620) exited normally]
(gdb)_

Breackpoints condicionales

Si se tiene una lista con muchos elementos y el error se produce cuando se trata de imprimir el último por ejemplo, es tedioso avanzar con next o con step hasta llegar a encontrar el error. Para evitar este inconveniente, en GDB existen los breakpoints condicionales, que solamente detienen la ejecución del programa si determinada condición se cumple. Para ilustrar esto veamos el archivo lista2.c. Ahora el main inicializa la lista antes de preguntar si es vacía. Podemos poner un breakpoint condicional de modo que pare si la lista está vacía, de esta forma:

$ gdb ./lista 
GNU gdb (GDB) 7.4.1-debian
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from lista-generica-c/lista...done.

(gdb) b 48
Breakpoint 1 at 0x8048508: file lista2.c, line 48.
(gdb) condition 1 l->dato == 0

Lo que sigue de condition es el número del breakpoint que se quiere condicionar y que aparece en Breakpoint 1 at 0x8048508: file lista2.c, line 48. Cabe mencionar que como NULL es una macro, GDB no lo conoce como símbolo, de modo que si intentamos algo como

(gdb) condition 1 l->dato == NULL
No symbol "NULL" in current context.

Obtendremos un error ;)

Si ahora corremos el programa con el comando run, se detendrá antes de imprimir que la lista no es vacia.

(gdb) run
Starting program: /home/jwackito/cvss/git/apuntes/lista-generica-c/lista 
Inicializo la lista
Pregunto si la lista sin inicializar es vacía.
Es vacía

Breakpoint 1, main (argc=1, argv=0xbffff324) at lista2.c:48
48              puts("No es vacía, así que trato de imprimir el contenido del nodo...");

Salir de GDB

Para terminar la ejecución de GDB simplemente invocamos el comando quit o su forma abreviada q.

Obtener ayuda

La consola de comandos de GDB tiene una ayuda en línea que permite obtener ayuda de cualquier comando durante la ejecución de GDB. El comando help o su forma abreviada h sin argumentos permite ver las secciones de ayuda.

(gdb) h
List of classes of commands: 

aliases -- Aliases of other commands
breakpoints -- Making program stop at certain points
data -- Examining data
files -- Specifying and examining files
internals -- Maintenance commands
obscure -- Obscure features
running -- Running the program
stack -- Examining the stack
status -- Status inquiries
support -- Support facilities
tracepoints -- Tracing of program execution without stopping the program
user-defined -- User-defined commands

Type "help" followed by a class name for a list of commands in that class.
Type "help all" for the list of all commands.
Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.

El comando help comando muestra la ayuda para el comando comando

(gdb) help list
List specified function or line.
With no argument, lists ten more lines after or around previous listing.
"list -" lists the ten lines before a previous ten-line listing.
One argument specifies a line, and ten lines are listed around that line.
Two arguments with comma between specify starting and ending lines to list.
Lines can be specified in these ways:
  LINENUM, to list around that line in current file,
  FILE:LINENUM, to list around that line in that file,
  FUNCTION, to list around beginning of that function,
  FILE:FUNCTION, to distinguish among like-named static functions.
  *ADDRESS, to list around the line containing that address.
With two args if one is empty it stands for ten lines away from the other arg.

Esta ayuda es de bastante utilidad a la hora de aprender algunos de los aspectos avanzados de GDB. Sin embargo, la ayuda en línea disponible en Debuggin with GDB la presenta de una manera más ordenada y accesible.


 Ante cualquier duda o inconveniente no dudes en visitar nuestros foros.
 http://lihuen.linti.unlp.edu.ar/foros