← Programación en C para Sistemas Operativos Bloque 9 · Errores, archivos y depuración · Tema 39 de 42

Tema 39

Depurar con gdb

Cuando un programa en C da un resultado equivocado o cae con Segmentation fault, el reflejo habitual es llenarlo de printf. Funciona, pero cada prueba obliga a editar, recompilar y volver a ejecutar, y solo se ve lo que se pensó imprimir de antemano. gdb, el depurador de GNU, permite lo contrario: detener el programa donde se quiera, mirar cualquier variable y la memoria, avanzar línea por línea, preguntar quién llamó a quién, y hasta cambiar un valor para comprobar una hipótesis, todo sin tocar el código. Este tema lo enseña con un programa que tiene un error de verdad: un promedio que da 21,33 cuando debería dar 8. Se compila con símbolos de depuración, se detiene en puntos de ruptura (condicionales incluidos), se vigila una variable con watch hasta ver exactamente cuándo se corrompe, se recorre la pila de llamadas, y se analiza una caída tanto en vivo como a posteriori, con un volcado de memoria (core dump). Las salidas son las de gdb 17.1 en Linux.

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:

demo.c
#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;
}
terminal
$ 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:

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:

terminal · sin símbolos (-O0 sin -g) y optimizado (-g -O2 -fno-inline)
$ 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:

terminal · gdb imprime además dos líneas sobre libthread_db al arrancar el programa; aquí se omiten
$ 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:

Las órdenes tienen abreviaturas, y con un poco de práctica casi no se escribe otra cosa:

OrdenAbreviaturaQué hace
run [args]rInicia el programa (con argumentos opcionales).
break lugarbPunto de ruptura en una función, archivo:línea o dirección.
continuecSigue hasta el próximo punto de ruptura.
next / stepn / sAvanza una línea sin entrar / entrando en funciones.
finishEjecuta hasta que termina la función actual y muestra lo que devuelve.
print exprpEvalúa y muestra una expresión de C.
backtracebtLa pila de llamadas.
listlMuestra el código fuente.
info locals / info argsi localsVariables locales / argumentos del marco actual.
info breakpointsi bLista los puntos de ruptura y vigilancias.
delete [n]dBorra un punto de ruptura (o todos).
quitqSale 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:

terminal
(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:

terminal · watch sobre una variable local
(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:

terminal
(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.

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:

terminal
(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):

terminal
(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:

terminal
(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:

terminal
(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:

guion de gdb · se ejecuta con "gdb -q -x guion.gdb ./demo"
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:

caida.c (las partes relevantes)
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;
}
terminal
$ ./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:

terminal
$ 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:

args.c
#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;
}
terminal
(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

11. Errores frecuentes

ErrorQué pasaCorrección
Depurar un ejecutable compilado sin -gSin líneas, sin variables: No symbol table info available.Compilar con -g y volver a ejecutar.
Depurar con -O2 o -O3Variables <optimized out>, líneas que saltan, funciones incorporadas que no existen.-g -O0 (o -Og) para depurar.
Recompilar y olvidar volver a ejecutargdb 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 runEl 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ónLa 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ímbolosgdb 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 arregloSe ve solo la dirección o un valor.print *p@n para ver n elementos.
Fiarse de una variable no inicializadaValores 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 programaSolo 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óngdb 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 normalNo coinciden: gdb desactiva ASLR.Comparar tipos, valores y estructura, no direcciones absolutas.
No encontrar el core tras una caídaEl 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 cuidadoContienen toda la memoria del proceso, incluso secretos.Borrarlos tras el análisis y limitar su acceso.
Pretender que gdb detecte desbordes que no caenEl programa «funciona» con memoria corrupta y gdb no se detiene.AddressSanitizer o Valgrind (tema 29); compilar con avisos (tema 40).

12. Resumen

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

  1. Compila demo.c con -g -O0 y repite el diagnóstico completo: punto de ruptura en promedio, print *v@3, watch total y seguimiento hasta ver el cuarto sumando. Corrige i <= n y comprueba que sale 8,00.
  2. Con un programa propio que tenga varias funciones, coloca un punto de ruptura en la más interna, usa bt y recorre los marcos con up y down mostrando las variables de cada uno.
  3. Escribe un programa que calcule la suma de los elementos de un arreglo dinámico (malloc) y mira con x/8dw la memoria justo después del bloque reservado. ¿Qué hay? ¿Qué pasa con watch si se escribe fuera?
  4. Provoca un Segmentation fault con un puntero nulo, otro con un puntero sin inicializar y otro con una cadena en .rodata modificada (char *s = "hola"; s[0] = 'H';). Compara cómo se ve cada caída en gdb.
  5. Activa los volcados con ulimit -c unlimited, haz caer un programa, y analiza el core sin ejecutarlo de nuevo. Comprueba qué pasa si recompilas el ejecutable entre la caída y el análisis.
  6. Escribe un guion de gdb (-batch -x) que ponga un punto de ruptura con commands en una función, imprima sus argumentos y continúe, y úsalo para trazar todas las llamadas de un programa sin modificarlo.
  7. Abre una sesión con layout src (o gdb -tui) y recorre un programa con next. Prueba layout asm y x/5i $pc: ¿qué instrucciones corresponden a una línea de C?
  8. Depura el mismo programa compilado con -O0, con -O2 y con -Og, y anota qué cambia en info locals, en el orden de las líneas con next y en los nombres de funciones con break.