Depurar con GDB: guía completa para analizar y corregir programas

GDB (GNU Debugger) es una herramienta muy usada para depurar programas, especialmente aquellos escritos en C o C++ (aunque también se puede usar con otros lenguajes). La depuración es un proceso que nos permite mirar paso a paso la ejecución de un programa para encontrar y corregir posibles errores que hayamos cometido durante su desarrollo.

GDB nos permite depurar de forma integral un programa, ejecutándolo paso a paso, haciendo un seguimiento a ciertas invocaciones, o comprobando el valor de algunas variables. Depurar programas en C con GDB es una habilidad fundamental para cualquier programador que quiera entender a fondo el comportamiento de su código y solucionar errores complejos.

Qué permite hacer GDB

Cuando nuestro programa no funciona como esperamos, la depuración nos permite inspeccionar paso a paso qué está ocurriendo internamente y corregir esos fallos que a veces se nos escapan con técnicas más básicas como el uso de printf. GDB te permite:

  • Ejecutar tu programa paso a paso para ver cómo se comporta.
  • Poner puntos de interrupción (breakpoints) donde quieras pausar la ejecución.
  • Inspeccionar variables y ver qué valores tienen en cierto momento.
  • Modificar valores en tiempo de ejecución (por ejemplo, cambiar el valor de una variable mientras el programa está pausado).
  • Ver la pila de llamadas (call stack) para saber cómo llegaste a cierta función.
  • Detectar errores como segmentation faults y ver en qué línea exacta falló el programa.

Compilar un programa con información de depuración

Para empezar a depurar con GDB, lo primero debemos compilar nuestro programa con la opción -g. Esta opción le indica al compilador GCC que incluya información adicional en el ejecutable, como los nombres de las variables y metadatos que GDB utilizará para mostrarnos el estado del programa de forma clara y precisa.

Aunque esto hace que el archivo resultante sea un poco más grande, es imprescindible para poder aprovechar todas las funcionalidades del depurador. Para poder depurar tu programa, debes compilar con la opción -g, que incluye información de depuración al ejecutable que el depurador utiliza.

Por ejemplo:

g++ -Wall -g programa.cc -o programa

Una vez compilado con -g, podemos lanzar GDB pasando el ejecutable como argumento. Esto nos abre una interfaz de texto donde podemos introducir comandos para controlar la ejecución del programa.

Iniciar GDB y consultar el código fuente

Uno de los comandos más útiles para comenzar es list, que nos muestra el código fuente del programa, permitiéndonos ver en qué línea estamos trabajando. Podemos usarlo sin argumentos para ver las primeras líneas, o pasarle un número o el nombre de una función para situarnos en un punto concreto.

También tienes un modo gráfico para ejecutar GDB, añadiendo el argumento -tui. También se puede activar/desactivar pulsando Ctrl + X + A sin necesidad del argumento -tui.

Breakpoints o puntos de interrupción

Aquí es donde entran en juego los breakpoints o puntos de ruptura. Un breakpoint es una instrucción que le damos a GDB para que detenga la ejecución del programa cuando llegue a una línea o función determinada.

Lo primero que debes hacer es poner un punto de parada para poder controlar el flujo del programa y revisar el estado de las variables. Si no pones un punto de parada, cuando ejecutes el código con la instrucción run, se ejecutará tu código y no parará hasta que el programa finalice o hasta que se encuentre con un error.

Por ejemplo, si queremos que el programa se detenga al entrar en la función leerNumero, escribimos:

break leerNumero

Cuando ejecutamos run después de poner este breakpoint, el programa se ejecutará hasta que llegue a esa función y se pausará, devolviéndonos el control en la consola de GDB.

También podrías haber puesto un punto de parada en una línea de código correspondiente al bucle. Por ejemplo la línea 8, que es donde se encuentra el cout, y tras poner el punto de parada, ejecutar run si todavía no has iniciado la ejecución o continue en caso de que ya esté iniciado:

break ejemplo.cc:8

Si necesitas eliminar un breakpoint, puedes usar clear seguido del nombre de la función o la línea donde está puesto:

clear nombre_de_funcion

Ejecutar el programa dentro de GDB

Para ejecutar el programa dentro de GDB, usamos el comando run. Esto inicia la ejecución normal, pero con la ventaja de que podemos controlar el flujo y detenernos en puntos específicos.

Cuando ejecutamos run después de poner este breakpoint, el programa se ejecutará hasta que llegue a esa función y se pausará, devolviéndonos el control en la consola de GDB. Si no pones un punto de parada, cuando ejecutes el código con la instrucción run, se ejecutará tu código y no parará hasta que el programa finalice o hasta que se encuentre con un error.

Para comenzar la depuración justo en la función main, podemos usar el comando start main, que pone un breakpoint temporal en main y ejecuta el programa hasta ese punto.

Avanzar paso a paso

Para avanzar en la ejecución, GDB nos ofrece dos comandos muy importantes: next y step. El comando next ejecuta la siguiente línea de código sin entrar en funciones llamadas, mientras que step entra dentro de las funciones para que podamos depurar línea a línea dentro de ellas.

