Depuración de código C++: cómo dejar de temer al segmentation fault y amar a GDB
¿Alguna vez has pasado tres horas buscando un error que al final resultó ser un break olvidado en un switch o un ; extra después de un if? Si es así, bienvenido al club. La depuración de C++ es un arte que roza el masoquismo. Pero, como cualquier arte, se puede dominar.
En este artículo, no solo veremos "cómo ejecutar GDB", sino técnicas reales que transforman la depuración de una adivinanza en un trabajo detectivesco digno de Sherlock Holmes. Hablaremos sobre sanitizadores modernos, depuración inversa e incluso cómo atrapar un error que solo se reproduce "los viernes por la tarde".
1. Clásico del género: GDB y LLDB
Empecemos por lo fundamental. GNU Debugger (GDB) y su competidor LLDB son las navajas suizas del depurador. Pero la mayoría los usa al 10% de su capacidad. Vamos a solucionarlo.
Puntos de interrupción con condiciones
¿Pones un punto de interrupción en un bucle de 10,000 iteraciones y presionas continue 9,999 veces? Detente. Usa breakpoints condicionales:
(gdb) break main.cpp:42 if i == 5000Esto te ahorrará horas de vida. En LLDB la sintaxis es similar:
(lldb) breakpoint set --file main.cpp --line 42 --condition 'i == 5000'Watchpoints: detectando cambios en variables
Imagina: tienes una variable que cambia sin que sepas dónde. Coloca un watchpoint:
(gdb) watch my_variable(gdb) rwatch my_variable # se activa al leer(gdb) awatch my_variable # se activa tanto al leer como al escribirAhora el programa se detendrá justo en el momento en que alguien toque tu variable. Magia, ¿verdad?
2. Depuración inversa: viaje en el tiempo
La peor pesadilla de un programador: el programa falló, pero no sabes cómo llegó a ese estado. La depuración inversa (reverse debugging) permite "rebobinar" la ejecución hacia atrás.
En GDB, necesitas soporte para record:
(gdb) record(gdb) continue... el programa falló ...(gdb) reverse-step # paso atrás(gdb) reverse-continue # continuar hacia atrásEn herramientas comerciales (como UndoDB o rr de Mozilla) esto funciona aún mejor. rr (Record and Replay) es una herramienta gratuita que graba la ejecución del programa y luego permite reproducirla una y otra vez:
$ rr record ./my_program$ rr replay(gdb) continue... encontramos el lugar del fallo ...(gdb) reverse-stepConsejo: usa rr para buscar condiciones de carrera (data races): reproduce cada ejecución de forma determinista.
3. Sanitizadores: vacuna contra el comportamiento indefinido
El C++ moderno (a partir de Clang y GCC 4.8+) tiene herramientas integradas que nuestros padres ni soñaban. AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan) y ThreadSanitizer (TSan) son imprescindibles.
Ejemplo: detectando una fuga de memoria
// compilar con: g++ -fsanitize=address -g main.cpp -o main#include <iostream>
void leak() { int* p = new int(42); // olvidamos delete}
int main() { leak(); return 0;}Lo ejecutamos y vemos:
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from: #0 0x7f... in operator new(unsigned long) #1 0x400... in leak() main.cpp:5 #2 0x400... in main main.cpp:10ASan mostrará la pila de llamadas exacta, incluyendo el número de línea donde ocurrió la asignación. Se acabó el "en algún lugar se fuga la memoria".
UBSan contra el comportamiento indefinido
// compilar con: g++ -fsanitize=undefined -g main.cppint main() { int x = INT_M