Guía de depuración con GDB y gestión de símbolos

Cuando el programa se comporta de forma errática o simplemente se cierra sin avisar (un “crash”), necesitas una herramienta que te permita “congelar” el tiempo y mirar qué está pasando dentro de la memoria y la CPU. Eso es gdb (GNU Debugger). Para que esto sea posible, el compilador debe incluir información adicional en el archivo ejecutable, algo que logras usando el flag -g (que genera información en formato DWARF). Si compilas sin este flag, el depurador solo verá instrucciones de procesador sin nombres de variables ni líneas de código, dejándote a ciegas. Usamos estas herramientas cuando la lógica falla pero el error es sutil, como un índice que se sale de un array por un solo elemento o una variable que cambia su valor de forma inesperada debido a otro hilo. Si intentas depurar un binario “limpio” (sin símbolos de depuración), te frustrarás porque no podrás ver el nombre de tus variables, solo direcciones de memoria incomprensibles.

Para empezar, la diferencia entre -g, -g2 y -g3 es simplemente la cantidad de información que el compilador mete en el binario: -g es lo estándar, pero -g3 puede incluir incluso información de macros de preprocesador, lo cual es útil pero hace el archivo más pesado.

#include <iostream>
#include <vector>
#include <thread>
#include <chrono>

// Esta función simula una tarea en segundo plano
void tarea_asincrona(int& contador) {
    for (int i = 0; i < 5; ++i) {
        // Queremos vigilar cuándo cambia este valor
        contador += 10; 
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

int main() {
    std::vector<int> datos = {10, 20, 30, 40, 50};
    int estado = 0;

    // Lanzamos un hilo para ver cómo interactúa con la variable 'estado'
    std::thread hilo(tarea_asincrona, std::ref(estado));

    // Error lógico intencionado: el bucle se pasa del tamaño del vector
    // El uso de i <= datos.size() provocará un acceso fuera de límites
    for (size_t i = 0; i <= datos.size(); ++i) {
        int valor = datos[i]; 
        estado += 1; // El hilo también está modificando esto
        
        std::cout << "Iteración " << i << ": " << valor << " (Estado: " << estado << ")\n";
    }

    if (hilo.joinable()) {
        hilo.join();
    }

    return 0;
}

Para compilar este ejemplo y poder depurarlo, usa:
g++ -std=c++20 -Wall -Wextra -Wpedantic -g -pthread -o depurador example.cpp

Al ejecutar gdb ./depurador, el programa se detendrá antes de empezar. Si usas run, verás que el programa falla. Aquí es donde aplicamos las herramientas:

  1. Poner paradas (break): Si quieres que el programa se detenga justo cuando el bucle esté a punto de fallar, usa break 28 (la línea del error) o break main.cpp:28. Incluso puedes poner condiciones: break 28 if i == 5.
  2. Navegar por el código: Una vez parado, usa next para saltar a la siguiente línea sin entrar en funciones (como std::cout), o step si quieres entrar dentro de la definición de una función para ver qué hace paso a paso. Si te has perdido en las entrañas de la biblioteca estándar, usa continue para seguir hasta la próxima parada o finish para salir de la función actual.
  3. Inspeccionar el estado: Usa print valor para ver el contenido de la variable actual. Si quieres que gdb te muestre el valor de estado cada vez que el programa se detenga, usa display estado. Para ver todas las variables locales en el contexto actual, usa info locals.
  4. El rastro del error (backtrace): Cuando el programa lanza un error de segmentación, lo primero que debes hacer es bt (o backtrace). Esto te mostrará la “pila de llamadas”: qué función llamó a qué función, permitiéndote ver la ruta exacta que siguió el flujo hasta el desastre. Si estás en una función profunda, usa frame N para moverte hacia arriba o abajo en la pila y examinar el estado de los parámetros en las funciones llamadas anteriormente.
  5. Vigilancia extrema (watch): Si no sabes qué parte de tu código está alterando la variable estado, usa watch estado. El depurador detendrá la ejecución en el instante exacto en que el valor de estado cambie, permitiéndote identificar si fue el hilo o el hilo principal.
  6. Inspección de memoria pura: Si sospechas que un puntero está apuntando a basura, usa x/4xb &datos[0]. Esto significa: “examina 4 elementos, en formato hexadecimal, de tamaño byte, en la dirección de la primera posición del vector”.
  7. Multihilo: En programas con hilos, info threads te mostrará todos los hilos activos. Si el problema parece venir de otro hilo, usa thread N para saltar al contexto de ese hilo específico y ver qué está haciendo.

Si prefieres un entorno más moderno o estás en macOS, lldb es el equivalente de la familia LLVM y utiliza comandos casi idénticos para la mayoría de estas operaciones.

El error frecuente
Un error clásico al empezar es compilar con optimizaciones de alto nivel (como -O3) y tratar de depurar. Cuando el compilador optimiza, aplica técnicas como el inlining (meter el código de una función directamente donde se llama) o elimina variables que considera que no son necesarias. Esto hace que, al usar gdb, veas que el programa “salta” líneas de código de forma errática o que te diga que la variable x ha sido “optimizada” (), lo que imposibilita la inspección. Siempre depura con -O0 (sin optimizar) y con -g.

9

Dejar un comentario

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

Scroll al inicio