Depuración con GDB: Controlando la ejecución de tu código

Imagina que tienes la capacidad de detener el tiempo dentro de tu programa, congelar cada variable en un estado exacto y observar cómo fluyen los datos por la memoria paso a paso. Eso es lo que hace GDB (GNU Debugger). Es una herramienta que te permite inspeccionar un programa mientras se está ejecutando, permitiéndote ver qué está ocurriendo “bajo el capó”.

Para que GDB pueda hacer esto, no basta con el binario estándar que produce el compilador. Necesitas compilar tu código con la opción -g (o -g3 para incluir información adicional sobre macros del preprocesador). Al hacerlo, el compilador inyecta símbolos de depuración en el archivo ejecutable. Estos símbolos son como un mapa que le dice a GDB: “La dirección de memoria 0x4005a4 corresponde a la línea 15 del archivo main.c y representa la variable entera contador“. Sin este mapa, GDB solo vería direcciones de memoria y lenguaje ensamblador, dejándote prácticamente a ciegas sobre la lógica de tu código en C.

Utilizarás GDB siempre que un programa se cierre inesperadamente con un error de segmentación (Segmentation fault), cuando un bucle parezca no terminar nunca o cuando un valor cambie de forma errática sin una razón aparente. Si intentas depurar sin los símbolos de depuración (sin el -g), no perderás la capacidad de ver la memoria, pero el depurador no podrá mostrarte tus nombres de variables ni las líneas de código originales, lo que hará que la tarea sea extremadamente difícil.

Para empezar, necesitas un programa que presente un error. El siguiente ejemplo contiene un error clásico de desbordamiento de búfer que es ideal para practicar.

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

/* Función que duplica el valor de cada elemento en un array */
void duplicar_valores(int *datos, int tamano) {
    // ERROR: El bucle debe ser i < tamano. 
    // Al usar <=, accederemos a una posición de memoria fuera del array.
    for (int i = 0; i <= tamano; i++) {
        datos[i] = datos[i] * 2;
    }
}

int main(void) {
    int n = 5;
    // Asignamos memoria para 5 enteros
    int *mi_array = malloc(n * sizeof(int));

    if (mi_array == NULL) {
        fprintf(stderr, "Error al asignar memoria\n");
        return 1;
    }

    // Inicializamos el array con valores 1, 2, 3, 4, 5
    for (int i = 0; i < n; i++) {
        mi_array[i] = i + 1;
    }

    printf("Antes de duplicar:\n");
    for (int i = 0; i < n; i++) {
        printf("%d ", mi_array[i]);
    }
    printf("\n");

    duplicar_valores(mi_array, n);

    printf("Después de duplicar:\n");
    for (int i = 0; i < n; i++) {
        printf("%d ", mi_array[i]);
    }
    printf("\n");

    free(mi_array);
    return 0;
}

Para analizar este código con GDB, primero compílalo con gcc -std=c11 -Wall -Wextra -Wpedantic -g -o programa programa.c. Una vez tengas el ejecutable, entra en el depurador con gdb ./programa.

Al ejecutar run, el programa terminará con un error de segmentación. Para entender qué pasó, usa el comando backtrace (o bt); esto te mostrará la pila de llamadas, indicándote exactamente en qué función y en qué línea ocurrió el desastre. En este caso, verás que el fallo ocurre dentro de duplicar_valores.

Si quieres observar el error antes de que el programa colapse, coloca un punto de interrupción en la función con break duplicar_valores. Al ejecutar run, el programa se detendrá al entrar en la función. Ahora puedes usar next (o n) para avanzar línea a línea, o step (o s) si quieres entrar dentro de una función que se esté llamando.

Para inspeccionar el estado, usa print i para ver el valor actual del índice o print datos[i] para ver el contenido de la memoria. Si sospechas que una variable está cambiando de forma sospechosa, usa watch datos[5]. Este es un watchpoint: GDB detendrá la ejecución automáticamente en el momento exacto en que el valor de esa posición de memoria cambie.

Si quieres ver cómo se ve la memoria en bruto, usa el comando de examen x. Por ejemplo, x/5dw mi_array te mostrará los 5 elementos del array como enteros decimales (d). Si el programa ha fallado y quieres ver qué hay en la memoria justo antes de que se corrompa, x/16xb mi_array te mostrará los siguientes 16 bytes en formato hexadecimal, lo cual es vital para entender el layout de la memoria y la alineación.

Si durante la ejecución te das cuenta de que una variable tiene un valor incorrecto y quieres probar una hipótesis sin reiniciar el programa, puedes usar set variable i = 4 para cambiar el valor de i sobre la marcha y ver si eso evita el error.

El error frecuente

Un error muy común es compilar el programa sin la bandera -g y tratar de usar GDB. Si haces esto, al intentar usar break main o print mi_array, GDB te responderá con un error indicando que no tiene información para esos símbolos. Te verás rodeado de direcciones de memoria como 0x0000555555555149 y tendrás que usar el comando disassemble para ver el código ensamblador y tratar de deducir qué parte de tu código C corresponde a qué instrucción.

Si estás trabajando en un sistema de producción donde los binarios se envían sin símbolos para proteger la propiedad intelectual o reducir el tamaño, y el programa lanza un core dump (un volcado de la memoria en disco), puedes analizarlo después de que el proceso haya muerto usando gdb ./programa core. Para que el sistema genere estos archivos, asegúrate de haber ejecutado ulimit -c unlimited en tu terminal antes de correr el programa.

120

Dejar un comentario

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

Scroll al inicio