1. Preparar el programa: -g y -O0
El programa de prueba calcula el promedio de las notas de un alumno y el factorial de 5. Tiene un error plantado, y por ahora no se sabe dónde:
#include <stdio.h>
struct alumno {
char nombre[16];
int notas[3];
int faltas;
};
static double promedio(const int *v, int n)
{
int total = 0;
for (int i = 0; i <= n; i++)
total += v[i];
return (double)total / n;
}
static long factorial(int n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
int main(void)
{
struct alumno a = {"Ana", {7, 9, 8}, 40};
double p = promedio(a.notas, 3);
printf("%s: promedio %.2f\n", a.nombre, p);
printf("5! = %ld\n", factorial(5));
return 0;
}
$ gcc -std=c17 -Wall -Wextra -g -O0 -o demo demo.c
$ ./demo
Ana: promedio 21.33
5! = 120
Las notas son 7, 9 y 8: el promedio debería ser 8,00. El compilador no advierte nada (el error está en la lógica, no en la sintaxis). Para depurar con comodidad, el ejecutable debe llevar información que lo relacione con el código fuente, y dos opciones de gcc controlan eso:
-gagrega los símbolos de depuración: tablas que dicen a qué línea del fuente corresponde cada instrucción, y el nombre, tipo y ubicación de cada variable. Sin ellas gdb solo ve direcciones y ensamblador.-O0desactiva las optimizaciones. Con optimización el compilador reordena, elimina y combina código y variables, y lo que se ve en el depurador deja de parecerse al fuente.
La diferencia se aprecia en tres compilaciones del mismo programa. El ejecutable con -g es algo mayor (17 880 bytes frente a 16 072 sin símbolos). Sin -g, gdb pierde las líneas y las variables; con -O2 las conserva pero muchas desaparecen:
$ gcc -std=c17 -O0 -o demo_sin_g demo.c
$ gdb -q ./demo_sin_g
(gdb) break promedio
Breakpoint 1 at 0x1171
(gdb) run
Breakpoint 1, 0x0000555555555171 in promedio ()
(gdb) bt
#0 0x0000555555555171 in promedio ()
#1 0x000055555555525b in main ()
(gdb) info locals
No symbol table info available.
$ gcc -std=c17 -g -O2 -fno-inline -o demo_o2 demo.c
$ gdb -q ./demo_o2
(gdb) break promedio
Breakpoint 1 at 0x1210: file demo.c, line 15.
(gdb) run
Breakpoint 1, promedio (v=v@entry=0x7fffffffd7d0, n=3) at demo.c:15
15 return (double)total / n;
(gdb) info locals
i = <optimized out>
total = <optimized out>
Sin -g, el punto de ruptura está en una dirección (0x1171), la pila muestra funciones sin líneas ni argumentos, y no hay variables. Con -O2, el punto de ruptura salta a la línea 15 (el bucle se optimizó) y total es <optimized out>: el compilador la mantuvo en un registro, o la eliminó, y gdb no puede mostrarla. (Con -O2 sin -fno-inline, promedio ni siquiera existe como función: se incorporó a main y break promedio responde Function "promedio" not defined.) De ahí la receta: para depurar, compilar con -g -O0 (o -Og, una optimización moderada pensada para depurar). Los ejecutables que se entregan se compilan después sin -g, o con -g pero separando los símbolos.
2. Una primera sesión
Se arranca con gdb ./demo (la opción -q omite el texto de bienvenida). gdb muestra el prompt (gdb) y espera órdenes. El programa aún no corre: primero se fijan los puntos de ruptura (breakpoints) y luego se lo lanza con run. Una sesión corta por main:
$ gdb -q ./demo
(gdb) break main
Breakpoint 1 at 0x120b: file demo.c, line 26.
(gdb) run
Breakpoint 1, main () at demo.c:26
26 {
(gdb) list
21 return 1;
22 return n * factorial(n - 1);
23 }
24
25 int main(void)
26 {
27 struct alumno a = {"Ana", {7, 9, 8}, 40};
28 double p = promedio(a.notas, 3);
29
30 printf("%s: promedio %.2f\n", a.nombre, p);
(gdb) next
27 struct alumno a = {"Ana", {7, 9, 8}, 40};
(gdb) next
28 double p = promedio(a.notas, 3);
(gdb) print a
$1 = {nombre = "Ana", '\000' <repeats 12 times>, notas = {7, 9, 8}, faltas = 40}
(gdb) print a.notas
$2 = {7, 9, 8}
(gdb) info locals
a = {nombre = "Ana", '\000' <repeats 12 times>, notas = {7, 9, 8}, faltas = 40}
p = 0
(gdb) step
promedio (v=0x7fffffffd7e0, n=3) at demo.c:11
11 int total = 0;
(gdb) bt
#0 promedio (v=0x7fffffffd7e0, n=3) at demo.c:11
#1 0x000055555555525b in main () at demo.c:28
Hay varias cosas que ver en esta sesión:
break mainpuso el punto de ruptura en la línea 26, la llave de apertura: enmainhay un arreglo local y gcc agrega código de protección de la pila antes de la primera línea «real», y el punto de ruptura se coloca después de preparar el marco. Dosnextllevan a la línea 28.listmuestra el fuente alrededor de la línea actual. Conlist 20,30olist promediose elige otro tramo.print amuestra la estructura completa; el nombre aparece como"Ana", '\000' <repeats 12 times>: tres letras y doce ceros que completan los 16 bytes. Cada resultado queda guardado en una variable de gdb,$1,$2…, que se puede reutilizar en otras expresiones.info localslista las variables locales del marco actual. Obsérvese quep = 0todavía: la línea 28 aún no se ejecutó.nextavanza una línea sin entrar en las funciones que se llamen en ella;stepsí entra. Aquístepentró enpromedio, ybt(backtrace) muestra que estamos dentro de ella, llamada desdemainen la línea 28.
Las órdenes tienen abreviaturas, y con un poco de práctica casi no se escribe otra cosa:
| Orden | Abreviatura | Qué hace |
|---|---|---|
run [args] | r | Inicia el programa (con argumentos opcionales). |
break lugar | b | Punto de ruptura en una función, archivo:línea o dirección. |
continue | c | Sigue hasta el próximo punto de ruptura. |
next / step | n / s | Avanza una línea sin entrar / entrando en funciones. |
finish | Ejecuta hasta que termina la función actual y muestra lo que devuelve. | |
print expr | p | Evalúa y muestra una expresión de C. |
backtrace | bt | La pila de llamadas. |
list | l | Muestra el código fuente. |
info locals / info args | i locals | Variables locales / argumentos del marco actual. |
info breakpoints | i b | Lista los puntos de ruptura y vigilancias. |
delete [n] | d | Borra un punto de ruptura (o todos). |
quit | q | Sale de gdb. |
Pulsar Intro sin escribir nada repite la última orden, cómodo para avanzar con next varias veces.
3. Cazar el error: argumentos, memoria y watch
Ya estamos dentro de promedio. El camino habitual es mirar los datos de entrada, sospechar, y confirmar. Se empieza con un punto de ruptura en la función (sesión nueva) y se examinan sus argumentos:
(gdb) break promedio
Breakpoint 1 at 0x1178: file demo.c, line 11.
(gdb) run
Breakpoint 1, promedio (v=0x7fffffffd7e0, n=3) at demo.c:11
11 int total = 0;
(gdb) info args
v = 0x7fffffffd7e0
n = 3
(gdb) print *v@3
$1 = {7, 9, 8}
(gdb) print v[3]
$2 = 40
Un puntero a int se muestra como una dirección, y no dice cuántos elementos tiene. El operador @ de gdb lo resuelve: *v@3 significa «los tres elementos que empiezan en *v» y muestra {7, 9, 8}. Hasta aquí todo bien. Pero print v[3] da 40: el arreglo tiene tres notas, y sin embargo v[3] existe (C no lo impide) y vale 40, que es justo el campo faltas que está a continuación en la estructura. Es una pista fuerte: la función lee un elemento de más. Para confirmarlo, en vez de adivinar, se puede vigilar la variable total con watch: gdb detiene el programa en cada instante en que cambia, e indica el valor anterior y el nuevo:
(gdb) watch total
Hardware watchpoint 2: total
(gdb) continue
Hardware watchpoint 2: total
Old value = 16777216
New value = 0
promedio (v=0x7fffffffd7e0, n=3) at demo.c:13
13 for (int i = 0; i <= n; i++)
(gdb) continue
Hardware watchpoint 2: total
Old value = 0
New value = 7
promedio (v=0x7fffffffd7e0, n=3) at demo.c:13
13 for (int i = 0; i <= n; i++)
(gdb) continue
Old value = 7
New value = 16
(gdb) continue
Old value = 16
New value = 24
(gdb) continue
Old value = 24
New value = 64
promedio (v=0x7fffffffd7e0, n=3) at demo.c:13
13 for (int i = 0; i <= n; i++)
(gdb) continue
Watchpoint 2 deleted because the program has left the block in
which its expression is valid.
0x000055555555525b in main () at demo.c:28
28 double p = promedio(a.notas, 3);
(En los cambios tercero a quinto se resume la salida: gdb repite el mismo encabezado y la misma línea cada vez.) La historia queda a la vista. La primera parada, Old value = 16777216, es la basura que había en la pila antes de inicializar total (por eso es importante inicializar las variables). Luego suma 7 (primera nota), 9 (16), 8 (24) y un cuarto sumando de 40 (64): el bucle ejecuta cuatro vueltas, para i de 0 a 3, cuando la lista solo tiene tres elementos. La condición del for es i <= n y debería ser i < n. Una vez que la función retorna, gdb elimina la vigilancia sola, porque la variable ya no existe. 64 / 3 = 21,33: el resultado erróneo del programa.
El «Hardware watchpoint» es la forma eficiente: los procesadores tienen registros de depuración que avisan al acceder a una dirección de memoria, y gdb los usa mientras hay pocos (suelen ser cuatro). Si no puede, usa una vigilancia por software, que ejecuta el programa instrucción por instrucción y es mucho más lenta. Hay tres variantes: watch x (avisa al cambiar), rwatch x (al leer) y awatch x (en ambos casos). watch -l expr vigila la dirección de la expresión y no su ámbito, útil para memoria dinámica o cuando se vigila a través de un puntero.
4. La pila de llamadas
Cada función en ejecución tiene un marco (frame) en la pila (tema 12), y bt los enumera de la función actual hacia atrás. Es la herramienta para responder «¿cómo llegué hasta aquí?». factorial es recursiva y sirve de ejemplo. Un punto de ruptura condicional (break … if condición) detiene el programa solo cuando la condición es cierta, aquí cuando n vale 2:
(gdb) break factorial if n == 2
Breakpoint 1 at 0x11d5: file demo.c, line 20.
(gdb) run
Ana: promedio 21.33
Breakpoint 1, factorial (n=2) at demo.c:20
20 if (n <= 1)
(gdb) bt
#0 factorial (n=2) at demo.c:20
#1 0x00005555555551f5 in factorial (n=3) at demo.c:22
#2 0x00005555555551f5 in factorial (n=4) at demo.c:22
#3 0x00005555555551f5 in factorial (n=5) at demo.c:22
#4 0x0000555555555292 in main () at demo.c:31
(gdb) frame 2
#2 0x00005555555551f5 in factorial (n=4) at demo.c:22
22 return n * factorial(n - 1);
(gdb) info frame
Stack level 2, frame at 0x7fffffffd790:
rip = 0x5555555551f5 in factorial (demo.c:22); saved rip = 0x5555555551f5
called by frame at 0x7fffffffd7c0, caller of frame at 0x7fffffffd760
source language c.
Arglist at 0x7fffffffd780, args: n=4
Locals at 0x7fffffffd780, Previous frame's sp is 0x7fffffffd790
Saved registers:
rbx at 0x7fffffffd778, rbp at 0x7fffffffd780, rip at 0x7fffffffd788
(gdb) finish
0x00005555555551f5 in factorial (n=5) at demo.c:22
22 return n * factorial(n - 1);
Value returned is $1 = 24
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555551d5 in factorial at demo.c:20
stop only if n == 2
breakpoint already hit 1 time
Cada elemento del bt trae el número de marco, la dirección de retorno, la función con sus argumentos y la línea. La imagen es la de la recursión del tema 12: factorial(5) llamó a factorial(4), y así hasta factorial(2); main está al fondo.
frame N(of N) cambia el marco «seleccionado»;upydownsuben y bajan uno. Con un marco seleccionado,print,info localsoinfo argsse refieren a sus variables: en el marco 2 lanvale 4.info framemuestra el detalle del marco: dónde guarda el registro de retorno (rip), la base del marco (rbp), etc. Se corresponden con lo estudiado en el tema 12.finishejecuta hasta que el marco seleccionado termina, y muestra el valor que devuelve: como el marco seleccionado era el 2 (n=4), el resultado esfactorial(4) = 24, que es lo que informaValue returned is $1 = 24.info breakpointslista todos los puntos de ruptura, con su condición y cuántas veces se activaron.- Un punto de ruptura temporal,
tbreak, se borra solo tras la primera parada.break archivo.c:25fija el punto en una línea concreta, ydelete 1(odisable 1) lo quita (o lo apaga).
5. Inspeccionar datos: print, ptype y x
print acepta casi cualquier expresión de C: aritmética, punteros, miembros de estructuras, elementos de arreglos, casts, llamadas a funciones. Se puede cambiar el formato con una barra: /x hexadecimal, /d decimal, /t binario, /c carácter, /o octal. Y para ver los tipos, ptype. Con el programa detenido en la línea 30:
(gdb) break 30
Breakpoint 1 at 0x1264: file demo.c, line 30.
(gdb) run
Breakpoint 1, main () at demo.c:30
30 printf("%s: promedio %.2f\n", a.nombre, p);
(gdb) print sizeof(a)
$1 = 32
(gdb) ptype struct alumno
type = struct alumno {
char nombre[16];
int notas[3];
int faltas;
}
(gdb) print/x a.notas
$2 = {0x7, 0x9, 0x8}
(gdb) print/c a.nombre[0]
$3 = 65 'A'
(gdb) print/t a.faltas
$4 = 101000
(gdb) print &a.notas[1]
$5 = (int *) 0x7fffffffd7e4
(gdb) print *&a.notas[1]
$6 = 9
(gdb) set print pretty on
(gdb) print a
$7 = {
nombre = "Ana", '\000' <repeats 12 times>,
notas = {7, 9, 8},
faltas = 40
}
(gdb) info line 28
Line 28 of "demo.c" starts at address 0x555555555246 <main+71> and ends at 0x555555555264 <main+101>.
Se confirma el tamaño del tema 25 (sizeof(a) es 32: 16 + 12 + 4, sin relleno), el tipo de la estructura, y las conversiones de formato (40 es 101000 en binario; 'A' es 65). set print pretty on imprime las estructuras en varias líneas, útil cuando son grandes.
Para ver la memoria cruda, la orden x (examine) tiene la forma x/NFU dirección: N es cuántas unidades, F el formato (x hexa, d decimal, s cadena, i instrucción, c carácter) y U el tamaño (b byte, h 2 bytes, w 4 bytes, g 8 bytes):
(gdb) x/s a.nombre
0x7fffffffd7d0: "Ana"
(gdb) x/4dw a.notas
0x7fffffffd7e0: 7 9 8 40
(gdb) x/16xb &a
0x7fffffffd7d0: 0x41 0x6e 0x61 0x00 0x00 0x00 0x00 0x00
0x7fffffffd7d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
La segunda orden, x/4dw a.notas («cuatro enteros de 4 bytes, en decimal»), es la que cierra el caso del error: en memoria, a las tres notas les sigue un cuarto valor, 40, y es el que promedio leyó de más. La tercera muestra los 16 primeros bytes de la estructura: 0x41 0x6e 0x61 son «Ana», seguidos de ceros. (Las direcciones tipo 0x7fffffffd7e0 de la pila son las de este equipo: gdb desactiva por defecto la aleatorización de direcciones, y el valor exacto depende del tamaño del entorno, de modo que en otra máquina serán otras.)
6. Probar hipótesis: set var y call
gdb no solo mira: también puede cambiar variables y llamar a funciones del programa detenido. Eso permite comprobar una hipótesis sin editar ni recompilar. Detenidos en la línea 28, antes de calcular el promedio:
(gdb) break 28
Breakpoint 1 at 0x1246: file demo.c, line 28.
(gdb) run
Breakpoint 1, main () at demo.c:28
28 double p = promedio(a.notas, 3);
(gdb) print promedio(a.notas, 3)
$1 = 21.333333333333332
(gdb) print factorial(4)
$2 = 24
(gdb) print a.faltas
$3 = 40
(gdb) set var a.faltas = 0
(gdb) print promedio(a.notas, 3)
$4 = 8
(gdb) continue
Ana: promedio 8.00
5! = 120
Primero se llama a promedio y a factorial directamente desde el depurador (print función(args), o call, que es igual pero no muestra resultado si la función devuelve void). Después se pone a.faltas a cero con set var: el promedio da 8, lo que prueba que el valor sobrante se estaba colando, y al continuar el programa imprime 8.00. No se corrigió el código —esa corrección se hace en el fuente (i < n)—, sino que se confirmó el diagnóstico antes de tocar nada. Otras formas de lo mismo: print x = 5 asigna también, y set var se usa cuando el nombre de la variable coincidiría con una orden de gdb.
7. Menos teclas: display, until y commands
display expr hace que gdb muestre la expresión automáticamente cada vez que el programa se detiene. Con un punto de ruptura dentro del bucle (línea 14) y dos display, cada continue enseña cómo evolucionan i y total:
(gdb) break 14
Breakpoint 1 at 0x1188: file demo.c, line 14.
(gdb) run
Breakpoint 1, promedio (v=0x7fffffffd7e0, n=3) at demo.c:14
14 total += v[i];
(gdb) display i
(gdb) display total
(gdb) continue
Breakpoint 1, promedio (v=0x7fffffffd7e0, n=3) at demo.c:14
14 total += v[i];
1: i = 1
2: total = 7
(gdb) continue
Breakpoint 1, promedio (v=0x7fffffffd7e0, n=3) at demo.c:14
14 total += v[i];
1: i = 2
2: total = 16
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1: y i
2: y total
(gdb) delete 1
(gdb) until 15
promedio (v=0x7fffffffd7e0, n=3) at demo.c:15
15 return (double)total / n;
2: total = 64
(Esta captura se hizo con un guion de gdb, y en ese modo los display no mostraron su valor al crearse; en una sesión interactiva gdb los muestra de inmediato y, desde entonces, en cada parada.) delete 1 borró el punto de ruptura, y until 15 ejecutó hasta la línea 15 sin detenerse en las vueltas restantes del bucle: cómodo para saltar un bucle largo o una llamada. Sin argumento, until es como next pero no retrocede en un bucle. undisplay N quita un display.
Para observar sin detenerse, un punto de ruptura puede llevar su propia lista de órdenes con commands. Con silent no imprime el aviso de parada, y terminando con continue el programa sigue solo. El resultado es una traza, como un printf insertado sin recompilar:
break factorial
commands
silent
printf "factorial(%d)\n", n
continue
end
run
Ana: promedio 21.33
factorial(5)
factorial(4)
factorial(3)
factorial(2)
factorial(1)
5! = 120
[Inferior 1 (process 473) exited normally]
Los guiones de gdb (la opción -x archivo, o -batch para que gdb termine al acabar) sirven para reproducir una sesión, repetirla tras corregir el código, o ejecutar un diagnóstico dentro de un script. gdb también lee, al iniciar, un archivo ~/.gdbinit y otro .gdbinit en el directorio actual, con las órdenes que se quiera ejecutar siempre (por ejemplo, set print pretty on); el segundo requiere autorización explícita por seguridad.
8. Cuando el programa se cae
Para la depuración más típica, un programa que termina con Segmentation fault, gdb es más rápido que cualquier otra herramienta, porque se detiene en el instante exacto del fallo, con la pila y todas las variables intactas. El programa caida.c construye una lista de tres nodos y recorre mostrando, para cada uno, el valor del siguiente:
struct nodo {
int valor;
struct nodo *sig;
};
static int valor_siguiente(const struct nodo *n)
{
return n->sig->valor;
}
static void mostrar(const struct nodo *n)
{
for (; n != NULL; n = n->sig)
printf("%d -> siguiente %d\n", n->valor, valor_siguiente(n));
}
int main(void)
{
struct nodo *lista = nuevo(1, nuevo(2, nuevo(3, NULL)));
mostrar(lista);
return 0;
}
$ ./caida; echo "rc=$?"
1 -> siguiente 2
2 -> siguiente 3
Segmentation fault
rc=139
$ gdb -q ./caida
(gdb) run
1 -> siguiente 2
2 -> siguiente 3
Program received signal SIGSEGV, Segmentation fault.
valor_siguiente (n=0x555555559010) at caida.c:22
22 return n->sig->valor;
(gdb) bt
#0 valor_siguiente (n=0x555555559010) at caida.c:22
#1 0x000055555555520c in mostrar (n=0x555555559010) at caida.c:28
#2 0x0000555555555286 in main () at caida.c:35
(gdb) frame 1
#1 0x000055555555520c in mostrar (n=0x555555559010) at caida.c:28
28 printf("%d -> siguiente %d\n", n->valor, valor_siguiente(n));
(gdb) print *n
$1 = {valor = 3, sig = 0x0}
(gdb) print n->sig
$2 = (struct nodo *) 0x0
(gdb) frame 2
#2 0x0000555555555286 in main () at caida.c:35
35 mostrar(lista);
(gdb) print *lista
$3 = {valor = 1, sig = 0x555555559030}
(gdb) print *lista->sig
$4 = {valor = 2, sig = 0x555555559010}
(gdb) print lista->sig->sig->sig
$5 = (struct nodo *) 0x0
La salida normal del programa ya sugería el problema (el valor «siguiente» del tercer nodo no llega a mostrarse), pero gdb lo explica. Al recibir la señal SIGSEGV, el programa se detiene en la línea 22, n->sig->valor; bt muestra los tres marcos: valor_siguiente ← mostrar ← main. En el marco 1, *n es {valor = 3, sig = 0x0}: el nodo tiene sig nulo, y por tanto n->sig->valor desreferencia un puntero nulo. Es el último nodo de la lista. En el marco 2 se confirma desde main que la lista tiene tres nodos cuyo último apunta a NULL (lista->sig->sig->sig es 0x0). El error no estaba en la función donde cayó, sino en quien la llamó sin comprobar que existía el siguiente: la corrección va en valor_siguiente o en mostrar (comprobar n->sig != NULL). Es el uso más habitual del depurador: la línea del fallo da el síntoma, y la pila y los datos del marco anterior dan la causa.
9. Después de la caída: volcados de memoria
No siempre se puede ejecutar el programa bajo gdb: la caída ocurrió en otra máquina, o una vez en mil ejecuciones. Para esos casos el sistema puede guardar, al morir el proceso, un volcado de memoria (core dump): un archivo con el estado completo de la memoria y los registros en ese instante, que gdb abre como si fuera el proceso detenido. Por omisión Linux no los genera porque el límite de tamaño es cero (ulimit -c lo muestra), y se activa en el shell con ulimit -c unlimited:
$ ulimit -c
0
$ ulimit -c unlimited
$ cat /proc/sys/kernel/core_pattern
core
$ ./caida > /dev/null; ls core*
Segmentation fault (core dumped)
core.837
$ gdb -q ./caida core.837
Core was generated by `./caida'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 valor_siguiente (n=0x56f05bcf5010) at caida.c:22
22 return n->sig->valor;
(gdb) bt
#0 valor_siguiente (n=0x56f05bcf5010) at caida.c:22
#1 0x000056f03f4c320c in mostrar (n=0x56f05bcf5010) at caida.c:28
#2 0x000056f03f4c3286 in main () at caida.c:35
(gdb) frame 1
#1 0x000056f03f4c320c in mostrar (n=0x56f05bcf5010) at caida.c:28
28 printf("%d -> siguiente %d\n", n->valor, valor_siguiente(n));
(gdb) print n->valor
$1 = 3
(gdb) print n->sig
$2 = (struct nodo *) 0x0
(gdb) info registers rip
rip 0x56f03f4c320c 0x56f03f4c320c <mostrar+30>
La «autopsia» da lo mismo que la sesión en vivo: el mismo punto de fallo, la misma pila y los mismos valores, sin volver a ejecutar nada. Dos observaciones. La primera, que se puede inspeccionar el estado, pero no avanzar: no hay proceso que continuar (next o continue dan error). La segunda, que las direcciones son otras (0x56f05bcf5010 en vez de 0x555555559010): bajo gdb en vivo se desactiva la aleatorización del espacio de direcciones (ASLR), pero la ejecución normal que produjo el volcado la tenía activada. Los valores y la estructura son los que importan.
El nombre core.837 incluye el identificador del proceso; en otros sistemas el nombre es solo core, y el patrón lo fija /proc/sys/kernel/core_pattern. Otras distribuciones envían los volcados a un servicio que los guarda aparte —systemd-coredump (se consulta con coredumpctl) o apport en Ubuntu de escritorio— y core_pattern lo indica; en tal caso hay que mirar allí. Los volcados pueden ocupar mucho espacio y contienen la memoria del proceso, con contraseñas o datos privados si los tenía: se tratan con cuidado y no se desactivan por descuido. Para que un volcado sea útil, el ejecutable debe haberse compilado con -g y ser exactamente el mismo que murió.
10. Argumentos, entrada y otras funciones
Cuando el programa recibe argumentos, hay tres formas de pasárselos. Con este programa, que muestra argv y devuelve 1 si hay más de dos argumentos:
#include <stdio.h>
int main(int argc, char **argv)
{
for (int i = 0; i < argc; i++)
printf("argv[%d] = %s\n", i, argv[i]);
return argc > 2;
}
(gdb) break main
Breakpoint 1 at 0x115c: file args.c, line 5.
(gdb) run uno "dos tres"
Breakpoint 1, main (argc=3, argv=0x7fffffffd918) at args.c:5
5 for (int i = 0; i < argc; i++)
(gdb) print argc
$1 = 3
(gdb) print argv[2]
$2 = 0x7fffffffdbad "dos tres"
(gdb) print *argv@argc
$3 = {0x7fffffffdb9b "/home/ana/c/args", 0x7fffffffdba9 "uno", 0x7fffffffdbad "dos tres"}
(gdb) show args
Argument list to give program being debugged when it is started is "uno "dos tres"".
(gdb) continue
argv[0] = /home/ana/c/args
argv[1] = uno
argv[2] = dos tres
[Inferior 1 (process 591) exited with code 01]
$ gdb -q --args ./args a b c
(dentro de gdb: break main; run; print argc → $1 = 4; print argv[1] → "a")
Los argumentos se pasan (1) con run arg1 arg2, (2) con set args arg1 arg2 antes de run, o (3) desde el shell con gdb --args ./prog arg1 arg2; show args muestra los actuales. La redirección vale igual: run < entrada.txt entrega ese archivo como entrada estándar. print *argv@argc es la forma de ver el arreglo completo de punteros. El final del programa se informa: exited with code 01 indica que main devolvió 1 (el código de salida del tema 37, en octal), y exited normally, que fue 0.
Otras posibilidades
- Interfaz de texto (TUI).
gdb -tui ./demo, olayout srcdentro de gdb, divide la terminal y muestra el fuente con la línea actual resaltada. Conlayout asmse ve el ensamblador. - Adjuntarse a un proceso en marcha.
gdb -p PID(oattach PID) depura un proceso que ya corre; en muchos sistemas el kernel lo restringe (ptrace_scope) y exige permisos adicionales. - Ensamblador y registros.
x/3i $pcdesensambla las próximas instrucciones,info registersmuestra los registros ydisassemblela función entera. Útil cuando no hay código fuente. - Señales. gdb se detiene en cada señal que recibe el programa;
handle SIGUSR1 nostop noprint passcambia el comportamiento. Las señales se estudian en el curso Procesos e Hilos, donde también se verá cómo depurar programas con varios hilos (info threads). - Fugas y desbordes de memoria. gdb muestra el estado del programa, pero no detecta por sí mismo errores de memoria que no provocan una caída. Para eso están Valgrind y los sanitizadores (tema 29), y el análisis estático que se verá en el tema siguiente.
11. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
Depurar un ejecutable compilado sin -g | Sin líneas, sin variables: No symbol table info available. | Compilar con -g y volver a ejecutar. |
Depurar con -O2 o -O3 | Variables <optimized out>, líneas que saltan, funciones incorporadas que no existen. | -g -O0 (o -Og) para depurar. |
| Recompilar y olvidar volver a ejecutar | gdb sigue con el ejecutable viejo en memoria y las líneas no coinciden con el fuente nuevo. | Salir y reiniciar (run recarga el archivo si cambió), y recompilar siempre antes de depurar. |
Poner el punto de ruptura y no ejecutar run | El programa no ha empezado: The program is not being run, No frame selected. | run (o start, que corre hasta main). |
Usar next esperando entrar en una función | La función se ejecuta entera sin detenerse. | step para entrar, o un punto de ruptura dentro de ella. |
Usar step sobre una función de la biblioteca (printf) sin símbolos | gdb salta la llamada: no hay información de depuración de libc. | Normal; finish si ya se entró en una función sin símbolos. |
Interpretar print p de un puntero a int como un arreglo | Se ve solo la dirección o un valor. | print *p@n para ver n elementos. |
| Fiarse de una variable no inicializada | Valores basura (como el 16777216 inicial de total) que cambian de ejecución en ejecución. | Inicializar siempre; watch muestra el valor viejo. |
Suponer que set var arregla el programa | Solo cambia la memoria de esta ejecución. | Sirve para confirmar el diagnóstico; la corrección va en el fuente. |
Un watch sobre una local y salir de su función | gdb lo borra solo («left the block in which its expression is valid»). | Normal; watch -l para vigilar la dirección más allá del ámbito. |
| Comparar direcciones de una sesión de gdb con las de una ejecución normal | No coinciden: gdb desactiva ASLR. | Comparar tipos, valores y estructura, no direcciones absolutas. |
No encontrar el core tras una caída | El límite era 0 o el sistema lo envía a otro servicio. | ulimit -c unlimited; revisar core_pattern y coredumpctl. |
Abrir un core con un ejecutable distinto del que murió | Líneas y símbolos que no coinciden; datos sin sentido. | Conservar el binario exacto junto con el volcado. |
| Dejar volcados de memoria sin cuidado | Contienen toda la memoria del proceso, incluso secretos. | Borrarlos tras el análisis y limitar su acceso. |
| Pretender que gdb detecte desbordes que no caen | El programa «funciona» con memoria corrupta y gdb no se detiene. | AddressSanitizer o Valgrind (tema 29); compilar con avisos (tema 40). |
12. Resumen
- Para depurar, compilar con
-g -O0:-gagrega símbolos (líneas, variables, tipos) y-O0evita que las optimizaciones desfiguren el código. Sin-gsolo hay direcciones; con-O2, variables<optimized out>. - Una sesión típica:
gdb ./prog,break(en una función o línea),run, y avanzar connext(sin entrar) ystep(entrando);continuesigue hasta el siguiente punto,finishhasta salir de la función yuntilhasta una línea. - Se inspecciona con
print(con formatos/x /d /t /c),info locals,info args,ptypeyx/NFUpara la memoria cruda.*ptr@nmuestranelementos desde un puntero ydisplayrepite una expresión en cada parada. watchdetiene el programa cuando una variable cambia y muestra el valor anterior y el nuevo, con lo que se ve el instante exacto en que algo se corrompe. Los puntos de ruptura admiten condiciones (break f if n == 2), son temporales contbreaky pueden llevar órdenes propias concommands.btmuestra la pila de llamadas;frame N,upydowncambian el marco seleccionado, y entoncesprinteinfo localsse refieren a sus variables.set varyprint función(args)permiten cambiar datos y llamar funciones en vivo, para comprobar una hipótesis antes de corregir el fuente.- Ante un
SIGSEGV, gdb se detiene en el instante del fallo: la línea es el síntoma, y la pila (bt) y los datos del marco que llamó (frame 1,print *n) dan la causa. - Con
ulimit -c unlimitedel sistema guarda un volcado de memoria al morir el proceso, ygdb ./prog core.PIDlo examina después (sin poder continuar la ejecución). Los argumentos se pasan conrun args,set argsogdb --args.
13. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué hacen las opciones -g y -O0 de gcc y por qué se usan para depurar?
-g incluye en el ejecutable los símbolos de depuración (qué instrucción corresponde a qué línea, y el nombre, tipo y lugar de cada variable); -O0 desactiva las optimizaciones, para que el código que se ejecuta se corresponda con el fuente. Sin -g, gdb ve solo direcciones; con optimización, aparecen variables <optimized out> y líneas que saltan.
2. ¿Cuál es la diferencia entre next y step?
Ambos ejecutan una línea del fuente. Si la línea llama a una función, next la ejecuta completa y se detiene en la línea siguiente; step entra en la función y se detiene en su primera línea (si tiene símbolos de depuración).
3. ¿Qué hacen continue, finish y until 15?
continue reanuda hasta el próximo punto de ruptura (o el fin del programa). finish ejecuta hasta que termina la función del marco seleccionado y muestra el valor que devuelve. until 15 ejecuta hasta llegar a la línea 15, sin parar en lo intermedio (por ejemplo, las vueltas restantes de un bucle).
4. ¿Cómo se ven los tres elementos a los que apunta const int *v en gdb?
Con print *v@3: el operador @ convierte «un elemento en esta dirección» en «tres elementos consecutivos desde esta dirección». También sirve x/3dw v para ver la memoria cruda.
5. ¿Para qué sirve watch total y qué muestra cuando se activa?
Detiene el programa cada vez que total cambia de valor, y muestra el valor anterior (Old value), el nuevo (New value) y la línea donde ocurrió. Permite ver el instante exacto de una corrupción o de un cálculo erróneo, sin avanzar a mano. Si la variable es local, se borra sola al salir de la función.
6. ¿Cómo se detiene el programa solo cuando n vale 2 dentro de una función recursiva?
Con un punto de ruptura condicional: break factorial if n == 2. gdb evalúa la condición en cada llamada y solo se detiene cuando es cierta; info breakpoints muestra la condición y el número de veces que se activó.
7. ¿Qué muestra bt y cómo se consulta una variable de otro marco?
La pila de llamadas, del marco actual (#0) hacia atrás hasta main, con la función, sus argumentos y la línea de cada una. Con frame N (o up/down) se selecciona el marco y entonces print e info locals hablan de las variables de ese marco.
8. ¿Qué dice x/4dw a.notas?
Examina la memoria desde la dirección de a.notas: 4 unidades (4), en decimal (d), de 4 bytes cada una (w, palabra). En el ejemplo mostró 7 9 8 40: las tres notas y el campo siguiente de la estructura.
9. ¿Qué diferencia hay entre corregir el código y usar set var en gdb?
set var cambia la memoria del proceso en ejecución, solo para esa ejecución; sirve para probar una hipótesis (si pongo a.faltas = 0, ¿el resultado es correcto?). El error se corrige en el fuente y se recompila.
10. Un programa termina con Segmentation fault. ¿Cómo lo investigas con gdb?
Se ejecuta bajo gdb (run); se detiene en la señal SIGSEGV, en la línea que falló. Con bt se ve cómo se llegó, y con frame N y print se examinan los valores de los punteros en cada marco (un puntero nulo o inválido es lo habitual). La causa suele estar en un marco anterior al de la línea del fallo.
11. ¿Qué es un volcado de memoria y cómo se usa?
Un archivo (core o core.PID) con el estado de la memoria y los registros de un proceso al morir. Se habilita con ulimit -c unlimited (por omisión el límite es 0) y se abre con gdb ./programa core.PID: permite ver la línea del fallo, la pila y los datos, sin volver a ejecutar, pero no continuar. Requiere el mismo ejecutable (compilado con -g) que murió.
12. ¿Por qué las direcciones del volcado no coinciden con las de la sesión en vivo?
Porque gdb, por defecto, desactiva la aleatorización del espacio de direcciones (ASLR) en las ejecuciones que lanza, mientras que la ejecución normal que produjo el volcado la tenía activa. Las direcciones cambian, pero los valores y la estructura son los mismos.
13. ¿De qué formas se pasan argumentos a un programa bajo gdb?
Con run arg1 arg2; con set args arg1 arg2 antes de run; o al iniciar gdb con gdb --args ./programa arg1 arg2. show args muestra los actuales, y run < archivo redirige la entrada.
14. ¿Qué es <optimized out> y cómo se evita?
Indica que el compilador eliminó la variable o la mantuvo solo en un registro y gdb no puede recuperar su valor. Aparece al depurar un programa compilado con optimización (-O1 o más). Se evita compilando con -g -O0 (o -Og) mientras se depura.
Para practicar
- Compila
demo.ccon-g -O0y repite el diagnóstico completo: punto de ruptura enpromedio,print *v@3,watch totaly seguimiento hasta ver el cuarto sumando. Corrigei <= ny comprueba que sale 8,00. - Con un programa propio que tenga varias funciones, coloca un punto de ruptura en la más interna, usa
bty recorre los marcos conupydownmostrando las variables de cada uno. - Escribe un programa que calcule la suma de los elementos de un arreglo dinámico (
malloc) y mira conx/8dwla memoria justo después del bloque reservado. ¿Qué hay? ¿Qué pasa conwatchsi se escribe fuera? - Provoca un
Segmentation faultcon un puntero nulo, otro con un puntero sin inicializar y otro con una cadena en.rodatamodificada (char *s = "hola"; s[0] = 'H';). Compara cómo se ve cada caída en gdb. - Activa los volcados con
ulimit -c unlimited, haz caer un programa, y analiza elcoresin ejecutarlo de nuevo. Comprueba qué pasa si recompilas el ejecutable entre la caída y el análisis. - Escribe un guion de gdb (
-batch -x) que ponga un punto de ruptura concommandsen una función, imprima sus argumentos y continúe, y úsalo para trazar todas las llamadas de un programa sin modificarlo. - Abre una sesión con
layout src(ogdb -tui) y recorre un programa connext. Pruebalayout asmyx/5i $pc: ¿qué instrucciones corresponden a una línea de C? - Depura el mismo programa compilado con
-O0, con-O2y con-Og, y anota qué cambia eninfo locals, en el orden de las líneas connexty en los nombres de funciones conbreak.