04. Depuración: visualizando variables

Publicado por

Una vez que hemos detenido nuestro programa, el depurador nos proporciona comandos para visualizar el valor que toman las variables dentro del ámbito en el que nos encontramos. Ya he utilizado el modo más simple para observar variables: el comando p o print, el cual simplemente despliega el valor actual de una variable. Si una variable tiene un valor inesperado, resulta ser un muy buen indicio sobre la naturaleza del error que estemos buscando en ese momento.

Cuando se trata de variables escalares, la visualización de su estado es simple, sin embargo, las cosas cambian un poco cuando se trata de objetos más complejos, como arreglos, apuntadores o estructuras. Considera el programa que utilicé de ejemplo en la nota anterior.

Dentro de la función main(), trabajamos con cinco variables; dos de ellas son argumentos de la función argc y argv, las cuales podemos examinar utilizando el comando print:

➜ gdb maximo
GNU gdb ...
...
Leyendo símbolos desde maximo...hecho.
(gdb) break 12
Punto de interrupción 1 at 0x4006d6: file maximo.c, line 12.
(gdb) run 5 1 2 3 4 10 6 7 8 9
Starting program: /home/user/maximo 5 1 2 3 4 10 6 7 8 9

Breakpoint 1, main (argc=11, argv=0x7fffffffe098) at maximo.c:12
12        if (argc == 1)
(gdb) print argc
$1 = 11
(gdb) print argv
$2 = (char **) 0x7fffffffe098
(gdb)

Hay algunas diferencias que notar en la salida del comando print para ambas variables: en el caso de argc simplemente se despliega el valor numérico puesto que el tipo de esta variable es int; en el caso de argv se muestra el tipo de dato antes de su valor, el cual se presenta en notación hexadecimal.

En ocasiones la salida hexadecimal de un apuntador nos sirve, a fin de cuentas, ese es el valor almacenado en el apuntador; sin embargo, a veces puede no ser suficiente. Si quieres observar qué es lo que está almacenado en el espacio que indica el apuntador, debes utilizar el operador de indirección * sobre este.

(gdb) print *argv
$3 = 0x7fffffffe3f1 "/home/user/maximo"
(gdb)

Y como los arreglos son un tipo de apuntador, los apuntadores también pueden ser interpretados como arreglos, por lo que también podemos desplegar sus elementos:

(gdb) print argv[0]
$4 = 0x7fffffffe3f1 "/home/user/maximo"
(gdb) print argv[1]
$5 = 0x7fffffffe425 "5"
(gdb) print argv[2]
$6 = 0x7fffffffe427 "1"
(gdb)

Si quieres ver todo el arreglo al que señala un apuntador, debes indicarle al depurador cuántos elementos contiene dicho segmento de memoria. La forma de hacerlo es accediendo al contenido del apuntador mediante el operador * y agregando un arroba @ de seguido una expresión numérica que indique el número de elementos; esta puede ser el número literalmente o una variable:

(gdb) print *argv@4
$7 = {0x7fffffffe3f1 "/home/user/maximo", 0x7fffffffe425 "5",
  0x7fffffffe427 "1", 0x7fffffffe429 "2"}
(gdb) print argv@argc
(gdb) print *argv@argc
$8 = {0x7fffffffe3f1 "/home/user/maximo",  0x7fffffffe425 "5",
  0x7fffffffe427 "1", 0x7fffffffe429 "2",  0x7fffffffe42b "3",
  0x7fffffffe42d "4", 0x7fffffffe42f "10", 0x7fffffffe432 "6",
  0x7fffffffe434 "7", 0x7fffffffe436 "8",  0x7fffffffe438 "9"}
(gdb)

En el caso de los arreglos estáticos, visualizar su contenido es más inmediato, puesto que GDB ya sabe lo que son. Simplemente hay que utilizar el comando print igual que con cualquier otro dato de tipo primitivo. Para ilustrarlo usaré el siguiente código de ejemplo:

#include <stdio.h>

int main()
{
  int arr[3] = {1, 2, 3};
  printf("Este es un buen lugar para detener el programa\n");
  return 0;
}

Contenido en un archivo llamado arreglo.c y compilado en el binario arreglo. Una vez dentro de GDB, para ver el contenido de arr basta con hacer lo siguiente:

(gdb) break 6
Punto de interrupción 1 at 0x4005c2: file arreglo.c, line 6.
(gdb) run
Starting program: /home/user/arreglo

Breakpoint 1, main () at arreglo.c:6
6         printf("Este es un buen lugar para detener el programa\n");
(gdb) print arr
$1 = {1, 2, 3}
(gdb)

Fácil ¿no? Esto aplica para arreglos de cualquier tipo, incluso de caracteres o estructuras. Haz tus propios experimentos.

Recuerda que en C puedes interpretar cualquier tipo de dato como te plazca, a esta acción se le conoce como casting, y por supuesto que está a tu disposición dentro de GDB utilizando la misma sintaxis: (tipo)expresión. Además de eso, también le puedes indicar al depurador en qué formato quieres que te imprima los datos. Observa el siguiente programa como ejemplo:

#include <stdio.h>

int main()
{
  int  num = 0x616C6F68;
  int *p = &num;
  printf("hola: 0x%X\n", num);
  return 0;
}

Compila el programa e inicia una sesión del depurador con el binario resultante. Luego fija un punto de quiebre en la línea ocho…

Leyendo símbolos desde magic...hecho.
(gdb) break 8
Punto de interrupción 1 at 0x4005bc: file magic.c, line 8.
(gdb) run
Starting program: /home/user/magic

Breakpoint 1, main () at magic.c:8
8         printf("hola: 0x%X\n", num);
(gdb) print num
$1 = 1634496360
(gdb) print /x num
$2 = 0x616c6f68
(gdb) print p
$3 = (int *) 0x7fffffffdecc
(gdb) print (char *)p
$4 = 0x7fffffffdecc "hola\314\336\377\377\377\177"
(gdb) print *((char *)p)@4
$5 = "hola"
(gdb) print *((char *)p)@sizeof(int)
$6 = "hola"
(gdb)

En este ejemplo puedes ver que al interpretar una variable como si fuera de otro tipo de dato, puedes descubrir cosas interesantes. Creo que con esto es suficiente para que sigas explorando el comportamiento de tus programas. Si quieres descubrir otras formas de visualizar la información contenida en tus variables, te recomiendo que leas la ayuda del comando: (gdb) help print. Espero que esta nota te sea útil para depurar tus programas en el futuro. ¡Hasta la próxima!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *