El depurador GDB (GNU Debugger) es una herramienta esencial en entornos Linux para la depuración de aplicaciones compiladas desde la línea de comandos. Aunque herramientas gráficas como VS Code o IDEs completos ofrecen interfaces visuales que pueden parecer más intuitivas, GDB sigue siendo increíblemente potente e indispensable para la depuración en servidores remotos o entornos puramente textuales.
Archivos Core (Core Dumps)
Un archivo 'core' es un volcado de memoria de un programa en el momento de su terminación anormal. Es una instantánea del estado de la aplicación y puede ser crucial para diagnosticar la causa de un fallo. Cuando un proceso finaliza de forma inesperada, el sistema operativo puede generar un archivo core si está configurado para ello. Este archivo permite usar GDB para analizar el estado del programa justo antes del error.
Análisis con archivos Core
Cuando el sistema genera un archivo core, GDB puede usarlo para localizar rápidamente el punto de fallo:
gdb [nombre_ejecutable] [nombre_archivo_core]
# Ejemplo: gdb mi_programa core.12345
GDB cargará el archivo core y mostrará la pila de llamadas (call stack) y la línea de código donde ocurrió la interrupción, junto con el contexto relevante.
Es importante destacar que los sistemas Linux no siempre generan archivos core por defecto. Para habilitar su creación, se pueden seguir estos métodos:
- Temporalmente (para la sesión actual): Ejecutar
ulimit -c unlimiteden la terminal. - Permanentemente (a nivel de sistema): Añadir la línea
* soft core unlimitedal archivo/etc/security/limits.conf. - Permanentemente (por usuario): Añadir la misma línea o el comando
ulimit -c unlimiteda un script de inicio como~/.bash_profile.
Ejemplo Práctico de Depuración
A continuación, exploraremos las funcionalidades de GDB con un ejemplo de código C++ que contiene un error de división por cero. Este tipo de error comúnmente provoca una señal SIGFPE (Floating Point Exception), incluso si no se trata de operaciones de punto flotante.
Código C++ con Error Demostrativo
Consideremos el siguiente programa, que simula una operación que eventualmente resultará en una división por cero:
#include <iostream> // Necesario para std::cout
#include <string> // No usado, pero para demostrar un include
void perform_division_loop() {
std::cout << "Entrando a la función perform_division_loop." << std::endl;
int denominator_val = 4; // Valor inicial del denominador
for (int k = 0; k < 6; ++k) { // El bucle ejecutará 6 iteraciones
std::cout << " Iteración " << k << ", denominador actual: " << denominator_val << std::endl;
// La siguiente línea causará un error de división por cero cuando denominator_val sea 0
int temp_result = 20 / denominator_val;
denominator_val--; // Decrementa el denominador en cada iteración
}
std::cout << "Saliendo de la función perform_division_loop." << std::endl;
}
int main() {
std::cout << "Programa iniciado." << std::endl;
int var_a = 10;
int var_b = 5;
var_a = var_a + 5; // Modificación de var_a
var_b = var_a - var_b; // Modificación de var_b
perform_division_loop(); // Llamada a la función que contiene el error
std::cout << "Programa finalizado." << std::endl; // Esta línea no se alcanzará
return 0;
}
El error se producirá dentro de la función perform_division_loop. El valor de denominator_val comenzará en 4 y se decrementará en cada iteración. En la quinta iteración (cuando k=4), denominator_val se convertirá en 0, lo que provocará la división por cero.
Recuerde: Para depurar con GDB, el programa debe compilarse con la opción -g para incluir la información de depuración. Por ejemplo: g++ -g mi_programa.cpp -o mi_programa.
Comandos Fundamentales de GDB
Aquí se presenta una lista de los comandos más utilizados en GDB:
listol [línea | función]: Muestra el código fuente. Se puede especificar un número de línea o el nombre de una función.runor: Ejecuta el programa.nexton: Ejecuta la siguiente línea de código, sin entrar en las llamadas a funciones.stepos: Ejecuta la siguiente línea de código, entrando en las llamadas a funciones.breakob [línea | función]: Establece un punto de interrupción en un número de línea o al inicio de una función.info breakpointsoi b: Muestra información sobre todos los puntos de interrupción activos.delete breakpoints [número]: Elimina uno o todos los puntos de interrupción (si se especifica un número).disable breakpoints [número]: Desactiva uno o todos los puntos de interrupción.enable breakpoints [número]: Reactiva uno o todos los puntos de interrupción.continueoc: Reanuda la ejecución del programa hasta el siguiente punto de interrupción o hasta que finalice.finish: Ejecuta el resto de la función actual y detiene la ejecución al regresar al punto de llamada.print [expresión]op [variable]: Imprime el valor de una variable o el resultado de una expresión.set variable [variable]=[valor]: Modifica el valor de una variable.display [variable]: Agrega una variable a la lista de "display", mostrando su valor cada vez que el programa se detiene.undisplay [número]: Elimina una variable de la lista de "display".until [línea]: Ejecuta el programa hasta alcanzar un número de línea específico dentro de la función actual.backtraceobt: Muestra la pila de llamadas (call stack), indicando las funciones activas y sus parámetros.info localsoi locals: Muestra los valores de las variables locales en el frame actual de la pila.quitoq: Sale de GDB.
Proceso de Depuración Detallado
1. Iniciar GDB
Para comenzar, ejecuta GDB con el nombre de tu programa:
gdb mi_programa
Verás el prompt de GDB: (gdb)
2. Establecer un Punto de Interrupción
Es una buena práctica establecer un punto de interrupción al inicio de la función main para controlar el flujo del programa desde el principio:
(gdb) b main
Breakpoint 1 at 0x...: file mi_programa.cpp, line 23.
El mensaje indica que se ha creado el punto de interrupción 1 en la línea 23 del archivo mi_programa.cpp.
3. Ejecutar el Prorgama
Ahora, ejecuta el programa. GDB lo ejecutará hasta el primer punto de interrupción que encuentre:
(gdb) r
Starting program: /ruta/a/mi_programa
Breakpoint 1, main () at mi_programa.cpp:23
23 std::cout << "Programa iniciado." << std::endl;
El programa se ha detenido en la primera línea de la función main, tal como se esperaba.
4. Navegación Paso a Paso (Step y Next)
Para inspeccionar el flujo de ejecución, se utilizan los comandos next y step. Podemos usar list para ver el código en el punto actual.
(gdb) list
18 }
19
20 int main() {
21 std::cout << "Programa iniciado." << std::endl;
22 int var_a = 10;
23 int var_b = 5;
24 var_a = var_a + 5;
25 var_b = var_a - var_b;
26 perform_division_loop();
(gdb) n
Programa iniciado.
22 int var_a = 10;
(gdb) n
23 int var_b = 5;
Cuando llegamos a la línea que llama a perform_division_loop(), podemos elegir cómo proceder:
n(next): Ejecutará la función completa y se detendrá en la siguiente línea delmain(línea 27). No entrará en el código deperform_division_loop.s(step): Entrará en la funciónperform_division_loopy se detendrá en su primera línea de código (línea 10).
Para este ejemplo, queremos depurar la función con el error, así que usaremos s:
(gdb) s
perform_division_loop () at mi_programa.cpp:10
10 std::cout << "Entrando a la función perform_division_loop." << std::endl;
Ahora estamos dentro de perform_division_loop. Podemos seguir usando n para avanzar línea por línea y observar el valor de las variables.
5. Inspección de Variables
Para ver el valor de una variable en cualquier momento, se usa el comando print:
(gdb) p denominator_val
$1 = 4
(gdb) n
Iteración 0, denominador actual: 4
13 int temp_result = 20 / denominator_val;
(gdb) n
14 denominator_val--;
(gdb) p denominator_val
$2 = 4
(gdb) n
12 std::cout << " Iteración " << k << ", denominador actual: " << denominator_val << std::endl;
(gdb) p denominator_val
$3 = 3
También podemos usar display para que GDB muestre el valor de una variable automáticamente cada vez que el programa se detiene:
(gdb) display denominator_val
1: denominator_val = 3
(gdb) n
Iteración 1, denominador actual: 3
1: denominator_val = 3
13 int temp_result = 20 / denominator_val;
<p>Si continuamos avanzando, GDB se detendrá inesperadamente cuando ocurra el error:</p>
(gdb) c
Continuing.
Iteración 0, denominador actual: 4
Iteración 1, denominador actual: 3
Iteración 2, denominador actual: 2
Iteración 3, denominador actual: 1
Iteración 4, denominador actual: 0
Program received signal SIGFPE, Arithmetic exception.
0x0000555555555193 in perform_division_loop () at mi_programa.cpp:13
13 int temp_result = 20 / denominator_val;
GDB nos indica claramente que se ha recibido la señal SIGFPE (Excepción Aritmética) en la línea 13, confirmando el problema de división por cero.
6. Análisis de la Pila de Llamadas (Backtrace)
Cuando el programa se detiene debido a un error, el comando backtrace (o bt) es invaluable para entender qué funciones llevaron al punto del fallo:
(gdb) bt
#0 0x0000555555555193 in perform_division_loop () at mi_programa.cpp:13
#1 0x00005555555551fa in main () at mi_programa.cpp:26
<p>La salida muestra que main() llamó a perform_division_loop(), y el error ocurrió dentro de perform_division_loop en la línea 13. Esto es muy útil para rastrear el origen de los problemas en programas complejos.</p>">