next ejecuta la siguiente línea completa de código, sin meterse en funciones llamadas desde esa línea. Si es una función, la ejecuta y pasa a la siguiente línea. step ejecuta la siguiente línea, y si es una llamada a función, entra dentro de ella para poder depurar línea por línea dentro de esa función.

Por ejemplo, si estamos en una llamada a leerNumero y queremos ver qué ocurre dentro, usamos step. Si solo queremos avanzar sin entrar en detalles, usamos next.

Además, si queremos repetir el último comando, simplemente pulsamos Enter sin escribir nada, lo que agiliza la depuración.

Continuar, salir de funciones y finalizar la ejecución

Cuando queremos que el programa continúe su ejecución hasta el siguiente breakpoint o hasta que termine, usamos continue.

Si nos hemos metido dentro de una función y queremos salir rápidamente hasta que termine, sin ir línea a línea, usamos finish, que ejecuta el resto de la función y nos devuelve al marco anterior.

Para salir de GDB, utiliza el comando quit o q.

Consultar variables y expresiones

En ese momento, podemos inspeccionar el estado del programa, ver los valores de las variables y decidir cómo continuar. Para inspeccionar variables y expresiones, el comando print es esencial.

Podemos ver el valor de una variable con:

print x

También podemos evaluar expresiones más complejas, como multiplicaciones o casteos:

print 5 * n + 5

print (char) n

Si queremos saber el tipo de una variable, usamos ptype seguido del nombre de la variable:

ptype x

GDB permite modificar valores en tiempo de ejecución, por ejemplo, cambiar el valor de una variable mientras el programa está pausado.

Examinar la pila de llamadas

Durante la depuración, si nos hemos metido dentro de varias funciones y queremos saber en qué punto del programa estamos, el comando where nos muestra la pila de llamadas o stack trace, indicándonos el camino que ha seguido la ejecución hasta el momento.

También puedes utilizar el comando backtrace para mostrar la pila de llamadas:

backtrace

Podemos navegar por los distintos marcos de la pila con up y down, para ver variables locales en diferentes contextos.

Ejemplo: detectar un segmentation fault

Fíjate en el siguiente ejemplo: tenemos un vector de 3 elementos, y después mostramos en consola con cout todos esos elementos.

#include <iostream>

using namespace std;

int main() {

int numeros[3] = {10, 20, 30};

for (int i = 0; i <= 3; i++) {

cout << "Elemento " << i << ": " << numeros[i] << endl;

}

return 0;

}

Al ejecutar este código, nos damos cuenta de que algo sucede, ya que salta el típico error segmentation fault. Este error aparece cuando intentamos acceder a una zona de memoria que no está reservada por nuestro programa.

Vamos a intentar depurar este código paso a paso:

  1. Compilar con -g:
  2. Abrir GDB en consola indicándole el ejecutable que quieres depurar.
  3. Poner un punto de parada para controlar el flujo del programa y revisar el estado de las variables.
  4. Comenzar la ejecución del código con run.
  5. Avanzar línea por línea con next o step.
  6. Consultar el estado de la variable i y los valores del vector.

En el momento que la ejecución esté paralizada, puedes ver el estado de las variables y avanzar línea por línea.

Una vez en el bucle, puedes ver el estado de la variable i o incluso los valores del vector:

print i

print numeros[i]

Fíjate en el valor de la variable i en todo momento y compárala con el tamaño del vector. Si ejecutas paso a paso con next o step, encontrarás que en un momento dado salta un error en el bucle.

Ejecutar instrucciones durante la depuración

Si necesitas probar una línea de código que no se encuentra en tu programa, puedes ejecutarlo con la instrucción call, que ejecuta lo que le indiques como si estuviese en el propio programa que se está depurando:

call std::cout << "Hola desde GDB" << std::endl

Tabla de comandos esenciales de GDB

ComandoDescripción
break o bPone un breakpoint por función o por línea.
break mainPone un breakpoint en la función main.
break imprimirPone un breakpoint en la función llamada imprimir.
run o rInicia la ejecución del programa.
next o nEjecuta la siguiente línea sin entrar a funciones.
step o sEjecuta la siguiente línea y entra en funciones.
print xMuestra el valor de la variable x.
ptype xMuestra el tipo de la variable x.
backtraceMuestra la pila de llamadas.
whereMuestra la pila de llamadas o stack trace.
continueContinúa la ejecución hasta el siguiente breakpoint.
finishEjecuta el resto de la función y vuelve al marco anterior.
quit o qSale de GDB.

Depurar con una interfaz gráfica

Como imaginarás, muchos interfaces de desarrollo integrado (IDEs) incluyen una manera de depurar más gráfico que el modo consola de GDB. Se puede depurar de la misma manera, pero podría considerarse más intuitivo depurar utilizando un editor de texto.

Depurar con Visual Studio Code

Para poder depurar en Visual Studio Code (VSCode), tienes que tener instalada la extensión correspondiente para C++.

