Programación: el depurador gdb

El complemento perfecto de GNU gcc es GNU gdb, para el proceso de debugging. La depuración de software es otro de las etapas esenciales a la hora de crear un programa. Si lo recuerdas, era el proceso para poder identificar y corregir bugs presentes en tu código fuente. No se trata de solventar problemas de sintaxis u otro tipo de errores al tipear el código, sino de errores lógicos.

Es decir, cuando creas un código fuente, los errores de sintaxis, cuando olvidas declarar alguna variable, etc., de eso te avisa el propio compilador, que lanzará mensajes de error o advertencias y la compilación fallará. Una vez consigues pasar esa etapa, la siguiente sería la depuración. A pesar de que el compilador haya creado el binario, eso no quiere decir que tu software esté libre de errores. Es posible que se produzcan problemas que solo se den con ciertos eventos puntuales, desbordamientos, etc. A eso te ayudará el depurador…

¿Qué es gdb?

El depurador de GNU se llama gdb (Gnu DeBugger) y también ha sido creado inicialmente por Richard Stallman para *nix. Al igual que gcc, a no ser que se instale algún front-end, no dispone de una GUI. Pero si prefieres trabajar en modo gráfico, puedes usar GDBtk/Insight, DDD, etc., o IDEs como KDevelop, etc. Yo en los ejemplos usaré el modo texto.

Puedes complementar gdb con herramientas como strace, Valgrind, etc.

Para depurar, debes usar una opción especial de gcc para que incluya en el ejecutable información para gdb necesaria para la depuración, de lo contrario no funcionará. Para ello, simplemente usa la opción -g:

gcc -o hola hola.c -g

No usar dicha opción va a producir que gdb nos diga en su salida que no encuentra los símbolos de depuración… Por cierto, te invito a usar ls -l para comprobar qué sucede con el tamaño del binario compilado sin -g y con -g.

Un ejemplo práctico de uso

Si tienes gdb instalado, puedes entrar en él invocándolo desde la consola, obtener ayuda y salir con:

gdb

(gdb) help
(gdb) quit

Cuando entras en el depurador, te aparece un nuevo prompt (gdb) mientras estés dentro para interactuar con él, como has podido comprobar.

Por ejemplo, puedes usar este código de aquí que encadena unos datos introducidos para formar una línea al final con dicha información. Es muy sencillo para que comprendas bien el funcionamiento de gdb:

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

int main()
{
          char Saludo[]="Hola AT";
          char Nombre [50];
          int Edad;

          printf("Introduce tu nombre: ");
          fgets(Nombre, 27, stdin);
          
          printf("Introduce tu edad: ");
          scanf("%d", &Edad);

          printf("%s %s de %d de Edad \n", Saludo,Nombre,Edad);
          return 0;
}

Este código tiene la sintaxis correcta Y NO TIENE PROBLEMAS, por lo que gcc lo compilará sin mostrar mensaje de alerta. Pero quiero usarlo más que nada para mostrarte cómo manejarte dentro de gdb. Pudes compilarlo y seguir los ejemplos que muestro.

Ya sabes que gdb usará tanto el fichero binario como el fichero fuente de tu programa. Por eso es importante que ambos estén en el mismo directorio.

Lo deberías compilar y luego usar gdb para depurarlo:

gcc cadena.c -o cadena -g
gdb cadena

Ahora puedes usar por ejemplo list para que muestre las líneas de código de 10  en 10:

 (gdb) list

También puedes pedirle que muestre una línea concreta (y unas cuantas que le preceden y otras cuantas que le siguen a la que hayas seleccionado):

 (gdb) list 4

Si quieres comenzar a analizar el código para depurarlo, puedes usar:

 (gdb) start

Si te fijas, ha pasado por alto todas las primeras líneas, es decir, ha comenzado a partir de la primera llave en adelante. Si quieres continuar, puedes usar next para que avance:

 (gdb) next

Recuerda que puedes usar también la opciones abrevadas. Por ejemplo, en vez de next, puedes usar simplemente n

Y así hasta que quieras. Te irán apareciendo líneas y también te aparecerán las opciones interactivas del programa. Por ejemplo, en este caso, si sigues haciendo next, llegarás al mensaje que te pide tu edad, tu nombre, etc., y los podrás ir introduciendo desde gdb para analizar su comportamiento.

Si al final muestra un mensaje de de error sobre libc o similar, no te preocupes, es normal. Eso no indica nada, puedes pasarlo por alto.

Puedes usar también step para ir paso a paso, y si lo ejecutas tras una función, como por ejemplo, tras el primer printf, te puede lanzar un mensaje porque no encuentra dicha función. Eso también es normal, puesto que no se encuentra en el código fuente, sino que se encuentra en una biblioteca, en este caso en stdio.h.

(gdb) start
(gdb) n
(gdb) n
. . .
(gdb) step
(gdb) continue

