03. Depuración: watchpoints

Publicado por

Siempre que el depurador alcance un punto de quiebre, éste detendrá la ejecución del programa en ese punto siempre que el breakpoint esté habilitado. Por lo tanto, podemos controlar de una manera un tanto burda, cuando detenernos o no en cierta parte del programa.

Al momento de buscar la causa de un bug, es común que se nos ocurran ideas como: “creo que mi programa fallará en la línea 78” o “tal vez la falla está en la función ‘tal'”, pero ¿qué ocurre cuándo el programa no siempre falla donde suponemos o no podemos anticipar en qué circunstancias lo hace?, tal vez sabemos que alguna expresión está relacionada con la falla, así que puede ser buena idea vigilarla para obtener más información sobre el defecto que buscamos corregir.

GDB nos proporciona una herramienta que nos sirve para hacer justamente eso: un tipo de puntos de quiebre llamados watchpoints. A diferencia de los puntos de quiebre que he mencionado hasta ahora, los watchpoints no se definen en una parte específica del código fuente, sino que son asociados a una expresión. El depurador hará una pausa en cualquier parte del código en el que dicha expresión se reduzca a un valor diferente. De esta manera no necesitamos poner breakpoints en todas las líneas de código en que usamos las variables relacionadas en dicha expresión.

Para muestra, considera el siguiente código, el cual es un programa sencillo que sirve para encontrar el máximo de una serie de números en la línea de comandos:

#include <stdio.h>
#include <stdlib.h>

int maximo(int *);

int main(int argc, char *argv[])
{
  int *buffer;
  int  i;
  int  resultado;

  if (argc == 1)
  {
    fprintf(stderr, "No hay valores de entrada\n");
    exit(1);
  }

  buffer = (int *)calloc(argc, sizeof(int));

  /* Almacena cuántos números hay que examinar. */
  *buffer = argc - 1;

  for (i = 1; i < argc; i++)
    buffer[i] = atoi(argv[i]);

  resultado = maximo(buffer); 
  printf("El valor mas grande es %d\n", resultado);

  free(buffer);

  return 0;
}

int maximo(int *numeros)
{
  int i;
  int maxv;
  int tam;

  /* El primer elemento indica el número de argumentos. */
  tam = *numeros++;

  for (i = 0; i < tam; i++)
    if (maxv < numeros[i])
      maxv = numeros[i];

  return maxv;
}

Una vez compilado, el programa se utiliza así:

➜ ./maximo 5 9 8 7 6 10 1 2 3 4

Dados los datos de entrada, el resultado esperado es 10, sin embargo, en ocasiones el programa devuelve un resultado diferente. ¿Por qué?

➜ ./maximo 5 9 8 7 6 10 1 2 3 4
El valor mas grande es 10
➜ ./maximo 5 9 8 7 6 10 1 2 3 4
El valor mas grande es 264819408

La forma de crear un watchpoint es a través del comando watch. Recuerda que los watchpoints se asocian con el valor al que se reduce una expresión. En particular, el identificador de una variable es una expresión, así que podemos usar simplemente el nombre de una variable que nos interese. En este caso, la función que no está entregando el resultado correcto es maximo(), y el valor que devuelve esta función es el que toma la variable maxv.

Para establecer uno de estos puntos, necesitamos que todos los símbolos (constantes y variables) involucrados en la expresión existan dentro del ámbito alcanzable. Por lo que te recomiendo que primero fijes un breakpoint en una función desde la cual esto se cumpla. Siguiendo con nuestro ejemplo, dicha función es maximo(). Así que para generar nuestro watchpoint hay que seguir los siguientes pasos:

  1. Entrar al depurador.
  2. Fijar un breakpoint en la función maximo().
  3. Ejecutar el programa, el cual se detendrá al alcanzar el breakpoint.
  4. Desplegar el valor inicial de maxv.
  5. Fijar el watchpoint en la expresión maxv.
  6. Reanudar el programa.

➜ gdb maximo
...
(gdb) break maximo
(gdb) run 5 9 8 7 6 10 1 2 3 4
(gdb) p maxv
(gdb) watch maxv
(gdb) continue
...

Al depurar el programa con GDB, cuando obtenemos el resultado correcto, se genera una salida similar a esta:

➜ gdb maximo
...
Reading symbols from maximo...done.
(gdb) break maximo
Breakpoint 1 at 0x400794: file maximo.c, line 41.
(gdb) run 5 9 8 7 6 10 1 2 3 4
Starting program: /home/user/maximo 5 9 8 7 6 10 1 2 3 4

Breakpoint 1, maximo (numeros=0x601010) at maximo.c:41
warning: Source file is more recent than executable.
41    tam = *numeros++;
(gdb) watch maxv
Hardware watchpoint 2: maxv
(gdb) p maxv
$1 = -140086576
(gdb) continue
Continuing.
Hardware watchpoint 2: maxv

Old value = -140086576
New value = 5
maximo (numeros=0x601014) at maximo.c:43
43    for (i = 0; i < tam; i++)
(gdb) continue
Continuing.
Hardware watchpoint 2: maxv

Old value = 5
New value = 9
maximo (numeros=0x601014) at maximo.c:43
43    for (i = 0; i < tam; i++)
(gdb) continue
Continuing.
Hardware watchpoint 2: maxv

Old value = 9
New value = 10
maximo (numeros=0x601014) at maximo.c:43
43    for (i = 0; i < tam; i++)
(gdb) continue
Continuing.

Watchpoint 2 deleted because the program has left the block in
which its expression is valid.
0x000000000040075d in main (argc=11, argv=0x7fffffffe158) at maximo.c:26
26    resultado = maximo(buffer); 
(gdb) continue
Continuing.
El valor mas grande es 10
[Inferior 1 (process 25493) exited normally]
(gdb)

Por el contrario, cuando el programa falla, el depurador no alcanza el watchpoint, puesto que la variable maxv no cambia su valor:

➜ gdb maximo
...
Reading symbols from maximo...done.
(gdb) break maximo
Breakpoint 1 at 0x400794: file maximo.c, line 41.
(gdb) run 5 9 8 7 6 10 1 2 3 4
Starting program: /home/user/maximo 5 9 8 7 6 10 1 2 3 4

Breakpoint 1, maximo (numeros=0x601010) at maximo.c:41
41    tam = *numeros++;
(gdb) p maxv
$1 = 264819408
(gdb) continue
Continuing.
El valor mas grande es 264819408
[Inferior 1 (process 26060) exited normally]
(gdb)

Observa que en ambos casos, el watchpoint es eliminado automáticamente al salir de la función maximo(), puesto que fuera de ésta, la variable maxv no existe. Al examinar los valores obtenidos, podemos concluir que en este caso el defecto radica en no asignar explícitamente un valor a la variable maxv al momento de declararla.

Los watchpoints son herramientas muy útiles cuando necesitamos diagnosticar defectos que no tenemos muy bien ubicados, pero tenemos una idea de qué expresiones están involucradas ya que nos dan la flexibilidad de detener la ejecución de un programa en varios puntos sin necesidad de definir más de un breakpoint. 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 *