Una vez instalada la extensión, debemos de abrir la carpeta donde se encuentra el código. Si abres únicamente el fichero, no te funcionará la depuración. Debes abrir la carpeta.

Después, si es la primera vez que depuramos nuestro proyecto, debemos configurar la depuración. Para ello, necesitamos el fichero tasks.json, que VSCode lo genera automáticamente por defecto al comenzar la primera depuración.

Para ello, primero abre el fichero donde se encuentra tu código y pulsa F5. Elige la opción de compilar y depurar con g++. Cuidado: no selecciones gcc (compilador de C, no de C++).

Si te fijas, se habrá creado una carpeta oculta llamada .vscode. En ese directorio VSCode guardará algunos archivos de configuración, incluido el tasks.json que comentábamos antes.

Estos son los argumentos que se añadirán al comando del compilador para compilar tu código. Fíjate que puedes encontrar el argumento -g, que decíamos antes que es necesario para poder depurar el programa después de compilarlo.

Por defecto, si solo tienes un único fichero de código, bastaría con abrir tu fichero en VSCode y pulsar F5. Con esto compilaría tu código y comenzaría en modo depuración.

Sin embargo, si tienes más de un fichero de código que deben ser compilados, deberías modificar esta línea. Si no modificas tasks.json, VSCode solo tratará de compilar el fichero abierto en ese momento, y por lo tanto no funcionará.

En este caso, tienes dos opciones:

  1. Añadir manualmente todos los ficheros .cc a compilar.
  2. Si tienes todo tu código en la misma carpeta y solo tienes un único main en todo el código, puedes decirle a VSCode que utilice todos los ficheros con extensión .cc, poniendo "*.cc" en los argumentos del compilador.

Si te fijas, también está el argumento -o, seguido por otro argumento que representaría el nombre del ejecutable que se genera. Por defecto, el fichero tendrá configurado lo siguiente: "${fileDirname}/${fileBasenameNoExtension}", que indica que se generará un ejecutable con el mismo nombre que el fichero de código en el que has pulsado la tecla F5 para depurar.

Asegúrate de que solo tengas una función main para que esto funcione. Una vez configurado todo lo anterior, ya podrás depurar tu código.

Deberás poner breakpoints en el código para que se pare el flujo del programa y puedas revisar el estado de las variables y ejecutar paso a paso, similar a cómo lo hacemos con GDB. Recuerda que VSCode internamente utiliza GDB.

Mientras estás depurando, encontrarás algunas opciones en la parte superior de VSCode, con las que podrás controlar el flujo del programa, similar a cómo funcionaba GDB. Funcionan exactamente igual.

Depurar con Data Display Debugger

Otra alternativa es utilizar DDD, que lo tienes instalado en el laboratorio de la EPS. Se trata de un interfaz gráfico para depurar.

La forma de trabajar con él es:

  1. Se compila manualmente el programa con el argumento -g.
  2. Se abre DDD y se carga el ejecutable generado.
  3. Se añaden breakpoints y se depura, parecido a VSCode.

Al iniciarlo, verás que tiene un interfaz bastante sencillo. Si entras en el menú File, verás que tienes varias opciones para cargar tu programa. Entra en Open program y selecciona tu programa generado tras compilarlo con -g previamente.

Una vez cargado, podrás incluir breakpoints y controlar el flujo del programa. Verás que tienes muchas opciones a la hora de controlar el flujo. Entre las opciones, aparece Run, Next y Step, que ya las conoces.

Fíjate que en la parte inferior tienes una consola donde puedes escribir comandos de GDB.

Depuración desde la línea de comandos y desde interfaces

GDB es el depurador por excelencia para C++ y de algunas interfaces que podemos utilizar para depurar, que utilizan internamente GDB.

Se puede depurar de la misma manera, pero podría considerarse más intuitivo depurar utilizando un editor de texto. Hay más alternativas para depurar código en C++. Un ejemplo es Nemiver, que puedes instalarlo en tu ordenador personal, y funciona similar a DDD.

Buenas prácticas para depurar

No tengas miedo de repetir la depuración las veces que haga falta para intentar detectar el error. Es posible, que si depuras el código, no localices exactamente dónde está el error, pero pueda darte una pista de dónde podría estar.

En este caso, es recomendable repetir la depuración centrándote en la parte sospechosa de código. Lo importante es que en cada depuración que hagas, tu objetivo sea intentar acotar la zona del código sospechosa, en la que dedicar la mayor parte de tu esfuerzo a la hora de detectar y corregir el error.

Para poder depurar correctamente, tienes que tener claro algo importante: debes saber a ciencia cierta qué es lo que esperabas obtener con tu código para poder compararlo con lo que realmente estás obteniendo.

Si no tienes claro lo que deberías obtener, que puede ser porque no entiendes bien tu programa o porque no entiendes algo de la especificación, te va a ser muy difícil detectar errores de ejecución.

flujo de depuración con GDB

Como depurar con gdb

tags: #depurar #con #gubed

Publicaciones populares: