- Compila siempre con -g o -ggdb para que GDB muestre líneas, variables y funciones de forma legible.
- Usa breakpoints (simples y condicionales) junto con run, next, step y continue para controlar la ejecución.
- Combina print, list, backtrace y watchpoints para inspeccionar variables, memoria y pila de llamadas.
- Aprovecha atajos de teclado y comandos info para depurar más rápido programas complejos en C y C++.
Si programas en C o C++ tarde o temprano te vas a topar con bugs que no se dejan cazar solo a base de printf y prueba‑error. En ese punto es cuando GDB deja de ser “ese comando raro” y se convierte en una herramienta imprescindible para entender qué está haciendo realmente tu programa línea a línea.
En esta guía completa vamos a ver cómo usar GDB con soltura: desde cómo compilar tu código con símbolos de depuración, hasta cómo colocar breakpoints, inspeccionar variables, moverte por la pila y aprovechar atajos de teclado. Todo desde la terminal, con ejemplos prácticos y explicaciones claras, para que puedas depurar como es debido y dejes de perseguir errores a ciegas.
Qué es GDB y por qué merece la pena usarlo
GDB (GNU Debugger) es el depurador estándar del ecosistema GNU y se utiliza principalmente con C y C++, aunque también soporta otros lenguajes. Funciona sobre ejecutables ya compilados y te permite “asomarte” al interior de tu programa mientras corre, ver qué pasa cuando se cuelga y manipular su ejecución.
A diferencia de depurar con mensajes por pantalla, GDB permite detener el programa en cualquier punto, examinar memoria, variables, stack, cambiar valores sobre la marcha y reanudar la ejecución. Todo esto se hace mediante una interfaz de línea de comandos muy veterana pero tremendamente potente.
GDB existe desde los inicios de Unix y, como otras herramientas clásicas tipo gcc o g++, nació para trabajar directamente en terminal, sin interfaz gráfica. Hoy hay frontends con ventanas y botones, pero una vez dominas los comandos básicos, la mayoría de desarrolladores termina siendo más rápido con GDB “a pelo”.
Un escenario típico donde GDB brilla es cuando tu programa explota con un temido Segmentation fault (SIGSEGV). Sin depurador solo ves que algo ha accedido a memoria inválida; con GDB puedes saber en qué línea ha petado, qué función estaba en curso y qué contenía cada variable importante en ese momento.
Compilar programas para depurarlos con GDB

Para que GDB pueda trabajar a gusto, el ejecutable debe incluir información de depuración: nombres de variables, funciones, líneas de código fuente, etc. Eso se consigue añadiendo opciones concretas al compilador, como explica nuestro tutorial completo del comando gcc y sus opciones clave.
Lo mínimo es compilar con la opción -g. Por ejemplo, si tienes un archivo crash.c con un bug, puedes generar un binario llamado crash con:
gcc -g -o crash crash.c
La opción -g no cambia la lógica del programa, solo añade metadatos para que GDB pueda mostrarte el código fuente, las líneas exactas y los nombres reales de las variables en los stack frames. El ejecutable pesa un poco más, pero merece totalmente la pena.
En proyectos donde te interese usar el estándar C99 y seguir depurando cómodamente, puedes combinar opciones como:
gcc -std=c99 -g -o test test.c
Si usas C++ o quieres una integración más afinada con GDB, muchas guías recomiendan -ggdb, que genera información de depuración específica para este depurador:
g++ -ggdb -o programa main.cpp otras.cpp
Resumiendo: siempre que vayas a depurar un binario con GDB, asegúrate de haberlo compilado con -g o -ggdb; de lo contrario verás direcciones de memoria y símbolos crípticos en lugar de funciones y líneas legibles.
Primer contacto: iniciar GDB y ejecutar tu programa
Una vez tienes el ejecutable con símbolos, puedes arrancar GDB de dos formas: lanzando el depurador “vacío” o indicándole ya el binario que quieres analizar. La segunda es la que más se usa: arrancas y depuras el ejecutable en un único paso.
Para cargar y depurar, por ejemplo, un binario llamado crash:
gdb crash
Verás un encabezado con la versión de GDB, copyright y aviso de licencia, y luego el prompt típico:
(gdb)
Desde ese prompt puedes ejecutar tu programa con el comando run (o simplemente r):
(gdb) run
Starting program: ./crash
Si tu programa espera entrada estándar, GDB se la pasará tal cual. Si en lugar de indicar el ejecutable al inicio has lanzado solo gdb, puedes asociar un binario más tarde con el comando file:
(gdb) file memsim
Para salir de GDB basta con escribir quit o su abreviatura q. Si el programa se está ejecutando, GDB te pedirá confirmación para terminar la depuración.
Ejemplo práctico: cazando un Segmentation fault
Veamos un ejemplo clásico para entender el flujo de trabajo. Imagina un programa en C que pide un número, calcula la suma de 1 a n y muestra el resultado, pero contiene un fallo de memoria:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char *buf;
int sum_to_n(int num) {
int i, sum = 0;
for (i = 1; i <= num; i++)
sum += i;
return sum;
}
void printSum() {
char line;
printf("enter a number:\n");
fgets(line, 10, stdin);
if (line != NULL)
strtok(line, "\n");
sprintf(buf, "sum=%d", sum_to_n(atoi(line)));
printf("%s\n", buf);
}
int main() {
printSum();
return 0;
}
Está claro que algo huele raro, pero si lo ejecutas tal cual basta con compilarlo y correrlo para ver el desastre:
gcc -g -o crash crash.c
./crash
enter a number:
5
Segmentation fault
El sistema solo avisa de un acceso inválido a memoria, nada más. Ahora, si arrancas el programa dentro de GDB puedes ver mucho más detalle. Carga el binario y ejecútalo:
gdb crash
(gdb) run
Starting program: ./crash
enter a number:
10
Cuando se produce el fallo, GDB te lo notifica con la señal correspondiente:
Program received signal SIGSEGV, Segmentation fault.
0x0017fa24 in _IO_str_overflow_internal () from /lib/tls/libc.so.6
Para saber qué te ha llevado hasta ahí, usas el comando backtrace (o bt):
(gdb) backtrace
#0 0x0017fa24 in _IO_str_overflow_internal() from /lib/tls/libc.so.6
#1 0x0017e4a8 in _IO_default_xsputn_internal() from /lib/tls/libc.so.6
#2 0x001554e7 in vfprintf() from /lib/tls/libc.so.6
#3 0x001733dc in vsprintf() from /lib/tls/libc.so.6
#4 0x0015e03d in sprintf() from /lib/tls/libc.so.6
#5 0x08048487 in printSum() at crash.c:22
#6 0x080484b7 in main() at crash.c:28
La traza muestra que main llamó a printSum, que a su vez llamó a sprintf, y a partir de ahí se encadenaron funciones internas de la libc hasta el crash. No podemos tocar la implementación de sprintf, pero sí ver qué parámetros le hemos pasado en la línea 22 de crash.c.
Breakpoints, ejecución paso a paso y comandos básicos
La gracia de un depurador es poder parar la ejecución donde te interesa. Para eso están los breakpoints o puntos de ruptura, que detienen el programa justo antes de ejecutar una línea o cuando se entra en una función.
En el ejemplo anterior, queremos ver qué pasa justo antes de llamar a sprintf. Podemos poner un breakpoint en la línea 22 de crash.c directamente desde GDB:
(gdb) break crash.c:22
Breakpoint 1 at 0x804845b: file crash.c, line 22.
Si reejecutas el programa con run, GDB te avisará de que ya estaba en marcha y te preguntará si quieres empezar de cero. Dile que sí y llega al punto de ruptura:
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: ./crash
enter a number:
10
Breakpoint 1, printSum() at crash.c:22
22 sprintf(buf, "sum=%d", sum_to_n(atoi(line)));
Con el programa parado ahí, podemos inspeccionar variables con el comando print (o p):
(gdb) print line
$1 = "10\000\000\000\000\000\000…"
La cadena leída parece razonable. Ahora miramos el puntero buf:
(gdb) print buf
$2 = 0x0
Ahí está el bicho: estamos pasando a sprintf un puntero nulo como destino, es decir, intentando escribir en una dirección inválida. La solución es reservar memoria para buf o usar un array local en lugar de un puntero sin inicializar.
GDB también permite ejecutar línea a línea gracias a los comandos next (n), step (s) y until (u):
- next (n): ejecuta la siguiente línea, pero si hay una llamada a función, la ejecuta de golpe sin entrar en ella.
- step (s): ejecuta la línea y, si hay función llamada, entra en su primera instrucción, ideal para seguir funciones propias.
- until (u): muy útil dentro de un bucle; continúa la ejecución hasta salir de ese bucle y alcanzar la línea indicada o la siguiente fuera del ciclo.
Por ejemplo, si pones un breakpoint en la línea del bucle de sum_to_n y luego usas n o u, puedes ver cómo evoluciona el acumulador sin tener que iterar manualmente todas las vueltas:
(gdb) break crash.c:10
(gdb) run
Breakpoint 1, sum_to_n (num=50) at crash.c:10
10 for (i = 1; i <= num; i++)
(gdb) n
11 sum += i;
(gdb) n
10 for (i = 1; i <= num; i++)
(gdb) until 12
12 return sum;
Cuando quieres que el programa siga corriendo hasta el próximo breakpoint o hasta que acabe, usas continue (o c):
(gdb) continue
Continuing.
Breakpoints condicionales y gestión de puntos de parada
En programas con bucles grandes o muchas iteraciones, no siempre quieres parar “siempre en el mismo sitio”. Ahí entran en juego los breakpoints condicionales, que solo se activan si se cumple una expresión lógica.
Supón que quieres parar dentro de sum_to_n solo cuando el parámetro num sea 50. Primero creas el breakpoint en la línea deseada y luego le añades la condición:
(gdb) break crash.c:10
Breakpoint 1 at 0x8048441: file crash.c, line 10.
(gdb) condition 1 num == 50
Tras lanzar de nuevo el programa, GDB solo se detendrá cuando num valga 50 al llegar a esa línea:
(gdb) run
Starting program: ./crash
enter a number:
50
Breakpoint 1, sum_to_n (num=50) at crash.c:10
10 for (i = 1; i <= num; i++)
Para consultar los puntos de ruptura activos y sus números, puedes usar info breakpoints. Los breakpoints se pueden manipular con varios comandos:
- delete: sin argumentos, borra todos los breakpoints; con número, elimina solo el indicado (
delete 1). - clear: quita el breakpoint asociado a una función o línea (
clear main,clear 42…). - disable / enable: permiten desactivar temporalmente un breakpoint y volverlo a activar más tarde.
Además de breakpoints clásicos también existen los watchpoints, que no paran en una línea concreta sino cuando se modifica o se lee el contenido de una variable. Se crean con watch variable (para cambios) o rwatch variable (para lecturas), y se listan con info watch. Son muy útiles para perseguir escrituras inesperadas sobre una estructura complicada.
Explorar código, variables y memoria
Con el programa detenido, lo que más harás será moverte por el código y mirar valores. GDB ofrece una colección de comandos bastante cómoda para eso, empezando por list, que muestra por pantalla fragmentos del fuente.
El uso más habitual de list es pedir unas cuantas líneas alrededor de un número de línea o de una función:
list 20: enseña unas líneas centradas en la línea 20.list main: empieza a listar desde el principio demain.lista secas: continúa listando a partir de la última línea mostrada.
Para inspeccionar valores, ya hemos visto que print es el comando estrella, pero no está limitado a simples variables. Puedes evaluar expresiones completas, hacer casts, acceder a campos de estructuras o mirar elementos de un array:
(gdb) print intermedio
$1 = 134513691
(gdb) print primero
$2 = 2654196
(gdb) print lista@25
En el ejemplo de un programa con variables sin inicializar, GDB permite comprobar rápidamente que no se ha dado un valor inicial a una variable local y que su contenido es basura. Esto explica por qué cada ejecución imprime un número distinto, algo típico cuando dependes de memoria automática sin inicializar.
Si necesitas ver el contenido de memoria cruda en una dirección, puedes combinar print &variable para obtener una dirección y luego usar el comando x (examine):
(gdb) print &num
$1 = (int *) 0xbffff580
(gdb) x 0xbffff580
0xbffff580: 0x00000064
Además, con ptype puedes pedirle a GDB que te indique el tipo de una variable o expresión, algo práctico cuando trabajas con estructuras complejas o plantillas en C++ y te lías con los tipos:
(gdb) ptype saldo_ptr
Otra función potente es la posibilidad de modificar variables en caliente con el comando set var. Por ejemplo, si quieres que un puntero llamado saldo_ptr pase a apuntar a saldo durante la depuración:
(gdb) set var saldo_ptr = &saldo
A partir de ese momento, la ejecución continúa con ese nuevo valor, lo que permite probar escenarios sin recompilar el programa cada vez.
Control fino de la ejecución y la pila de llamadas
Cuando te metes en programas grandes, no basta con avanzar línea a línea; necesitas saber por qué has llegado hasta ahí y moverte entre distintos contextos de llamada. GDB ofrece varios comandos para gestionar la pila (stack) y navegar por los distintos frames.
El comando where (o backtrace/bt) muestra la pila de llamadas, desde la función actual hasta el punto de entrada. Cada frame incluye la función, parámetros y archivo/linea. Puedes subir y bajar por esos frames con up y down para inspeccionar variables locales de distintos niveles:
(gdb) where
#0 funcion_profunda() at modulo.c:120
#1 calcula() at core.c:45
#2 main() at main.c:30
Para salir rápidamente de una función sin ir paso a paso hasta su final, existe el comando finish, que ejecuta el resto del cuerpo de la función actual y detiene la ejecución en el punto de llamada, devolviéndote al frame anterior.
Si quieres que la depuración comience directamente en main sin tener que pararte antes en código de inicio de la libc, hay un atajo muy útil: start main. Este comando arranca el programa y coloca un breakpoint temporal justo al entrar en la función main.
Para repetir el último comando introducido, basta con pulsar ENTER en una línea en blanco. Esto resulta muy cómodo cuando estás encadenando varios next o step seguidos.
Atajos de teclado y manejo del historial en GDB
Como GDB se usa desde la terminal, aprovecha las capacidades de edición de línea de GNU Readline. Eso significa que puedes usar un buen repertorio de atajos de teclado para moverte, editar comandos y reutilizar órdenes anteriores sin tener que escribirlo todo a mano.
Entre los atajos más útiles están:
- CTRL+P / CTRL+N: navegar hacia atrás o adelante en el historial de comandos.
- CTRL+A / CTRL+E: saltar al inicio o al final de la línea actual.
- CTRL+B / CTRL+F: moverte un carácter a la izquierda o derecha.
- ALT+F / ALT+B: moverte palabra a palabra hacia adelante o hacia atrás.
- CTRL+K: cortar desde la posición actual del cursor hasta el final de la línea, guardándolo en un buffer.
- CTRL+Y: pegar lo último cortado en la posición del cursor.
- CTRL+_ o CTRL+X CTRL+U: deshacer la última edición de la línea (puedes repetirlo varias veces).
- CTRL+L: refrescar la pantalla si la salida se ha quedado hecha un lío.
- CTRL+C: interrumpir la ejecución del programa que estás depurando.
- CTRL+X A: activar o desactivar una vista de código fuente dividida en la terminal.
- CTRL+X 2: abrir una segunda “ventana” (por ejemplo, con ensamblador) y alternar entre vistas.
Una costumbre que se adquiere rápido es tirar mucho del historial con CTRL+P y ENTER cuando repites comandos como run, next o print, y usar CTRL+R (búsqueda inversa, heredada de Readline) para encontrar una orden que ejecutaste hace un rato.
Otros comandos útiles de GDB en el día a día
Aparte de lo ya mencionado, hay un puñado de órdenes que, aunque no uses cada minuto, conviene tener presentes porque resuelven situaciones muy concretas de forma elegante.
- help: muestra ayuda integrada;
helpsolo enseña temas generales, yhelp comandoentra al detalle de cada orden. - info con subcomandos: por ejemplo,
info functionspara listar funciones del programa,info localspara ver variables locales del frame actual, oinfo breakpointspara revisar todos los puntos de ruptura. - run con argumentos: igual que en la consola normal, puedes pasar parámetros y redirecciones:
run 2048 24 4orun >salida.txt. - clear: elimina el breakpoint situado en una función o línea concreta sin necesidad de recordar su número interno.
- continue: reanuda la ejecución hasta el siguiente breakpoint o el final del programa.
- quit: termina la sesión de GDB; si el programa sigue vivo, te pedirá confirmación.
Con esta colección de comandos y atajos puedes afrontar prácticamente cualquier sesión de depuración habitual en C o C++: compilar con símbolos, arrancar GDB, parar donde interesa, examinar el estado del programa, alterar variables si hace falta y seguir adelante hasta encontrar el origen del fallo.
Una vez te acostumbras a usar GDB para seguir la ejecución paso a paso, revisar la pila tras un SIGSEGV, vigilar variables con watchpoints y saltar bucles enteros con until, la depuración deja de ser un proceso a base de adivinar y probar y se convierte en una investigación mucho más precisa y controlada, que además mejora tu comprensión del propio código.
Redactor apasionado del mundo de los bytes y la tecnología en general. Me encanta compartir mis conocimientos a través de la escritura, y eso es lo que haré en este blog, mostrarte todo lo más interesante sobre gadgets, software, hardware, tendencias tecnológicas, y más. Mi objetivo es ayudarte a navegar por el mundo digital de forma sencilla y entretenida.