Imagina que quieres crear un punto de ruptura en la línea 10, por ejemplo, es decir, en la del primer printf. Si luego usas run, arrancará hasta ese punto de ruptura, en vez de ir paso a paso. Por ejemplo:

(gdb) break
(gdb) run

Para borrar el punto de ruptura puedes usar delete:

(gdb) delete

Incluso puedes cambiar el valor de las variables. Por ejemplo, la variable Nombre, lo podemos alterar aunque ya hayamos añadido otro anteriormente. Para eso:

(gdb) set Nombre="hola"
(gdb) print Nombre

Si quieres obtener datos de una variable, como su longitud, o su tipo, puedes usar también:

(gdb) print strlen(Nombre)
(gdb) ptype Nombre

Puedes poner alertas para que cuando se modifique el valor de una variable nos muestre un aviso:

(gdb) watch Nombre
(gdb) info watch

Si en un momento dado quieres trabajar con el shell y «salir» momentáneamente de gdb para ejecutar cualquier comando, puedes usar, por ejemplo:

(gdb) shell ls
(gdb) shell cd ..

Y también crear lotes y ejecutarlo invocando el nombre que le hayamos dado como se muestra al final:

(gdb) define ejemplo_lote
>shell ls
>shell cat hola.c
>list
>run
>end
ejemplo_lote

Es un poco complicado mostrar esto así, en mi curso de Linux lo muestro en un vídeo de forma bastante más intuitiva… Pero bueno, he intentado hacerlo lo mejor posible. De todas formas, recuerda que solo quiero que aprendas a manejarte dentro de gdb y próximamente veremos más ejemplos en esta serie de posts.

Un ejemplo práctico de depuración

Ahora que ya sabes moverte por gdb, puedes hacer tu primera depuración sencilla. Por ejemplo, un caso de código fuente que aparentemente está correcto, que pasaría el escrutinio de gcc, pero que presenta un error al mostrar factoriales erróneos. Aquí es donde verás mejor la utilidad de la depuración.

El código factorial.c es:

#include <stdio.h>

int main(void)
{
          int n, factorial, i;
          do {
                     printf("Ingrese un número: ");
                     scanf("%d", &n);
          } while(n<0);
         for(i=1; i<=n; i++){
                     factorial=factorial*i;
         }
         printf("%d! = %d", n, factorial);
         return 0;
}

Un simple programa que pedirá un número e irá haciendo una cadena de multiplicaciones hasta conseguir el factorial. Es decir, si le ponemos 5, irá multiplicando 1 por 2, por 3, por 4, por 5, y debería dar 120. Si lo compilas y lo ejecutas, verás que el resultado que da no es el adecuado:

gcc -o factorial factorial.c -g
./factorial

¡Algo le pasa y gcc no se ha enterado! Pero para eso está el depurador…  Y aquí están los pasos:

gdb factorial
(gdb)

Vamos paso a paso, para no complicar mucho todo, voy a ir poniendo todos los pasos con comentarios diciendo qué hace cada comando (te aconsejo que lo vayas haciendo en tu sistema con el fichero fuente abierto justo en la ventana de al lado para que puedas ver el código completo y el resultado de gdb):

#Ejecutamos
(gdb )run

#Crea un punto de ruptura en la línea 4
(gdb) b 4

#Sigue como has aprendido con next hasta que te pida el valor de n
...

#Muestra el valor de la variable n y comprueba que vale lo mismo que tú has introducido y no ha variado
(gdb) print n

#Haz lo mismo con la otra variable factorial
(gdb) print factorial

#Como puedes ver, el valor de n es correcto, pero el de factorial es un disparate. ¡Aquí está el problema de este programa! ¡Hemos dado con el bug! La i no se ha iniciado a 0 y ha tomado un valor almacenado anteriormente que genera todo el problema...

#Puedes salir de gdb y arreglar el problema 
(gdb) quit 

Así puedes ir comprobando el valor que toman las variables en cada paso del programa y determinar si es causa de alguna de ellas, como en este caso. ¿Cómo arreglarías este desperfecto? Muy sencillo, para que factorial no se dispare al inicio, debes iniciar factorial=1, para que esté en un valor conocido y genere el resultado correcto:

#include <stdio.h> 

int main(void) 
{ 
           int n, factorial=1, i; 
           do { 
                      printf("Ingrese un número: "); 
                      scanf("%d", &n); 
           } while(n<0); 
          for(i=1; i<=n; i++){ 
                      factorial=factorial*i; 
          } 
          printf("%d! = %d", n, factorial); 
          return 0; 
}

Ahora compila nuevamente y obtendrás el binario que debe calcular correctamente:

rm factorial
gcc -o factorial factorial.c
./factorial

Espero que lo hayas entendido…

Isaac

Apasionado de la computación y la tecnología en general. Siempre intentando desaprender para apreHender.

Deja un comentario

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

A %d blogueros les gusta esto:

Si continuas utilizando este sitio aceptas el uso de cookies. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar