147 KiB
Capítulo 20 — GDB: el microscopio de tu programa
Capítulo avanzado, pero escrito para que se entienda de verdad. Se apoya en compilación y
main(01), variables y tipos (02), operadores (03), condicionales y bucles (04), funciones (05), arreglos y cadenas (06), punteros (07),struct(08), memoria dinámica (09) y archivos (10).Este capítulo no te enseña a escribir C: te enseña a mirar un programa de C mientras corre. Vas a aprender a frenar el programa en la línea exacta donde algo se rompe, a ver el valor de una variable en el instante en que se corrompe, a recorrer la pila de llamadas después de un
Segmentation Faulty a leer la memoria cruda byte a byte. Y vas a entender por qué, la mayoría de las veces, el bug no estaba donde estabas mirando.La herramienta del capítulo es GDB (GNU Debugger). No es la única — hay
LLDB, Visual Studio Debugger,rr,lldb— pero es la que está en el 90 % de las máquinas Linux y en casi todos los cursos de C. Los conceptos que vas a ver acá (símbolos, direcciones, frames, registros) son conceptos de depuración, no de GDB: los necesitás igual con cualquier depurador.
Cómo leer este capítulo
Este es el capítulo donde "mi programa a veces se rompe" se convierte en "sé exactamente por qué se rompe". No es teoría: cada idea viene con comandos para ejecutar y salidas para leer, y cada bloque cierra con práctica para que la uses vos.
Está dividido en ocho partes. Después de cada concepto importante hay uno o varios ejercicios cortos. No los saltees: son la mitad del capítulo.
| Parte | De qué habla | La idea en una frase |
|---|---|---|
| 1 | El terreno | Un binario sin símbolos es un libro en clave: DWARF es la llave, y -g3 -O0 te la da. |
| 2 | La TUI | La pantalla partida: código, ensamblador, registros y comandos, todo a la vista. |
| 3 | Control e inspección | Puntos de interrupción, control del flujo y mirar variables en vivo. |
| 4 | Punteros y control avanzado | Direcciones, desreferenciación, watchpoints y la pila de llamadas. |
| 5 | Errores de memoria | Core dumps, el comando x y los sanitizers: la caja negra del Segfault. |
| 6 | Bajo nivel | Ensamblador, registros, stepi y parcheo en caliente. |
| 7 | Automatización y procesos | .gdbinit, hilos, fork, attach y depuración remota. |
| 8 | Taller integrador | Un caso real con fuga de memoria, Segfault y bucle infinito. |
Los ejercicios se numeran en orden: 20.1, 20.2, 20.3, y así. Cada uno trae su escenario de
prueba (qué hacés y qué tiene que pasar) para que lo ejecutes y compruebes.
Regla de oro del libro: el capítulo explica y muestra cómo funciona cada cosa (con comandos y salidas reales), pero los ejercicios no traen la solución. El enunciado te da el problema y el programa con la falla; el diagnóstico lo escribís vos.
Antes de empezar, dos advertencias honestas.
- GDB es una herramienta de Unix. En Linux funciona tal cual. En macOS funciona, pero el depurador del sistema es
LLDBy hay que firmar el binario degdbpara que el sistema lo deje ejecutar. En Windows,gdbexiste vía MSYS2 / MinGW-w64 y funciona bien para programas C, pero sinulimit -c(o sea: sin core dumps automáticos) y con otras diferencias que vamos a señalar cuando aparezcan.- Depurar es leer evidencia, no adivinar. Un depurador no te dice dónde está el bug: te dice dónde está el programa y qué cree que valen sus variables. El diagnóstico sigue siendo tuyo. Por eso la última parte del capítulo es un taller con método, no una lista de comandos.
PARTE 1 — El terreno: qué hay adentro de tu programa
Antes de tocar un solo comando de GDB, entendamos qué es un programa compilado y por qué
./prog y ./prog -g no son la misma cosa. Sin esto, GDB va a parecer magia y los errores que
vas a ver ("no symbol table info available") van a parecer brujería.
1.1 Depurar es volver al lugar del accidente
Empecemos por el problema real. Tenés un programa de 2.000 líneas. Corre. A veces imprime una
basura. A veces se cuelga. A veces revienta con Segmentation fault. ¿Dónde está el error?
Si tu único instrumento es leer el código, tenés una búsqueda en el vacío: 2.000 líneas, y el error puede estar en cualquiera. Leés todo dos veces, no encontrás nada, y empezás a sospechar del demonio.
Un depurador te da tres superpoderes que cambian el problema por completo:
| Poder | En palabras | En GDB |
|---|---|---|
| Frenar en el momento exacto | Detener el programa justo cuando algo pasa, no "en algún lugar cerca" | Puntos de interrupción (break) |
| Ver el estado real | Mirar el valor de verdad de las variables en ese instante, no el que imaginás | print, display, x |
| Avanzar de a uno | Dar un solo paso, ver el efecto, y decidir el siguiente | next, step |
Pensalo como una cámara de seguridad a 1/1000 de velocidad. Un auto choca contra un poste. No sirve para leer el código del auto: sirve para ver el momento exacto del impacto, con la cámara apuntando al lugar. GDB es esa cámara: no te dice por qué chodaste, te muestra el cuadro exacto donde pasó, con los datos del motor y de la velocidad.
Y un detalle que importa mucho: el depurador no te da la respuesta, te da la evidencia. Un
print te muestra lo que la variable vale ahora — no lo que debería valer. Toda la
habilidad está en mirar esa evidencia y razonar: "esto vale 0, pero acá debería valer 7… ¿por
qué?".
La trampa más común del principiante. Muchos creen que "GDB encuentra el bug por mí". No es así: GDB te dice dónde estás. El razonamiento sigue siendo 100 % tuyo. Conviene tenerlo claro desde el primer minuto.
1.2 El binario ELF: el envase
Cuando compilás prog.c, el compilador no te da "un programa": te da un archivo ELF (Executable
and Linkable Format), que es un envase con secciones adentro. Cada sección tiene un propósito
distinto, y varias de ellas son exactamente lo que GDB necesita.
Pensá en un paquete de repuestos que llega con un inventario adentro. El programa es la máquina; el inventario es lo que le dice a GDB qué pieza es cada cosa.
./prog (ELF)
┌───────────────────────────────────────────────────────────┐
│ .text → el CÓDIGO (las instrucciones de máquina) │
│ .rodata → datos de solo lectura (textos literales) │
│ .data → variables globales inicializadas │
│ .bss → variables globales en cero (no ocupa disco)│
│ .symtab → TABLA DE SÍMBOLOS: nombres de funciones │
│ y variables globales │
│ .strtab → los textos de esos nombres │
│ .debug_info ┐ │
│ .debug_line ├→ INFORMACIÓN DE DEPURACIÓN (DWARF) │
│ .debug_str │ solo si compilaste con -g │
│ .debug_abbrev┘ │
└───────────────────────────────────────────────────────────┘
Lo podés ver con readelf, que viene con binutils:
$ readelf -S ./demo
There are 31 section headers, starting at offset 0x...:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 1] .text PROGBITS 0000000000001060 001060 00001f 00 AX 0 0 16
[ 6] .rodata PROGBITS 0000000000002000 002000 000010 00 A 0 0 8
[10] .data PROGBITS 0000000000004040 004040 000010 00 WA 0 0 8
[14] .bss NOBITS 0000000000004050 004050 000008 00 WA 0 0 8
[16] .symtab SYMTAB 0000000000000000 0023a8 000480 18 25 18 8
[17] .strtab STRTAB 0000000000000000 002828 0002c4 00 0 0 1
[24] .debug_info PROGBITS 0000000000000000 003000 0003a1 00 0 0 1
[25] .debug_abbrev PROGBITS 0000000000000000 0033a8 0000cc 00 0 0 1
[26] .debug_line PROGBITS 0000000000000000 003480 0002f3 00 0 0 1
[27] .debug_str PROGBITS 0000000000000000 003780 00016a 01 MS 0 0 1
Fijate en la diferencia entre
.symtaby.debug_info. La tabla de símbolos te da los nombres de las funciones y de las variables globales. La información de depuración te da mucho más: en qué línea está cada instrucción, qué tipo tiene cada variable, en qué dirección de memoria vive cada local, cómo se desenrolla la pila. Con solo.symtabpodés poner un breakpoint enmain; sin.debug_infono podés poner un breakpoint en la línea 42, ni mirar una variable local, ni avanzar paso a paso.
1.3 DWARF: el manual de instrucciones del edificio
DWARF es el estándar de información de depuración que usan GCC, Clang y casi todos los depuradores de Unix. La sigla significa Debugging With Attributed Record Formats.
La idea es simple y vale la pena internalizarla:
La CPU no sabe qué es una variable. Para el procesador,
totales la dirección0x7ffd.... DWARF es el libro de anotaciones que dice: "en la dirección0x1129hay código de la línea 42 del archivomain.c; en ese punto, la variabletotalestá en el offset-0x18del marco de la pila y es de tipoint".
Sin ese libro, el depurador solo tiene direcciones, y las direcciones no significan nada para nosotros.
La información no está en un solo lugar: DWARF la reparte en secciones, cada una con un trabajo:
| Sección | Qué guarda | Para qué la usa GDB |
|---|---|---|
.debug_info |
Los tipos: funciones, variables, structs, enums | Saber qué es cada cosa y cómo imprimirla |
.debug_line |
La tabla de líneas: dirección ↔ archivo:línea | Traducir "estoy en 0x1129" a "estoy en main.c:42" |
.debug_abbrev |
Las abreviaturas (cómo se codifica cada tipo de dato) | Comprimir la información sin repetirla |
.debug_str |
Los textos: nombres de variables, de archivos | Mostrarte total y no v24 |
.debug_frame |
La información de desenrollado de la pila | Poder reconstruir la pila de llamadas |
Analogía del edificio. Imaginá un edificio de oficinas de 20 pisos. El ELF es el edificio físico: hormigón, caños, cables. La tabla de símbolos es el cartel con los nombres de las oficinas ("Recepción", "Contabilidad"), colgado en la planta baja. Y la información DWARF es el plano completo: qué hay en cada oficina, dónde está el baño, cómo subir al piso 7, y en qué punto exacto de la escalera estás cuando mirás el Termómetro del piso 5. Sin plano, podés caminar pero estás a ciegas; con plano, ubicás cualquier cosa al instante.
Ojo con esto. DWARF no es lo mismo que ELF. ELF es el envase y DWARF es el formato de la información que va adentro. En Windows con MSVC el envase es PE/COFF y la información de depuración es PDB (formato CodeView). En macOS el envase es Mach-O, pero DWARF también se usa adentro. O sea: el concepto viaja, el nombre del envase cambia.
1.4 Las banderas que lo cambian todo: -g, -g3, -O0
Acá está la parte que más resultado te da, y la que más se ignora. Hay dos decisiones independientes que hay que tomar al compilar:
- ¿Guardo información de depuración? →
-g,-g3 - ¿Optimizo el código? →
-O0,-Og,-O2
La primera decisión: cuánta información de depuración
GCC tiene tres niveles (más uno que apaga todo). El default de -g es el nivel 2:
| Bandera | Qué agrega | ¿Lo vas a usar? |
|---|---|---|
-g0 |
Nada. Apaga la información de depuración | Solo para binarios de producción stripped |
-g1 |
Mínimo: funciones, variables globales y tabla de líneas (sin variables locales) | Rara vez |
-g (= -g2) |
Todo lo de -g1 más variables locales y typedefs |
Tu default |
-g3 |
Todo lo de -g2 más las definiciones de macros |
Cuando el bug está en una macro |
El nivel 3 es el que casi nunca se enseña y el que más rescata. Cuando el código sospechoso es
CHECK_NULL(p) y esa macro viene de una cabecera de tu empresa, GDB con -g3 te puede mostrar
la expansión de la macro y decirte de qué archivo salió.
# Sin información de depuración
$ gcc -o demo demo.c
# Con lo justo (nivel 2, el default)
$ gcc -g -o demo demo.c
# Con macros también (nivel 3)
$ gcc -g3 -o demo demo.c
La segunda decisión: optimizar o no
Acá está la trampa clásica, y la reason por la que mucha gente cree que "GDB no funciona".
El optimizador (-O1, -O2, -O3) reescribe tu programa. Hace cosas que son correctas pero
que rompen la correspondencia entre "una línea de C" y "un fragmento de máquina":
TU CÓDIGO LO QUE EL COMPILADOR HACE CON -O2
───────── ──────────────────────────────────
int a = 1, b = 2; los dos valen 1 → el compilador
int c = a + b; borra las variables que no usa
if (x > 0) { ... } reordena, invierte, fusiona ramas
("if (x > 0)" puede pasar a ser
un salto hacia atrás)
suma += valores[i]; mete el bucle en un vector (SIMD):
4 elementos por instrucción
llama_a(); llama_b(); METE la función adentro de la otra
(inlining) → desaparece del mapa
Y las consecuencias para GDB son concretas:
Con -O0 |
Con -O2 |
|---|---|
| Cada línea de C ↔ un bloque de instrucciones | Varias líneas de C pueden caer en la misma instrucción |
| Las variables viven en la pila (memoria) | Las variables pueden vivir en un registro |
print x muestra el valor |
print x dice <optimized out> |
next avanza una línea |
next a veces saltea o retrocede |
El bug más famoso del principiante (versión depuración). Compilás con
-O2(porque "en producción va más rápido"), el programa falla, lo metés a GDB, y te encontrás con esto:(gdb) print counter $1 = <optimized out>No es un bug de GDB ni tuyo: el compilador legalmente eliminó la variable porque no hacía falta. No hay nada que mirar. Por eso la regla de abajo es innegociable.
La regla de oro
Depurá con
-O0 -g. Medí con-O2. Nunca al revés.Son dos compilaciones distintas del mismo código, para dos públicos distintos. La que corrés para medir Performance no es la que depurás, y viceversa.
Y si querés un punto medio moderno, GCC te da -Og:
$ gcc -Og -g -o demo demo.c
-Og activa solamente las optimizaciones que no estorban a la depuración (se la deja a
gcc decidir). Es el flag que recomienda la propia documentación de GCC cuando querés
"optimizado pero depurable".
La línea de compilación que vas a usar mil veces
$ gcc -std=c11 -g3 -O0 -Wall -Wextra -o demo demo.c
Desglosada:
| Parte | Qué te da |
|---|---|
-std=c11 |
El estándar de C que querés (capítulo 02) |
-g3 |
Información de depuración completa, incluidas las macros |
-O0 |
Sin optimización: una línea de C = un bloque de instrucciones |
-Wall -Wextra |
Todos los avisos del compilador (son la mitad de los bugs de origen) |
-o demo |
El nombre del ejecutable |
Truco de laboratorio: dos compilaciones, dos binarios. Guardá el comando de arriba en un archivo
build.shjunto a tu proyecto. Te va a ahorrar mucho tiempo y te garantiza que siempre estás depurando con la misma configuración.
1.5 Mirar el binario por fuera
Antes de entrar a GDB, conviene saber mirar el envase. Estos cuatro comandos son tus amigos:
$ file ./demo
demo: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, ...
file te dice qué es el archivo. Si dice not stripped, tiene tabla de símbolos.
$ nm ./demo
0000000000001129 T main
0000000000001139 t helper
0000000000004040 D global_counter
nm te da la tabla de símbolos: qué funciones y qué globales existen y en qué dirección.
T = texto (código), D = datos, t = símbolo local, U = indefinido (viene de otro lado).
$ objdump -d ./demo
...
0000000000001129 <main>:
1129: 48 89 e5 mov %rsp,%rbp
112c: 89 7d ec mov %edi,-0x14(%rbp)
...
objdump -d te da el desensamblado: el binario traducido a instrucciones.
$ size ./demo
text data bss dec hex filename
2192 16 8 2216 8a demo
size te dice cuánto ocupa cada parte.
Un detalle que da vergüenza preguntar y que hay que saber. Los binarios que se publican (los que descarga la gente de internet) suelen estar stripped: les sacaron la tabla de símbolos y la información de depuración para que ocupen menos. Es una decisión de distribución, no de compilación: el
demoque vos depurás tiene que tener-g. La forma sana de hacerlo es distribuir los símbolos aparte (el paquetedbgsymen Linux, los archivos.dSYMen macOS), para que quien recibe el programa pueda depurar sin que el binario crezca.
1.6 El checklist antes de empezar
Guardá esta lista. El 80 % de los "GDB no me funciona" son porque una de estas siete cosas no se cumplió.
[ ] 1. Tengo gcc/clang instalado y puedo compilar
[ ] 2. Compilo con -g3 -O0
[ ] 3. `file ./demo` dice "ELF ... not stripped" (o "with debug_info")
[ ] 4. `gdb --version` responde
[ ] 5. `gdb ./demo` abre y muestra el prompt (gdb)
[ ] 6. `break main` no se queja
[ ] 7. `run` para en `main`
Vamos a verificarlo juntos, en la primera práctica.
Práctica de la Parte 1
Ejercicio 20.1 — El binario desnudo y el binario con mapa
Conceptos · ELF, secciones, tabla de símbolos, DWARF, -g, -g3, file, readelf -S, nm
Contexto · Un mismo programa se puede compilar de dos maneras: como se publica (liviano, sin información de depuración) y como se depura (con toda la información). Los dos ejecutables hacen exactamente lo mismo. Lo que cambia es qué puede saber el depurador sobre ellos.
Consigna · Escribí un programa demo.c con una función global, una variable global y una
variable local, e imprimí (con printf) el valor de la variable global para que el programa
haga algo observable. Compilalo dos veces:
gcc -o demo_stripped demo.cgcc -g3 -O0 -o demo_debug demo.c
Después compará los dos binarios con estas herramientas y contestá:
file— ¿qué diferencia reporta?ls -l— ¿cuánto más pesa el que tiene información de depuración? ¿Por qué?readelf -S— ¿qué secciones aparecen en uno y no en el otro?nm— ¿cambia algo entre los dos? ¿Por qué?
Entrada · Ninguna: es una comparación de dos binarios. Salida · Un informe corto con las cuatro comparaciones y tu explicación de cada diferencia.
Escenario de prueba
$ ls -l demo_stripped demo_debug
-rwxr-xr-x 1 user user 16096 demo_stripped
-rwxr-xr-x 1 user user 22192 demo_debug
$ file demo_stripped
demo_stripped: ELF 64-bit LSB pie executable, x86-64, ..., stripped
$ file demo_debug
demo_debug: ELF 64-bit LSB pie executable, x86-64, ..., with debug_info, not stripped
Identificadores sugeridos · demo.c, demo_stripped, demo_debug, global_counter
Restricciones/pistas
- El ejecutable sin
-gno tiene por qué fallar: sigue funcionando perfectamente. Lo que pierde es la capacidad de ser depurado en detalle. readelf -Smuestra muchísimas secciones: a vos te interesan.symtab,.strtaby las que empiezan con.debug_.nmno cambia entre los dos: la tabla de símbolos la genera el compilador siempre. Lo que cambia es la información de depuración, que va en otras secciones.
Criterios de aceptación
- Los dos binarios se compilan con los comandos exactos del enunciado.
filereporta la diferencia de stripped / with debug_info.- La diferencia de tamaño está medida y explicada.
- Se identificó qué secciones
.debug_*aparecen solo en el binario con-g3. - Se respondió por qué
nmda lo mismo en los dos casos.
Ejercicio 20.2 — "No symbol table info available"
Conceptos · información de depuración, tabla de líneas, break, info line, diagnóstico
Contexto · Este es el error más común del primer día: querés poner un punto de interrupción en una línea y GDB te responde que no sabe de qué archivo hablás. No es un fallo de GDB: es la consecuencia directa de lo que aprendiste en 1.2.
Consigna · Reproducí exactamente ese error y diagnosticálo con las herramientas:
- Compilá un programa sin
-g. - Abrilo con GDB y pedí un punto de interrupción en una línea de código (por ejemplo, la línea 10 de tu archivo).
- Leé el mensaje de error de GDB entero y anotá qué te dice.
- Usá
info line <archivo>:<línea>yinfo functionspara juntar evidencia. - Recompilá con
-g3 -O0y repetí el paso 2: ahora debería funcionar.
Entrada · Ninguna. Salida · Un diagnóstico escrito: síntoma → evidencia → causa → solución.
Escenario de prueba
$ gcc -o demo demo.c
$ gdb ./demo
(gdb) break demo.c:10
No source file named demo.c.
Make breakpoints pending? (y or n)
$ gcc -g3 -O0 -o demo demo.c
$ gdb ./demo
(gdb) break demo.c:10
Breakpoint 1 at 0x1129: file demo.c, line 10.
Identificadores sugeridos · demo.c, demo
Restricciones/pistas
- Fijate en la diferencia entre el archivo fuente (
.c, que es el que vos escribís) y el archivo objeto (.o): GDB no abre.c, abre el ejecutable y busca en él la información de depuración que le dice qué hay endemo.c. info functionsresponde qué funciones conoce GDB. Si la lista está vacía o es mínima, no hay información de depuración.
Criterios de aceptación
- El error se reprodujo de verdad (no de memoria).
- El mensaje de error de GDB está citado textualmente en el diagnóstico.
- Se usó
info lineyinfo functionscomo evidencia. - La causa está explicada en términos de la Parte 1 (secciones
.debug_*), no como "hay que poner-g". - La solución se verificó recompilando y repitiendo el paso 2.
PARTE 2 — La interfaz TUI: la pantalla partida
Ya tenés el terreno. Ahora sí: sentate frente a GDB.
La primera vez que arrancás GDB, la tentación es usarlo como si fuera una terminal más: escribís comandos, leés texto. Funciona. Pero es como usar una calculadora científica para sumar dos números: GDB tiene una pantalla dividida en paneles que la mayoría nunca descubre.
2.1 Arrancar GDB
Tres formas de entrar, y una cuarta que es la más útil de todas:
$ gdb ./demo # el programa está en ./demo
$ gdb demo # igual (GDB busca el archivo)
$ gdb --args ./demo a b c # el programa y SUS argumentos
$ gdb ./demo core.core # un core dump (Parte 5)
--args es la clave: GDB no ejecuta el programa al arrancar, así que los argumentos que le
pasás son exactamente los que va a usar el programa cuando le digas run. Después los podés
cambiar sin recompilar:
(gdb) set args 10 20 30
(gdb) show args
Argument list to give program when it is started is "10 20 30".
Al arrancar, GDB se presenta y te da el prompt. Ese prompt es la única cosa que tenés que mirar:
GNU gdb (GDB) 14.2
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
...
(gdb)
El prompt (gdb) es una REPL: un bucle que lee un comando, lo ejecuta, imprime el resultado y
vuelve a leer. Igual que el >>> de Python. Los comandos no llevan punto y coma.
Y como en cualquier REPL, hay ayuda:
(gdb) help # lista de categorías
(gdb) help breakpoint # ayuda de un comando concreto
(gdb) apropos watch # busca comandos que contengan "watch"
(gdb) break ma<TAB> # el autocompletado completa a "break main"
Momento ajá.
aproposy el autocompletado conTABson los dos atajos que hacen que GDB pase de "mando a ciegas" a "herramienta de trabajo".aproposes tu buscador: si sabés lo que querés pero no cómo se llama el comando,aproposte lo encuentra.
2.2 La ventana de comandos
En modo TUI, la pantalla no es un scrolls de texto: es un conjunto de ventanas. La de abajo se
llama cmd y es la ventana de comandos: escribís ahí, y arriba se va redibujando el estado
del programa.
El layout por defecto es simple: la ventana de código fuente arriba y la de comandos abajo.
+-----------------------------------------------------------+
| src: main.c |
| |
| 8 int total = 0; |
| 9 for (int i = 0; i < count; i++) { |
| 10 total += values[i]; |
| >11 } |
| |
| 12 printf("total = %d\n", total); |
| |
+-----------------------------------------------------------+
| (gdb) print total |
+-----------------------------------------------------------+
La línea que tiene el > adelante es la línea actual: dónde está el programa ahora mismo. En
GDB, ese > se llama el arrow marker y es tu lugar en el mapa.
Tecla rápida. La flecha
>no es decorativa: si pulsásCtrl+G(o hacés clic), GDB te muestra dónde está el puntero de instrucción actual respecto al archivo.
2.3 Las ventanas que tenés disponibles
GDB trae un juego de ventanas prefabricadas. Cada una muestra un aspecto distinto del programa:
| Ventana | Qué muestra | Para qué la querés |
|---|---|---|
src |
El código fuente con el > en la línea actual |
Orientarte: ¿dónde estoy? |
asm |
El ensamblador de la función actual | Ver qué hace la máquina (Parte 6) |
regs |
Los registros del procesador | Ver rax, rsp, rip (Parte 6) |
stack |
La pila de llamadas (frames) | Ver quién llamó a quién (Parte 4) |
threads |
La lista de hilos | Depurar programas concurrentes (Parte 7) |
breakpoints |
Los puntos de interrupción | Ver y administrar tus paradas |
cmd |
La ventana de comandos | Escribir comandos |
status |
El estado: proceso, señales, frames | Un resumen de todo |
history |
El historial de comandos | Volver a un comando anterior |
expr |
La expresión que estás por evaluar | Historial de expresiones |
block |
Los bloques de código de la función actual | Ver la estructura del código |
global |
Las variables globales | Ver el estado global |
Para guardar.
stack,threadsybreakpointsson las que más rinden en el día a día.regsyasmlas vas a necesitar en la Parte 6.cmdestá siempre: es la única que no desactives nunca.
2.4 Cambiar la disposición
Acá es donde GDB deja de ser una terminal y se vuelve una console de depuración.
Poner una ventana
El comando layout mete una ventana en la disposición actual:
(gdb) layout src # ventana de código fuente
(gdb) layout stack # ventana de pila de llamadas
(gdb) layout regs # ventana de registros
(gdb) layout cmd # ventana de comandos
Ver la disposición actual
(gdb) show layout
Selected layout:
1) src
2) stack
3) regs
4) cmd
Recorrer los layouts prefabricados
GDB trae varios layouts ya armados (el clásico de "código arriba, comandos abajo", el de "pila a la derecha", etc.). Para rotar entre ellos:
(gdb) tui next-layout
Y para elegir uno por nombre:
(gdb) tui select-layout first
(gdb) tui select-layout default
(gdb) tui select-layout legacy
El atajo que casi nadie conoce:
tui new-layout. Armá las ventanas como te gusten (layout src,layout asm,layout regs,layout cmd), y después:(gdb) tui new-layout depurarGDB guarda esa disposición con nombre. Después la recuperás cuando quieras con
tui select-layout depurar. Es un.gdbinithecho a medida, y en la Parte 7 lo vas a ver automatizado.
Mover el foco
El teclado actúa sobre la ventana que tiene el foco. Para cambiarlo:
(gdb) tui focus src # el foco pasa al código
(gdb) tui focus next # al siguiente
(gdb) tui focus prev # al anterior
Un layout de trabajo típico
Uno de los que vas a usar mucho cuando estés con problemas de memoria:
+---------------------------+-------------------------+
| src: main.c | stack |
| | #0 main () at main.c |
| 42 free(p); | #1 run () at run.c |
| >43 p = NULL; | #2 ... |
| | |
+---------------------------+-------------------------+
| regs | cmd |
| rax 0x0 rbx 0x... | (gdb) |
+---------------------------+-------------------------+
2.5 Redibujar la pantalla
A veces la TUI se desordena: pasaste un less interno, la terminal cambió de tamaño, o
pegaste algo raro. GDB tiene comandos para arreglarla:
| Qué | Cómo |
|---|---|
| Redibujar todo | refresh |
| Redibujar con el teclado | Ctrl+L |
| Ajustar el alto/ancho que GDB cree | set height 40, set width 120 |
| Cambiar el estilo de los bordes | set tui border-mode ascii (para terminales viejitas) |
| Ver la configuración | show tui |
El borde
asciies tu amigo en Windows. En la terminal de Windows (y en algunos emuladores de terminal viejos), los bordes con caracteres de línea de Unix se ven como caracteres raros.set tui border-mode asciilos reemplaza por+,-,|. Se ve menos elegante pero se lee perfecto.
La trampa N.º 1 de la TUI: perdiste el foco. Si escribís
print totaly en pantalla no pasa nada, casi seguro estás en la ventana equivocada.tui focus cmdy listo. Y si la pantalla está del revés o medio rota:tui disable(vuelve al modo texto clásico),tui enable(vuelve a la TUI) y listo.
2.6 Alternar entre TUI y línea de comandos
A veces la TUI molesta: cuando querés pegar un comando largo, o cuando la salida es enorme. GDB vive en los dos mundos y los dos mundos se alternan:
(gdb) tui disable # al modo clásico: solo texto
(gdb) tui enable # de vuelta a la pantalla partida
En el modo clásico, todo lo que aprendiste funciona igual. La TUI es una capa visual, no un modo distinto de depurar.
Y hay dos comandos que vale la pena conocer desde el primer día:
$ shell ls -la # ejecutás un comando de shell SIN salir de GDB
(gdb) set logging file session.txt
(gdb) set logging on
(gdb) run
(gdb) set logging off
Truco de laboratorio: grabá tus sesiones.
set logging onescribe todo lo que pasa (la salida del programa, los comandos, los resultados) en un archivo de texto. Cuando entregues el informe de una práctica, ese archivo es tu evidencia. Es el equivalente en texto de la caja negra del avión.
2.7 Mapa de atajos de la TUI
| Atajo | Qué hace |
|---|---|
Ctrl+L |
Redibuja la pantalla |
Ctrl+C |
Interrumpe el programa que está corriendo |
Ctrl+D |
Salir de GDB (en la ventana de comandos) |
↑ / ↓ |
Historial de comandos (con el foco en cmd) |
TAB |
Autocompletado de comandos y de nombres de variables |
Ctrl+G |
Muestra el estado del programa en la ventana de estado |
PageUp / PageDown |
Desplazar la ventana de código |
Práctica de la Parte 2
Ejercicio 20.3 — Tu consola de depuración
Conceptos · TUI, layout, tui next-layout, tui new-layout, tui focus, refresh
Contexto · La TUI se configura. Cada persona trabaja distinto: hay quienes quieren la pila de llamadas siempre visible, y quienes prefieren ver los registros. GDB te deja armar tu consola.
Consigna · Con un programa de prueba en marcha:
- Poné la ventana de código fuente (
layout src) y la de comandos (layout cmd). - Recorré los layouts prefabricados con
tui next-layouty anotá cuáles existen en tu versión de GDB (el comandohelp tuite los lista). - Armá una disposición con código fuente y pila de llamadas (
layout stack) que te sea cómoda, y guardala con nombre:tui new-layout <nombre>. - Recuperala con
tui select-layout <nombre>. - Practicá el foco:
tui focus src,tui focus cmd,tui focus next. - Provocá un desborde: hacé
tui disableytui enabley comprobá que la pantalla vuelve a estar sana.
Entrada · Ninguna. Salida · Un layout guardado con nombre, y una nota con los atajos que te quedaron cómodo.
Escenario de prueba
(gdb) layout src
(gdb) layout stack
(gdb) layout cmd
(gdb) tui new-layout depurar
Saved new TUI layout "depurar"
(gdb) tui select-layout depurar
Criterios de aceptación
- Existe un layout con nombre guardado y recuperable.
- El stack (
layout stack) y el source (layout src) aparecen juntos en el layout final. - Se comprobó que el foco se mueve entre ventanas con
tui focus. tui disable/tui enabledeja la pantalla sana.- El layout se documentó con un nombre que signifique algo para vos.
Ejercicio 20.4 — El tour de la TUI
Conceptos · ventanas de la TUI, layout, show layout, foco, lectura del estado
Contexto · Conocer las ventanas disponibles es como conocer los instruments de un tablero: no vas a usar todos todos los días, pero cuando uno falla necesitás reconocerlo al instante.
Consigna · Hacé un recorrido documentado de cada ventana de la TUI. Para cada una:
- Ponela en pantalla con
layout <ventana>. - Poné el foco en ella con
tui focus <ventana>. - Escribí qué información muestra y para qué te sirve.
- Buscá un momento del programa donde esa ventana tenga contenido interesante (por ejemplo:
la ventana
stackcon varias funciones en la pila, o la ventanaregscon valores distintos de cero).
Completá esta tabla en tu cuaderno o en un archivo de texto:
ventana │ ¿qué muestra? │ ¿cuándo me sirve?
─────────────┼──────────────────────────────────────┼──────────────────────────
src │ │
asm │ │
regs │ │
stack │ │
threads │ │
breakpoints │ │
status │ │
cmd │ │
Entrada · Ninguna. Salida · La tabla completa.
Escenario de prueba
(gdb) layout regs
(gdb) tui focus regs
(gdb) info registers
rax 0x1 0
rbx 0x0 0
rip 0x401136 0x401136 <main+13>
rsp 0x7fffffffe320 0x7fffffffe320
Identificadores sugeridos · tour.md, una tabla por ventana
Restricciones/pistas
- Las ventanas que GDB muestra vacías (porque el programa no tiene hilos, o no hay breakpoints) también son información: te dicen que eso todavía no pasó.
help tuilista todo lo que la TUI sabe hacer en tu versión.- No hace falta que el programa sea complejo: uno con tres funciones anidadas alcanza para que
stackse vea interesante.
Criterios de aceptación
- Las 8 ventanas están documentadas con qué muestran y cuándo sirven.
- Cada ventana se focused (no solo puesta: el foco importa).
- Hay al menos una ventana capturada con contenido no trivial.
- La tabla distingue las ventanas de "estado" de las ventanas de "contenido".
PARTE 3 — Control de ejecución e inspección básica
Tenés el programa cargado, la pantalla dividida y el prompt esperando. Ahora viene lo central: frenar el programa en el lugar justo y mirar lo que hay ahí.
3.1 Ejecutar el programa
Hay tres formas de arrancar, y cada una frena en un lugar distinto:
| Comando | Qué hace | Dónde frena |
|---|---|---|
run |
Ejecuta el programa completo | En el próximo punto de interrupción |
start |
Ejecuta hasta el comienzo de main |
En la primera instrucción de main |
starti |
Ejecuta hasta la primera instrucción del programa | En _start, antes de main |
(gdb) start
Temporary breakpoint 1 at 0x1129: file main.c, line 8.
[Thread debugging using libthread_db enabled]
Temporary breakpoint 1 at 0x1129: file main.c, line 8.
Continuing.
Breakpoint 1, main () at main.c:8
8 int total = 0;
(gdb)
Leé esa salida con cuidado, porque tiene mucha información:
Temporary breakpoint 1→ GDB puso un breakpoint temporal enmainy lo va a borrar solo cuando se dispare.Temporary breakpoint 1 at 0x1129: file main.c, line 8.→ la dirección del código, el archivo y la línea. Acá GDB usó la información DWARF para traducir.Breakpoint 1, main () at main.c:8→ estamos parados. El nombre de la función y la línea.8 int total = 0;→ el código fuente de esa línea, mostrado automáticamente. Esta es la forma más cómoda de orientarse: GDB te muestra el código donde estás.
La diferencia entre
runystart, en una frase.rundeja el programa correr hasta que pase algo interesante (un breakpoint, un crash).startte deja dentro demain, listo para que examines las variables locales desde el primer momento. Si vas a depurar una función concreta,start+break <función>+continuees el combo padrão.
3.2 Puntos de interrupción: frenar el programa
El breakpoint es el corazón de la depuración. Es una marca que dice "stop acá". Cuando el programa llega a esa marca, GDB frena el proceso justo antes de ejecutar esa instrucción y te devuelve el control.
Hay cuatro formas de poner uno:
(gdb) break main # por nombre de función
(gdb) break demo.c:42 # por archivo y línea
(gdb) break 42 # por línea del archivo actual
(gdb) break *0x401136 # por dirección de memoria
Cuál usar. El 90 % de las veces, break archivo.c:línea. Es la más precisa y la que mejor
funciona cuando el archivo tiene funciones repetidas o nombres parecidos.
Y hay dos variantes que resuelve problemas específicos:
(gdb) tbreak demo.c:42 # TEMPORAL: se borra solo después de dispararse
tbreak (de temporary) es perfecto para "pasame por acá una sola vez". Si ponés un break
normal y el código pasa mil veces por esa línea, vas a tener que seguir pulsando continue mil
veces. Con tbreak, seguís una vez y seguís libre.
(gdb) dprintf demo.c:42, "i = %d\n", i # al pasar por acá, imprime y sigue
dprintf es un breakpoint que imprime y sigue. No te frena: te deja una nota en la salida.
Ideal para instrumentar un bucle sin tocar el código.
Ojo con esto. El
breakse pone en una línea ejecutable. Si ponésbreak demo.c:7y la línea 7 es un comentario, un}o la declaración de una función, GDB te va a decir que no puede poner un breakpoint ahí y te va a ofrecer moverlo a la línea siguiente. Es normal: no es un error tuyo.
3.3 Administrar tus puntos de interrupción
Cuando tenés varios, necesitás verlos y administrarlos.
Verlos
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000001129 in main() at demo.c:8
2 breakpoint keep y 0x0000000000001160 in add_item() at demo.c:20
3 breakpoint keep y 0x0000000000001175 in main() at demo.c:42
breakpoint already hit 1 time
Cada punto tiene un número. Ese número es su nombre, y lo vas a usar para todo lo demás.
keep = queda puesto; y/n = habilitado o deshabilitado; Address = la dirección de
memoria; What = la función y el archivo:línea.
Deshabilitar, habilitar, borrar
(gdb) disable 2 # desactiva el breakpoint 2 (no lo borra)
(gdb) enable 2 # lo vuelve a activar
(gdb) delete 2 # lo borra
(gdb) disable # sin número: desactiva el ÚLTIMO creado
(gdb) delete # sin número: borra TODOS (pregunta confirmación)
Truco de laboratorio: deshabilitar es mejor que borrar. Si un breakpoint te molesta mientras investigás otra cosa,
disable 2lo apaga sin perderlo.deletese lleva el numero puesto: y los números se reutilizan, lo cual confunde.
Condicionar un breakpoint
Un breakpoint condicional solo se dispara si la condición es verdadera. Es el comando más poderoso del capítulo, y lo vemos en detalle en la Parte 4.
(gdb) break demo.c:42 if i == 7
Automatizar un breakpoint: commands
Acá GDB deja de ser interactivo. Con commands le decís "cuando se dispare este breakpoint,
hacé esto":
(gdb) break add_item
(gdb) commands
> printf "add_item: count = %d, cap = %d\n", g_count, g_capacity
> continue
> end
(gdb) run
Cada línea entre commands y end es un comando que GDB ejecuta automáticamente cada vez que
se dispara el breakpoint. El continue final es el que deja el programa seguir.
El poder real de
commands. Con esto podés instrumentar un programa sin tocar su código: contadores, trazas, validaciones. Es la base de la automatización de la Parte 7 y del taller final.
3.4 Controlar el flujo: las cinco palancas
Acá está el corazón de la depuración interactiva. Estos cinco comandos te dejan avanzar de a una instrucción, y son los que vas a usar el 90 % del tiempo.
| Comando | Atajo | Qué hace | Entra a las funciones |
|---|---|---|---|
next |
n |
Avanza a la línea siguiente | No |
step |
s |
Avanza a la línea siguiente | Sí |
finish |
fin |
Corre hasta que la función actual termine | — |
continue |
c |
Corre hasta el próximo breakpoint | — |
until |
u |
Corre hasta una línea más abajo de la actual | No |
next contra step: la diferencia que lo cambia todo
Esta es la pregunta que todos se hacen el primer día. Y la respuesta es una analogía de calle:
Imaginá que estás caminando por una calle y mirás una panadería.
next: seguís de largo por la vereda. Viste la panadería, seguiste, no entraste.step: cruzás la calle y entrás a mirar qué hacen adentro.
En el código:
void procesar(int *datos, int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += sumar(datos[i]); /* línea 4 */
}
printf("total = %d\n", total); /* línea 6 */
}
- Con
nexten la línea 4, el próximonextte deja en la línea 6:sumar()se ejecutó entera, pero no te preocupaste por lo que pasó adentro. - Con
stepen la línea 4, el próximostepte mete dentro desumar().
Regla práctica. Si estás buscando un valor (
print total), usánext: no te interesa lo que pasa adentro de las funciones, te interesa llegar a la línea siguiente. Si estás rastreando el camino de los datos ("¿quién me cambió esta variable?"), usástep: querés ver cada llamada.
finish: salir de la función sin apretar step 300 veces
finish corre hasta que la función actual retorne y te deja en la línea de llamada. Es el
"ya está, volvamos arriba" de la depuración.
(gdb) step
_sumar () at math.c:5
5 return x * 2;
(gdb) finish
Run till exit from _sumar () at math.c:5
total = 14
9 printf("total = %d\n", total);
La línea total = 14 es la salida del programa: finish deja que el programa siga corriendo
normalmente hasta que vuelve a vos.
until: saltar hasta una línea concreta
until es como next, pero "hasta acá y no más allá". Se usa para saltear un bloque largo:
(gdb) until 20
Si estás en la línea 5 y querés llegar a la 20 sin pasar por la 18, until lo hace. Y si en
el camino se dispara un breakpoint, se para ahí (igual que continue).
return: salir ahora (con Warning)
(gdb) return 42
Make selected stack frame return now? (y or n) y
return fuerza el retorno de la función actual con el valor que le des. Es útil para probar
hipótesis ("¿y si esta función devolviera 42, el bug seguiría?"), pero es una mentira: el
programa no vuelve de la función de verdad, la función se saltea. Usalo con cuidado y solo para
experimentar.
3.5 Inspeccionar: print y compañía
print (o p) es el comando que más vas a usar. Imprime el valor de una expresión en el
momento actual.
(gdb) print total
$1 = 0
(gdb) print total + 1
$2 = 1
(gdb) print i * n
$3 = 12
Detalle importante: podés imprimir cualquier expresión, no solo variables. Y GDB te devuelve
un número de referencia ($1, $2, …). Ese número te deja referenciar el valor después:
(gdb) print total
$1 = 0
(gdb) print $1 + 100
$2 = 100
Formatear la salida
El comando print acepta sufijos que cambian el formato:
| Sufijo | Formato | Ejemplo |
|---|---|---|
/x |
Hexadecimal | p/x total → $1 = 0x0 |
/d |
Decimal | p/d total |
/u |
Decimal sin signo | p/u valor |
/c |
Carácter | p/c letra → $1 = 65 'A' |
/s |
Cadena | p/s nombre |
/f |
Punto flotante en varios formatos | p/f valor |
(gdb) print/x total
$1 = 0x0
(gdb) print/c letra
$2 = 65 'A'
Los otros comandos de inspección
(gdb) ptype total # el TIPO de la variable
$1 = int
(gdb) info locals # todas las locales del frame actual
total = 0
i = 0
(gdb) info args # los argumentos de la función actual
n = 5
datos = 0x7fffffffe320
(gdb) info variables # TODAS las variables del programa (¡muchas!)
(gdb) info functions # todas las funciones conocidas
(gdb) info scope procesar # variables y miembros visibles en esa función
(gdb) info line demo.c:42 # qué código hay en esa línea
(gdb) info address total # la dirección de una variable
ptype es especialmente útil: te dice cómo está declarada la variable, incluso si no la
podés ver. Con ptype struct Item te imprime la definición completa de la struct.
Y hay dos comandos para imprimir con formato (más parecidos a printf):
(gdb) printf "total=%d i=%d\n", total, i
total=0 i=0
(gdb) output total
0
output imprime sin registrar el valor en el historial (útil en scripts).
Ajustar cómo se imprime
Estos son los set que más se tocan:
(gdb) set print pretty on # las structs se muestran en varias líneas
(gdb) set print elements 0 # Arrays: SIN límite (por defecto, 200 elementos)
(gdb) set print repeats 0 # no abreviar arrays con "[...]"
(gdb) set print address on # mostrar la dirección de los punteros
set print elements 0 es imprescindible cuando trabajás con arreglos grandes: sin eso,
print arr te muestra solo los primeros 200 elementos y un ... que te puede hacer creer que
el arreglo es más chico.
3.6 display y watch: mirar sin parar, y parar cuando cambia
Estos dos comandos parecen parecidos y no lo son. La diferencia está en cuándo actúan:
| Comando | Qué hace | ¿Frena el programa? |
|---|---|---|
print x |
Imprime x una vez, ahora |
No |
display x |
Imprime x cada vez que el programa se frena |
No (solo muestra) |
watch x |
Frena el programa cuando x cambia |
Sí |
display: el monitor de variables
(gdb) display total
1: total = 0
(gdb) display i
2: i = 0
(gdb) next
total = 0
i = 0
3 total += datos[i];
Cada vez que avanzás, GDB vuelve a imprimir los valores. Es exactamente lo que querés cuando estás siguiendo una variable a lo largo de un bucle.
Para administrarlos:
(gdb) display # lista los displays actuales
1: total = 0
2: i = 0
(gdb) undisplay 1 # saca el display 1
(gdb) undisplay # saca todos
(gdb) delete display 1 # alternativa
Truco de laboratorio.
display+nexten un bucle es el patrón clásico para seguir una variable. No es lo más elegante del mundo (la salida se llena de ruido), pero funciona y es lo primero que tenés que tener a mano.
watch: el centinela
watch frena el programa en el instante en que la expresión cambia de valor. Es la herramienta
más precisa del capítulo para encontrar dónde se corrompe un dato, y la vemos en detalle en la
Parte 4.
(gdb) watch counter
Hardware watchpoint 2: counter
(gdb) continue
...
Hardware watchpoint 2: counter
Old value = 3
New value = 4
Práctica de la Parte 3
Ejercicio 20.5 — Organizar puntos de interrupción
Conceptos · break, tbreak, delete, disable, enable, info breakpoints, commands
Contexto · Un programa con varias funciones se depura con varios puntos de interrupción. Pero no todos sirven siempre, y dejar puntos de más es ruido que tapa la señal.
Consigna · Escribí un programa reloj.c con tres funciones (main, tic, tac) donde
cada una imprima algo. Después:
- Poné un punto de interrupción por función en las tres (
break main,break tic,break tac). - Poné un punto de interrupción por archivo y línea dentro de
tic. - Poné un punto de interrupción temporal en
tac. - Listalos con
info breakpointsy anotá qué muestra cada columna. - Deshabilitá uno, habilitalo de nuevo, y borrá otro.
- Dejá un punto de interrupción con
commandsque imprima un mensaje y siga.
Entrada · Ninguna.
Salida · El listado de info breakpoints en cada estado, y una nota de qué hace cada columna.
Escenario de prueba
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000001129 in main() at reloj.c:12
2 breakpoint keep y 0x0000000000001150 in tic() at reloj.c:5
3 breakpoint keep y 0x0000000000001168 in tac() at reloj.c:9
4 breakpoint keep y 0x0000000000001155 in tic() at reloj.c:6
5 breakpoint dlt y 0x0000000000001168 in tac() at reloj.c:9
Identificadores sugeridos · reloj.c, tic, tac
Restricciones/pistas
- Fijate en la columna
Disp:keep= permanente,dlt= delete (temporal, se borra solo). - La columna
Enbte dice si el breakpoint está activo (y) o apagado (n). - El
Whatte dice dónde va a parar: eso es el mapa de tu investigación.
Criterios de aceptación
- Hay al menos un breakpoint por función, uno por archivo:línea y uno temporal.
- El listado de
info breakpointsestá capturado y anotado. - Se demuestra el ciclo
disable→enabley eldelete. - El breakpoint con
commandsimprime un mensaje automáticamente al dispararse.
Ejercicio 20.6 — Instrumentar con commands
Conceptos · commands, printf, continue, break, automatización
Contexto · A veces no querés frenar el programa: querés que te avise cuando pasa algo.
commands convierte un breakpoint en un sensor: cada vez que se dispara, deja una nota.
Consigna · Escribí un programa cola.c que modele una cola con un arreglo de enteros y dos
funciones: encolar(int valor) y desencolar(void). La primera escribe en la posición ultimo y
la segunda saca de la posición primero.
- Poné un breakpoint en
encolarconcommandsque imprima el valor recibido y el índice de la posición de escritura, y que siga automáticamente. - Corré el programa con cinco encolados y capturá la salida completa.
- Ahora hacé lo mismo con
desencolar. - Repetí el punto 1 pero sin el
continuefinal: observá qué pasa y explicá por qué.
Entrada · Ninguna: el programa se prueba con entradas fijas. Salida · La salida del programa con las trazas de GDB intercaladas.
Escenario de prueba
(gdb) break encolar
(gdb) commands
> printf "encolar: valor=%d ultimo=%d\n", valor, ultimo
> continue
> end
(gdb) run
encolar: valor=10 ultimo=0
encolar: valor=20 ultimo=1
encolar: valor=30 ultimo=2
Identificadores sugeridos · cola.c, encolar, desencolar, valores, primero, ultimo
Restricciones/pistas
- El
printfde GDB usa la misma sintaxis que elprintfde C:%d,%s,%x… - Los argumentos de
printfen GDB se evalúan en el contexto del breakpoint: es decir, en el frame de la función donde estás. - Si
printfdiceNo symbol "valor" in current context, es que el breakpoint no está en el frame donde vive esa variable.
Criterios de aceptación
- El breakpoint con
commandsse dispara automáticamente en cada llamada. - La salida de GDB y la del programa se distinguen claramente.
- El caso sin
continueestá explicado (el programa queda parado en el primer disparo). - El
printfusa variables reales del frame del breakpoint.
Ejercicio 20.7 — Describir un tipo con el depurador
Conceptos · print, ptype, info locals, info args, set print pretty on, struct
Contexto · Antes de lógica viene la descripción: saber qué hay adentro de una struct,
cómo se llama cada campo y qué tipo tiene. En el depurador podés descubrirlo sin abrir el archivo
fuente, y a veces el depurador te dice más que el fuente.
Consigna · Escribí un programa ficha.c con una struct de al menos cuatro campos (con
tipos distintos: int, double, char[] y un puntero) y una función que la recibe por puntero.
- Poné un breakpoint dentro de la función.
- Usá
ptypepara imprimir la definición del tipo de lastructy de cada campo. - Usá
printpara mostrar lastructcompleta, y después campo por campo. - Usá
printsobre el puntero a la struct, y sobre el puntero desreferenciado. - Activá
set print pretty ony compará cómo cambia la salida. - Usá
info localseinfo argspara ver todo el frame de una sola vez.
Entrada · Ninguna. Salida · Un volcado anotado de cada comando, con el tipo y el valor que devolvió.
Escenario de prueba
(gdb) ptype struct Ficha
struct Ficha {
int id;
double peso;
char nombre[32];
int *scores;
}
(gdb) print ficha
$1 = {id = 7, peso = 72.5, nombre = "Ana", scores = 0x7fffffffe330}
Identificadores sugeridos · ficha.c, struct Ficha, id, peso, nombre, scores
Restricciones/pistas
printde unastructcon unchar[]muestra el texto hasta el primer'\0'. Si ves basura, el array no está terminado — y eso puede ser el bug.ptypeacepta un tipo (ptype struct Ficha) o una expresión (ptype ficha).- Con
set print pretty onla struct se muestra en varias líneas, que es más legible cuando tiene muchos campos.
Criterios de aceptación
ptypedevuelve la definición completa de la struct.- Cada campo se imprimió individualmente y coincide con su tipo.
- El puntero y su desreferencia se imprimieron por separado y son distintos.
set print pretty oncambió la salida y el cambio está documentado.
PARTE 4 — Punteros, estructuras y control avanzado
Ahora vamos a donde se cocina el plato fuerte: mirar lo que hay en la memoria.
4.1 Direcciones y desreferenciación
En C, un puntero es una variable que guarda una dirección. GDB te deja ver las dos cosas por separado, y esa separación es la clave de todo lo que viene:
(gdb) print &total # la DIRECCIÓN de total
$1 = (int *) 0x7fffffffe320
(gdb) print total # el VALOR de total
$2 = 0
(gdb) print *&total # la dirección, desreferenciada → el valor
$3 = 0
& significa "la dirección de". * significa "el valor que hay en esa dirección". Con esos dos
operadores podés moverte por la memoria en cualquier dirección:
(gdb) print *0x7fffffffe320
$1 = 0
GDB acepta direcciones literales. Y si la variable es un puntero:
(gdb) print p # el VALOR de p (una dirección)
$1 = 0x7fffffffe330
(gdb) print *p # lo que HAY en esa dirección
$2 = 42
La analogía.
pes una ficha con la dirección de un sobre.*pes abrir el sobre y ver lo que hay adentro.&totales la dirección del sobre. Confundirpcon*pes el error conceptual más común de la primera hora — y es exactamente el error que produce elSegmentation Faultdel capítulo siguiente.
Y para ver los tamaños:
(gdb) print sizeof(total)
$1 = 4
(gdb) print sizeof(struct Ficha)
$2 = 56
4.2 Arreglos: los diez elementos de un vistazo
Un arreglo es un bloque contiguo de memoria. GDB lo sabe y lo muestra completo:
(gdb) print datos
$1 = {4, 8, 15, 16, 23, 42}
(gdb) print datos[2]
$2 = 15
(gdb) print *datos
$3 = 4
Y con la sintaxis de arreglo artificial podés ver una porción arbitraria:
(gdb) print *datos@10
$1 = {4, 8, 15, 16, 23, 42, 0, 0, 0, 0}
p *datos@10 significa: "tratá datos como si fuera un arreglo de 10 elementos y mostrame los 10".
El @10 es el artificial array de GDB. Es la herramienta clave para mirar buffers que
se desbordaron: si el arreglo real tiene 6 elementos y el buffer tiene 10, los últimos cuatro
son basura.
Momento ajá. La sintaxis del arreglo artificial es una de esas cosas que parece un error de tipeo la primera vez que la ves.
@no es "dirección": es el operador que le dice a GDB "esto que sigue es un arreglo de N elementos". Masterizala: la vas a usar muchísimo.
Dos cosas más:
(gdb) print &datos
$1 = (int (*)[6]) 0x7fffffffe330
&datos es la dirección del arreglo completo (tipo int (*)[6]), mientras que datos decae a
puntero al primer elemento. Son direcciones distintas (¡y por eso &datos + 1 avanza 24 bytes,
no 4!).
(gdb) set print elements 0
Con esto, los arreglos se muestran enteros, sin el límite de 200 elementos.
4.3 Estructuras: dos sintaxis, un mismo dato
Una struct se imprime completa, y después podés entrar a sus campos:
(gdb) print ficha
$1 = {id = 7, peso = 72.5, nombre = "Ana", scores = 0x7fffffffe330}
(gdb) print ficha.id
$2 = 7
(gdb) print ficha.peso
$3 = 72.5
Con punteros a struct, la sintaxis cambia: usás -> en lugar de .:
(gdb) print *pf
$1 = {id = 7, peso = 72.5, nombre = "Ana", scores = 0x7fffffffe330}
(gdb) print pf->id
$2 = 7
(gdb) print pf->nombre
$3 = "Ana"
Y si querés ver un arreglo de structs:
(gdb) print *fichas@3
$1 = {{id = 1, ...}, {id = 2, ...}, {id = 3, ...}}
La trampa N.º 1 de las structs.
print fichayprint *pfmuestran lo mismo, peroprint ficha.idno compila sifichaes un puntero, yprint pf->idno compila sipfno lo es. GDB te avisa con un error bastante críptico. La regla: si tenés puntero, usá->; si tenés la struct misma, usá..
Para ver la definición del tipo (no los valores):
(gdb) ptype struct Ficha
struct Ficha {
int id;
double peso;
char nombre[32];
int *scores;
}
Y para ver dónde vive cada campo dentro de la struct (muy útil para entender el
padding del capítulo 08):
(gdb) ptype /o struct Ficha
struct Ficha {
int id; /* offset 0, 4 bytes */
double peso; /* offset 8, 8 bytes */
char nombre[32]; /* offset 16, 32 bytes */
int *scores; /* offset 48, 8 bytes */
}
4.4 Breakpoints condicionales: el filtro que te salva del corso
Un breakpoint normal frena siempre. En un bucle de 100.000 iteraciones, eso es una tortura. Un breakpoint condicional frena solo cuando se cumple la condición:
(gdb) break procesar
(gdb) condition 1 i == 500
O todo de una:
(gdb) break procesar if i == 500
Y hay un comando gemelo que es todavía más útil: ignore.
(gdb) break procesar
(gdb) ignore 1 499
ignore 1 499 significa: "el breakpoint 1, ignorá las primeras 499 veces que se dispare".
Con eso, en la iteración 500 te frena exactamente. Es la manera de saltar a una iteración
sin que te frene en todas las anteriores.
Y el combo definitivo, el que se usa todo el tiempo:
(gdb) break procesar
(gdb) condition 1 i == 500
(gdb) commands
> silent
> print i
> print total
> continue
> end
El silent (que solo existe dentro de commands) evita que GDB anuncie el breakpoint cada vez.
El resultado es un programa que se auto-explica: imprime lo que te interesa y sigue.
Para guardar. La combinación
break+condition+ignore+commandses, honestamente, la mitad de la potencia de GDB. Es lo que convierte "un depurador" en "un instrumento de medición".
4.5 Puntos de observación: watch
Un breakpoint dice "frená en este punto". Un watchpoint dice "frená cuando esto cambie".
Es la diferencia entre "fijate en la línea 20" y "fijate en el valor de count".
(gdb) watch count
Hardware watchpoint 2: count
(gdb) continue
Old value = 0
New value = 1
Hardware watchpoint 2: count
GDB frena, te muestra el valor viejo y el nuevo, y te deja ver dónde cambió. Eso te da la línea exacta del bug en un solo comando. Es la herramienta más precisa del capítulo para diagnosticar corrupción de datos.
Hay tres variantes:
| Comando | Se dispara cuando… | Uso típico |
|---|---|---|
watch expr |
El valor de expr cambia |
"¡alguien me cambió el contador!" |
rwatch expr |
Alguien lee expr |
"¡alguien está leyendo esto y no debería!" |
awatch expr |
Alguien lee o escribe expr |
"¡alguien ni lo tocaría!" |
Y una variante técnica importante:
(gdb) watch -l count
El -l (de location) hace que el watchpoint vigile la dirección, no el valor. Importa
cuando la variable está optimizada (GDB la tiene en un registro y no puede vigilarla) o cuando
el scope cambió (la variable fue Optimizada out y ahora hay otra con el mismo nombre).
Para administrarlos:
(gdb) info watchpoints
El límite real de los watchpoints. Los watchpoints usan hardware del procesador, y la CPU tiene un límite (típicamente 4). Si pedís más, GDB avisa: "Hardware watchpoint 4: - expression". Y hay otro detalle: el hardware no existe en todas las arquitecturas con la misma granularidad. En una máquina de 64 bits, un watchpoint de 4 bytes funciona perfecto.
4.6 La pila de llamadas: quién llamó a quién
Cada vez que un programa llama a una función, se apila un marco (frame) en la pila. La pila de llamadas es la historia de cómo llegaste hasta donde estás.
El comando es bt (backtrace):
(gdb) bt
#0 sumar (x=5) at math.c:5
#1 0x0000000000001129 in main () at demo.c:12
#2 0x0000000000001196 in __libc_start_main () at ../csu/libc-start.c:308
#3 0x00000000000011a3 in _start () at ../csu/crt1.c:101
Leélo así:
#0es el frame actual: estás adentro desumar, conx = 5.#1es quien la llamó:main, en la línea 12.#2y#3son el arranque del proceso (la biblioteca de C y el punto de entrada).
El formato es siempre #N función (args) en archivo:línea. Ese archivo:línea es oro: te
dice exactamente en qué línea de qué archivo hubo la llamada.
Moverse entre los frames
(gdb) frame 1 # move al frame #1
(gdb) f 1 # abreviado
(gdb) up # un frame hacia arriba (hacia quien llamó)
(gdb) down # un frame hacia abajo (hacia el más interno)
Y dentro de un frame:
(gdb) info frame # la dirección del frame, el PC guardado, el SP, el FP
(gdb) info args # los argumentos de ESE frame
(gdb) info locals # las variables locales de ESE frame
El poder real. Cuando estás en el frame
#0y mirástotal, ves eltotaldesumar. Cuando hacésupy mirástotal, ves otrototal: el demain. Misma variable en el código, distinta variable en la realidad. Por eso los bugs más sutiles de C son bugs de alcance (capítulo05), y por esoup/downson la herramienta para cazarlos.
La pila crece hacia abajo
dirección ALTA ┌──────────────┐
│ frame de main│ ← main (quien llamó)
├──────────────┤
│ frame de │ ← sumar (donde estás)
│ sumar │
├──────────────┤
dirección BAJA │ datos │ ← variables locales
└──────────────┘
$rsp ────────► (apunta acá)
El puntero de pila ($rsp) baja a medida que el programa llama a funciones: cada llamada
reserva un bloque. Cuando la función returns, el bloque se libera y $rsp vuelve a subir.
Práctica de la Parte 4
Ejercicio 20.8 — Anatomía de un puntero
Conceptos · &, *, direcciones, print, ptype, sizeof, punteros a heap
Contexto · Un puntero tiene dos caras: la dirección que guarda y el valor que hay ahí.
Confundirlas es el error conceptual que después produce un Segfault. Este ejercicio te hace
ver las dos, explícitamente.
Consigna · Escribí un programa punteros.c que:
- declare una variable local
int numero = 42; - declare un puntero
int *p = № - haga un
mallocde unint, le escriba un valor, y lo recorra con un puntero.
Compilá con -g3 -O0 y, con un breakpoint en el main:
- Imprimí
&numero,numero,py*p. - Verificá que
py&numeroson el mismo número. - Imprimí el puntero del
malloc, su contenido (*ptr) y elsizeofde ambas cosas. - Mirá la diferencia entre el puntero y la variable:
print pyprint *pdan cosas distintas. Anotá cuál es cuál. - Liberá la memoria y comprobá que el puntero sigue apuntando a algo (¿por qué?).
Entrada · Ninguna.
Salida · Un volcado anotado de cada print, con qué representa cada valor.
Escenario de prueba
(gdb) print &numero
$1 = (int *) 0x7fffffffe320
(gdb) print numero
$2 = 42
(gdb) print p
$3 = (int *) 0x7fffffffe320
(gdb) print *p
$4 = 42
Identificadores sugeridos · punteros.c, numero, p, ptr, valor
Restricciones/pistas
- El
mallocdevuelvevoid *: en C hay que convertirlo (capítulo07). GDB puede mostrarlo como(int *) 0x.... - El puntero liberado no se vuelve
NULL:freeno lo toca. Volver a leerlo es undefined behavior (capítulo09) y con suerte vas a ver el valor viejo. - Usá
p/xpara ver las direcciones en hexadecimal, que es más legible.
Criterios de aceptación
- Se imprimieron las cuatro formas (
&numero,numero,p,*p) y se compararon. - Se comprobó que
py&numerocoinciden. - El volcado distingue explícitamente "dirección" de "valor".
- El caso del puntero liberado está explicado con el concepto de dangling pointer.
- La memoria del
mallocsí se liberó (confree).
Ejercicio 20.9 — El anzuelo: un breakpoint condicional
Conceptos · break ... if, condition, ignore, commands, silent, continue
Contexto · Un programa recorre una lista de 10.000 elementos. El problema aparece solo con uno de ellos. Frenar 10.000 veces no es depurar: es sufrir.
Consigna · Escribí un programa buscar.c que reciba por argv un número objetivo y
recorra un arreglo de 10.000 enteros generado con una fórmula determinista (por ejemplo
valor = (i * 7919) % 10000), informando el índice donde lo encontró.
- Corré el programa sin depurador y anotá el índice que devuelve.
- En GDB, poné un breakpoint en la línea donde se informa el índice y usá
printpara ver el valor en cada iteración. - Ahora reemplazá ese breakpoint por uno condicional que solo se dispare cuando el valor actual sea el que buscás.
- Repetí con
ignorepara saltar directo a la iteración que te interesa. - Sumá
commands+silent+continuepara que el programa imprima solo el dato que te importa y siga hasta el final.
Entrada · Un número objetivo (por argv, capítulo 13).
Salida · El índice donde se encontró el valor.
Escenario de prueba
$ ./buscar 4321
encontrado en el indice 8123
(gdb) break buscar.c:18 if valor == 4321
Breakpoint 1 at 0x1150: file buscar.c, line 18.
(gdb) run 4321
...
Breakpoint 1, main () at buscar.c:18
18 printf("encontrado en el indice %d\n", i);
Identificadores sugeridos · buscar.c, valor, objetivo, indice, total
Restricciones/pistas
- La condición se evalúa en el contexto del breakpoint: usá los nombres de variables que existen ahí, no las de otra parte del programa.
- Si la condición nunca se cumple, GDB simplemente nunca para. Eso también es información.
ignorecuenta disparos, no iteraciones: si el breakpoint está en una línea que se ejecuta dos veces por iteración, el número es el doble.
Criterios de aceptación
- El índice que devuelve el programa sin depurador es el mismo que el que encontraste con GDB.
- El breakpoint condicional se dispara una sola vez.
- La variante con
ignoretambién llega al mismo lugar. - La variante con
commands+silent+continueimprime el dato y el programa termina solo. - La diferencia entre "parar siempre" y "parar solo cuando importa" está explicada.
Ejercicio 20.10 — El centinela: un watchpoint
Conceptos · watch, rwatch, awatch, watch -l, info watchpoints, print
Contexto · Una variable que debería tener siempre el mismo valor de pronto tiene otro. Nadie
sabe quién la cambió. Con print no lo vas a encontrar: tendrías que adivinar dónde mirar. Con
un watchpoint, GDB te lo dice solo.
Consigna · Escribí un programa centinela.c que:
- tenga una variable global
int saldo = 100; - tenga cuatro funciones que la toquen: dos la modifican de forma correcta (sumando y restando valores válidos) y dos la modifican mal (asignando un valor que no corresponde).
- un
mainque las llame en un orden dado.
Con un breakpoint en el main, hacé:
watch saldoy seguí la ejecución hasta que se dispare.- Anotá el valor viejo y el valor nuevo, y en qué línea se dispara.
- Repetí hasta identificar cuál de las cuatro funciones rompe el saldo.
- Con
awatchen lugar dewatch, observá también las lecturas. - Con
info watchpointslistá los que definiste.
Entrada · Ninguna. Salida · La identificación de la función culpable, con la evidencia del watchpoint.
Escenario de prueba
(gdb) watch saldo
Hardware watchpoint 2: saldo
(gdb) continue
Old value = 100
New value = 250
Hardware watchpoint 2: saldo
0x0000000000001152 in aplicar_bonus () at centinela.c:24
Identificadores sugeridos · centinela.c, saldo, aplicar_bonus, aplicar_descuento,
cerrar_caja
Restricciones/pistas
- El watchpoint necesita poder evaluar la expresión. Si la variable está en un registro
(optimizada), GDB avisa: "watchpoint on a register". Compilá con
-O0y evitás el problema. watchsobre una constante global no tiene sentido: nunca cambia.- El hardware tiene un límite de watchpoints (típicamente 4). Si pedís más, GDB lo dice.
Criterios de aceptación
- El watchpoint se dispara y muestra valor viejo y valor nuevo.
- La línea exacta donde el valor cambia está identificada.
- La función culpable se determinó por evidencia (no por lectura del fuente).
awatchmostró también las lecturas, y la diferencia conwatchestá explicada.- El programa tiene al menos un dato que queda inconsistente, y eso se detecta con el watchpoint.
Ejercicio 20.11 — La escalera: recorrer la pila de llamadas
Conceptos · bt, frame, up, down, info frame, info args, info locals, finish
Contexto · Cuando el bug está "en un nivel más abajo", la pregunta es siempre la misma: ¿quién llamó a esta función, y con qué datos? La pila de llamadas es el historial de eso.
Consigna · Escribí un programa escalera.c con cuatro funciones anidadas: nivel1 llama
a nivel2, que llama a nivel3, que llama a nivel4. Cada una recibe un int y lo transforma
de una forma distinta, y cada una tiene una variable local con un nombre repetido (por
ejemplo, todas se llaman valor).
Con un breakpoint en nivel4:
bt— dibujá la escalera completa.print valor— ¿de quévalores este?up→print valor→up→print valor→ repetí hastamain.- Anotá el valor de
valoren cada nivel. ¿Son el mismo? ¿Por qué no? info argseinfo localsen dos niveles distintos.- Usá
finishpara volver al frame de arriba ydownpara bajar otra vez. info frameen el frame actual: ¿qué datos da?
Entrada · Ninguna.
Salida · La escalera anotada, con el valor de valor en cada escalón.
Escenario de prueba
(gdb) bt
#0 nivel4 (valor=7) at escalera.c:20
#1 nivel3 (valor=14) at escalera.c:14
#2 nivel2 (valor=21) at escalera.c:9
#3 nivel1 (valor=28) at escalera.c:5
#4 main () at escalera.c:30
Identificadores sugeridos · escalera.c, nivel1, nivel2, nivel3, nivel4, valor
Restricciones/pistas
- El mismo nombre de variable local en distintos frames son variables distintas: cada frame tiene su propia copia en la pila. Es el punto central del ejercicio.
upydowncambian el frame actual: a partir de ahí,print,info localsyinfo argsmiran a ese frame.finishejecuta el resto de la función: el programa avanza hasta quenivel4retorna.
Criterios de aceptación
- La escalera completa (
bt) está dibujada con los argumentos de cada nivel. - El valor de
valoren cada escalón está anotado y se ve que difieren. - Se explicó por qué difieren (cada frame tiene su propia copia).
info argseinfo localsse usaron en dos frames distintos.up,downyfinishse usaron y su efecto se documentó.
PARTE 5 — Diagnóstico de errores críticos de memoria
Acá empieza la parte que justifica la existencia de un depurador. Los bugs de lógica (una
condición mal escrita, un > en vez de un >=) se ven leyendo el código. Los bugs de memoria
—un puntero inválido, un arreglo desbordado, una fuga— no se ven leyendo el código: se ven
mirando la memoria mientras el programa corre.
5.1 Segmentation Fault: qué es realmente
Cuando el programa se cuelga con Segmentation fault, el mensaje que ves es:
Segmentation fault (core dumped)
Traducción: el kernel recibió una señal 11 (SIGSEGV) porque el programa intentó acceder a una
dirección de memoria que no le pertenece o que no existe.
Pensalo como el portero de un edificio. El proceso tiene un "plano" de qué direcciones de memoria puede tocar (lo que el kernel le asignó). Cuando el programa pide una dirección que está fuera de ese plano, el kernel le manda la señal y lo echa. No es un bug de la compilación ni del sistema: es el sistema protegiendo su memoria.
Las causas típicas en C son casi siempre cuatro:
| Causa | Qué pasó | Cuándo la encontrás |
|---|---|---|
| Puntero nulo | Desreferencias un puntero que es NULL |
La más común de largo |
| Fuera de límites | Escribís más allá del final de un arreglo o de un buffer | Muy común con índices calculados |
| Use after free | Usás memoria que ya devolviste con free |
Cuando el free está lejos del uso |
| Desbordamiento de pila | Recursión infinita: la pila crece hasta el límite | Con funciones recursivas |
El mensaje NO te dice la causa. "Segmentation fault" es el síntoma. Te dice dónde y qué, no por qué. Todo el trabajo de esta parte es pasar del síntoma a la causa.
Y hay un cuarto comportamiento que parece un Segfault pero no lo es: cuando escribís sobre un
dato de solo lectura (como un literal de cadena, que vive en .rodata), el kernel te manda
SIGSEGV también. El mensaje de GDB suele ser distinto:
Program received signal SIGSEGV, Segmentation fault.
0x0000000000001150 in main () at demo.c:8
La diferencia clave: un desbordamiento de un arreglo con malloc no siempre revienta. Si
escribís 4 bytes de más en un bloque de 100, el programa sigue vivo y corrupto. El Segfault es
solo el caso donde el daño es tan grande que toca una página que no te pertenece. Ahí está la
trampa: el programa que "no se rompe" puede estar peor que el que se rompe.
5.2 Los core dumps: la caja negra
Cuando un proceso muere por una señal fatal, el kernel puede guardar una fotografía de su memoria: el core dump. Es el equivalente en software de la caja negra del avión: el programa ya no está, pero su estado final quedó guardado en un archivo.
Es la herramienta clave para el diagnóstico post-mortem: no hace falta reproducir el crash en cámara lenta; se analiza la foto.
Habilitarlos
En Linux, el límite de tamaño del core se controla con ulimit:
$ ulimit -c
0 ← 0 = DESHABILITADO (es el valor por defecto en muchos sistemas)
$ ulimit -c unlimited ← habilitar, sin límite de tamaño
$ ulimit -c
unlimited
Ese ulimit aplica a los procesos que lances desde esa terminal. Si lo ponés y después abrís
otra terminal, no hereda el cambio.
Ojo con esto. El mensaje
Segmentation fault (core dumped)dice "core dumped" solo si el core se generó de verdad. Si tuulimit -cestá en 0, el programa se cuelga igual pero no queda archivo. Es la confusión más común: "dice core dumped pero no encuentro el archivo".
Dónde queda el archivo
Eso lo decide el kernel, y lo podés ver:
$ cat /proc/sys/kernel/core_pattern
core
| Valor | Significado |
|---|---|
core |
El archivo se llama core y aparece en el directorio actual |
core.%p |
Se llama core.<pid> (útil con varios programas a la vez) |
|/usr/lib/systemd/systemd-coredump ... |
Lo captura systemd-coredump (no hay archivo en tu carpeta) |
|/usr/share/apport/apport ... |
Lo captura Apport (Ubuntu) |
En los sistemas con systemd-coredump o apport, el core se guarda en un lugar del sistema y se
consulta con coredumpctl:
$ coredumpctl list
$ coredumpctl gdb <PID>
Para guardar. En Windows no existe
ulimit -c: el volcado lo genera el Windows Error Reporting. Se activa por registro (HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps) y produce archivos.dmp, que se abren con WinDbg o con el GDB de MSYS2 (con más difficulties). En macOS, los core dumps se activan conulimit -c unlimitedy se llamancore.<pid>. Si estás en Windows y el ejercicio de core dumps no te cierra, no estás haciendo nada mal: es que la plataforma no tiene la misma caja negra.
5.3 Post-mortem: interrogar el cadáver
Una vez que tenés el archivo del core, lo abrés con el programa adentro:
$ gdb ./demo core.core
GDB carga el estado guardado y te deja interrogarlo. Y ahora el programa no corre: ya terminó. Solo podés mirar.
Los tres comandos del diagnóstico post-mortem:
(gdb) bt
#0 0x0000000000001150 in main () at demo.c:12
#1 0x0000000000001196 in __libc_start_main () at ../csu/libc-start.c:308
(gdb) info registers
rax 0x0 0
rbx 0x7fffffffe3a0 0x7fffffffe3a0
rip 0x555555554150 0x555555554150 <main+32>
rsp 0x7fffffffe300 0x7fffffffe300
(gdb) x/i $pc
=> 0x555555554150 <main+32>: mov %eax,(%rbp-0x4)
Leélo así:
btte dice dónde se rompió: en qué función y en qué línea.info registerste dice el estado en ese instante: qué había en los registros.x/i $pcte dice qué instrucción se estaba por ejecutar.
Esa tríada es el triángulo forense del crash. Con esos tres datos, muchas veces ya sabés
qué pasó: "estaba por escribir en (rbp-0x4), o sea, en una variable local, y rax valía 0"
→ es un puntero nulo desreferenciado.
Y si querés el contexto completo:
(gdb) info frame
(gdb) p *ptr
(gdb) thread apply all bt
La ventaja brutal del post-mortem. Con un core, analizás el crash en frío, con calma, sin cronómetro y sin que el programa se te escape. Podés abrir el mismo core cien veces y probar hipótesis distintas. Con un depurador en vivo, cada intento te cuesta una ejecución.
5.4 x: el microscopio de la memoria cruda
El comando x (de examine) es el que te deja mirar la memoria byte a byte, sin que el
programa tenga que imprimirla. Su formato es:
x/NFU dirección
N = cuántos elementos mirar
F = en qué formato mostrarlos
U = de qué tamaño cada uno
Los formatos (F)
| Letra | Formato | Ejemplo |
|---|---|---|
x |
Hexadecimal | 0x00007ffd |
d |
Decimal con signo | 34567 |
u |
Decimal sin signo | 34567 |
o |
Octal | 42017 |
t |
Binario | 100001011111111 |
c |
Caracteres | 68 'D' |
s |
Cadena (hasta el '\0') |
"hola" |
i |
Instrucción | mov %eax,(%rbp-0x4) |
a |
Dirección | 0x7fffffffe320 |
f |
Punto flotante | 3.14 |
Los tamaños (U)
| Letra | Tamaño |
|---|---|
b |
1 byte |
h |
2 bytes (halfword) |
w |
4 bytes (word) |
g |
8 bytes (giant word) |
Ejemplos concretos
(gdb) x/4xb $sp
0x7fffffffe300: 0x01 0x02 0x00 0x00
(gdb) x/8xw $sp
0x7fffffffe300: 0x00000001 0x00000002 0x00000000 0x00000000
0x7fffffffe310: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) x/s nombre
0x7fffffffe340: "Ana"
(gdb) x/i $pc
=> 0x555555554150 <main+32>: mov %eax,(%rbp-0x4)
(gdb) x/16c buffer
0x7fffffffe360: 104 'h' 101 'e' 108 'l' 108 'l' 111 'o' 32 ' ' 119 'w' 111 'o' 114 'r' 108 'l' 100 'd' 33 '!' 10 '\n' 0 '\0'
El último es hermoso: cada byte de un texto, con su carácter al lado. Es la forma de ver exactamente qué hay escrito en un buffer y dónde empieza la basura.
El uso clásico:
xpara auditar un buffer. Si un programa llena unchar buf[32]con un texto de 40 caracteres,x/40cb bufte muestra los 32 primeros bytes (tu texto, ya desbordando) y los 8 siguientes: la basura que pisaste. Es la forma de ver un desbordamiento de buffer, no de inferirlo.
5.5 Los sanitizers: el guardián que te avisa antes del crash
Los core dumps y x son forense: actuán después del desastre. Los sanitizers son
preventivos: instrumentan el programa para que te avise en el momento exacto en que
hacés algo prohibido.
Son parte del compilador: no son un programa externo, son código extra que el compilador mete dentro de tu binario.
$ gcc -g3 -O0 -fsanitize=address -o demo_asan demo.c
| Sanitizer | Bandera | Qué detecta |
|---|---|---|
| AddressSanitizer (ASan) | -fsanitize=address |
Buffer overflows, use-after-free, fugas de memoria, doble free |
| UndefinedBehaviorSanitizer (UBSan) | -fsanitize=undefined |
Overflow con signo, NULL desreferenciado, shifts imposibles, alineación |
| LeakSanitizer (LSan) | -fsanitize=leak |
Fugas de memoria (standalone; ASan ya lo incluye) |
| MemorySanitizer (MSan) | -fsanitize=memory |
Lectura de memoria no inicializada (solo Clang, y con libc instrumentada) |
| ThreadSanitizer (TSan) | -fsanitize=thread |
Carreras de datos entre hilos (Parte 7) |
Qué se ve cuando ASan encuentra algo
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000eff4 at pc 0x0000004012a3 bp 0x7ffd... sp 0x7ffd...
READ of size 4 at 0x60200000eff4 thread T0
#0 0x4012a0 in main /tmp/demo.c:12
#1 0x4013f7 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+2.34)
#2 0x401260 in _start (/lib/x86_64-linux-gnu/libc.so.6+2.34)
0x60200000eff0 is located 0 bytes after 12-byte region [0x60200000eff0,0x60200000effc)
allocated by thread T0 here:
#0 0x7f... in malloc
#1 0x4011ab in main /tmp/demo.c:8
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/demo.c:12 in main
Leélo con cuidado, porque tiene todo:
- Qué pasó:
heap-buffer-overflow(te leíste más allá de un bloque del heap). - Dónde:
/tmp/demo.c:12— el archivo y la línea exacta. - La cadena de llamadas:
#0,#1,#2— quién llamó a quién. - La evidencia forense:
is located 0 bytes after 12-byte region— el bloque era de 12 bytes y leíste justo después del final. - Quién lo reservó:
allocated by thread T0 here: ... main /tmp/demo.c:8.
Eso es más que un bt de GDB. ASan te da el lugar exacto de la escritura culpable.
Y la fuga de memoria
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 40 byte(s) in 1 object(s) allocated from:
#0 0x7f... in malloc
#1 0x4012b0 in main /tmp/demo.c:20
#2 0x4013f7 in __libc_start_main ...
SUMMARY: AddressSanitizer: 40 byte(s) leaked in 1 allocation(s).
Direct leak = se perdió la referencia y no hay puntero que apunte al bloque. Indirect leak =
el bloque perdido era el contenido de otro bloque perdido.
Combinar el sanitizer con GDB
Acá está el truco que más te da: hacer que el sanitizer se detenga en el error, para que puedas depurarlo con GDB como cualquier otro crash.
$ ASAN_OPTIONS=abort_on_error=1:detect_leaks=1 ./demo_asan
Con abort_on_error=1, en vez de terminar limpiamente, el sanitizer lanza un SIGABRT. Y un
SIGABRT se puede atrapar en GDB:
(gdb) run
Continuing.
Program received signal SIGABRT, Aborted.
__asan::ReportGenericError(...) at ...
Ahora tenés el programa pausado en el momento exacto del error de memoria, con todos los comandos de GDB disponibles.
Para guardar. El flujo de trabajo profesional para bugs de memoria es: primero ASan (te dice archivo y línea exactos), después GDB (mirás el estado en ese punto), después lo arreglás. Al revés es perder tiempo.
5.6 Valgrind: el microscopio lentísimo
Antes de los sanitizers existía (y sigue existiendo) Valgrind, que instrumenta el binario desde fuera y detecta muchísimos errores de memoria sin recompilar:
$ valgrind ./demo
==12345== Memcheck, Memerror: ...
==12345== at 0x400A1E: main (demo.c:12)
==12345== Address 0x5... is 0 bytes after a block of size 12 alloc'd
==12345== at 0x4C2A3E: malloc (vg_replace_malloc.c:...)
==12345== at 0x4011AB: main (demo.c:8)
Con --leak-check=full agrega el reporte de fugas:
$ valgrind --leak-check=full --show-leak-kinds=all ./demo
| Ventaja | Desventaja |
|---|---|
| No recompilás (funciona con cualquier binario) | Es lentísimo: entre 20 y 50 veces más lento |
| Detecta leaks, overflows, uso de memoria no inicializada | No funciona con Threads (usa helgrind/DRD para eso) |
Cuándo usar cada uno. Sanitizer: estás en desarrollo, querés velocidad de iteración y el error es de memoria. Valgrind: no podés recompilar (binario de terceros), o querés
memchecksobre algo que no instrumentaste. GDB: siempre, para entender el estado.
Práctica de la Parte 5
Ejercicio 20.12 — La caja negra: análisis post-mortem
Conceptos · core dump, ulimit -c, core_pattern, gdb ./prog core, bt, info registers,
x/i $pc, lectura del triángulo forense
Contexto · Un programa se cuelga con Segmentation fault en producción, una sola vez, y
no lo podés reproducir. El core dump es la única evidencia que te queda.
Consigna · Escribí un programa caja.c que a propósito provoque un Segfault de forma
controlada: un puntero que se hace NULL y después se desreferencia.
- Verificá
ulimit -cy ponelo enunlimited. - Corré el programa y confirmá que se cuelga. Capturá el mensaje exacto.
- Localizá el archivo del core (
ls,cat /proc/sys/kernel/core_pattern). - Abrilo con
gdb ./caja core(ocore.core). - En el post-mortem, reuní el triángulo forense:
bt,info registersyx/i $pc. - Escribí el diagnóstico: ¿en qué línea se rompió? ¿Qué valor tenía el puntero? ¿Cómo lo sabés a partir de la evidencia?
Entrada · Ninguna. Salida · El diagnóstico post-mortem, con las tres piezas de evidencia.
Escenario de prueba
$ ulimit -c unlimited
$ ./caja
Segmentation fault (core dumped)
$ ls
core caja
$ gdb ./caja core
...
Program received signal SIGSEGV, Segmentation fault.
0x0000000000001150 in main () at caja.c:12
12 printf("%d\n", *nulo);
Identificadores sugeridos · caja.c, nulo, core
Restricciones/pistas
- Si tu sistema usa
systemd-coredumpoapport, no vas a ver el archivo en tu carpeta: usácoredumpctl listycoredumpctl gdb. - En el post-mortem,
runno funciona: el programa ya terminó. Solobt,info registers,x/,print. - Para el diagnóstico del puntero nulo:
p nulote muestra el valor, yinfo registerste muestra si el registro que se iba a desreferenciar estaba en cero.
Criterios de aceptación
- El core se generó de verdad y se localizó su archivo (o se usó
coredumpctl). - El triángulo forense está completo:
bt+info registers+x/i $pc. - El diagnóstico identifica la línea y el valor del puntero, citando la evidencia.
- Se explica por qué
runno sirve en post-mortem. - El diagnóstico es un documento, no una captura suelta de pantalla.
Ejercicio 20.13 — El examen de memoria con x
Conceptos · x/NFU, formatos x d u o t c s i a f, tamaños b h w g, x/s, x/i,
$sp, arreglos artificiales
Contexto · A veces la pregunta no es "¿qué vale la variable?" sino "¿qué hay realmente
escrito en esta dirección?". x responde eso, byte a byte.
Consigna · Escribí un programa examen.c que:
- declare un
char buffer[32]; - lo llene con un texto más corto que 32 usando
strcpy; - declare un
int numeros[8]y lo llene con valores conocidos; - declare un
char *texto = "hola mundo";.
Con un breakpoint después de las cargas, usá x para:
- Mostrar los 32 bytes del buffer en hexadecimal (
x/32xb buffer). - Mostrar el buffer como caracteres (
x/32cb buffer) y apuntar dónde está el'\0'. - Mostrar el arreglo de enteros en palabras de 4 bytes (
x/8xw numeros) y verificar que coincide con lo que cargaste. - Mostrar la cadena con
x/s textoy conx/12cb texto. - Mirar los primeros bytes de la pila con
x/8xw $sp. - Desensamblar la instrucción actual con
x/i $pc.
Entrada · Ninguna.
Salida · Cada salida de x con una explicación de qué se ve.
Escenario de prueba
(gdb) x/16xb buffer
0x7fffffffe360: 0x68 0x6f 0x6c 0x61 0x20 0x6d 0x75 0x6e 0x64 0x6f 0x00 0x00 0x00 0x00 0x00 0x00
(gdb) x/8xw numeros
0x7fffffffe340: 0x00000001 0x00000002 0x00000003 0x00000004
0x7fffffffe350: 0x00000005 0x00000006 0x00000007 0x00000008
Identificadores sugeridos · examen.c, buffer, numeros, texto
Restricciones/pistas
- El
'\0'final es un byte con valor 0. En el volcado hexadecimal aparece como0x00. Si el texto tiene 10 caracteres, el0x00está en la posición 11 (índice 10). x/slee hasta el primer'\0': si el buffer no está terminado,x/sse va de largo y te muestra basura (o se cuelga). Eso es información, no un bug del comando.- El tamaño
wson 4 bytes (32 bits) en x86-64;gson 8 bytes.
Criterios de aceptación
- Se usaron al menos tres formatos (
x,d,cos) y dos tamaños (b,w). - La posición del
'\0'está identificada y explicada. - El volcado del arreglo de enteros coincide con los valores cargados.
x/syx/Ncbsobre la misma cadena dan resultados distintos, y la diferencia está explicada.x/i $pcse usó y se interpretada.
Ejercicio 20.14 — ASan: la fuga que no se ve
Conceptos · -fsanitize=address, LeakSanitizer, ASAN_OPTIONS, abort_on_error=1,
combinación sanitizer + GDB, free, ownership de punteros
Contexto · Una fuga de memoria no rompe el programa: el programa funciona perfectamente y simplemente usa más y más memoria hasta que el sistema lo mata. Es el bug más difícil de ver "a ojo" y el más fácil de ver con un sanitizer.
Consigna · Escribí un programa fuga.c que:
- tenga una función
crear_lista(int n)que arma una lista denelementos conmallocy la devuelve; - tenga una función
filtrar(Lista *l)que cree una nueva lista con los elementos pares y la devuelva; - el
mainllame a las dos y no libere la lista original.
Después:
- Compilá sin sanitizer y corré el programa con un
ngrande: observá que no falla. - Recompilá con
-fsanitize=address -g3 -O0y corré el mismo programa. - Leé el reporte de ASan: ¿qué dice? ¿de dónde salió la memoria perdida?
- Recompilá con
ASAN_OPTIONS=abort_on_error=1y metelo en GDB: ¿dónde se detiene? - Arreglá el programa (liberando todo lo que corresponde) y verificá que ASan deja de reportar la fuga.
Entrada · Un n (por argv).
Salida · La memoria usada, y el reporte de ASan antes y después del arreglo.
Escenario de prueba
$ gcc -g3 -O0 -fsanitize=address -o fuga_asan fuga.c
$ ./fuga_asan 100
=================================================================
==1234==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 800 byte(s) in 100 object(s) allocated from:
#0 0x7f... in malloc
#1 0x4012b0 in crear_lista fuga.c:14
#2 0x4013f7 in main fuga.c:31
SUMMARY: AddressSanitizer: 800 byte(s) leaked in 100 allocation(s).
Identificadores sugeridos · fuga.c, crear_lista, filtrar, Lista, n
Restricciones/pistas
- El tamaño del reporte es
n(el número de objetos) por el tamaño de cada uno. Si el reporte dice "800 byte(s) in 100 object(s)", cada objeto ocupa 8 bytes. - Una fuga no es un error de ASan "de buffer": es un reporte distinto (
LeakSanitizer). Si no aparece, revisá que el programa termine (si queda en un bucle infinito, LSan no corre). - La compilación con
-fsanitize=addressya incluye el detector de fugas: no hace falta-fsanitize=leakaparte. - El arreglo sintáctico correcto de "quien libera qué" se llama ownership. Cuando una función
devuelve memoria pedida con
malloc, la documentación tiene que decir quién la libera.
Criterios de aceptación
- El programa sin sanitizer corre "bien" (y eso se documenta como el peligro).
- El reporte de ASan está capturado y se identifica el origen exacto de la fuga (archivo y línea de la asignación).
- El tamaño de la fuga coincide con lo esperado según los cálculos del enunciado.
- El programa con
abort_on_error=1se detiene en GDB y se documentó dónde. - Después de arreglar, ASan no reporta fugas (verificado, no supuesto).
PARTE 6 — Depuración de bajo nivel y ensamblador
Hasta acá, todo fue C: líneas, variables, funciones, estructuras. Ahora bajamos un piso. La máquina no sabe qué es una variable: la máquina mueve bytes entre registros y direcciones de memoria. GDB te deja ver esa capa, y entenderla es lo que separa a quien depura de memoria de quien reza.
Una advertencia honesta antes de empezar. Esta parte es la menos útil del capítulo para el 95 % de los bugs. No la saltees, pero sabé que la respuesta a "mi programa no funciona" casi nunca está en el ensamblador. Está en la lógica, en los punteros o en la memoria. Bajamos al ensamblador cuando la evidencia de los niveles superiores no alcanza.
6.1 Ver el ensamblador
El desensamblado de una función
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000001129 <+0>: push %rbp
0x000000000000112a <+1>: mov %rsp,%rbp
0x000000000000112d <+4>: sub $0x20,%rsp
0x0000000000001131 <+8>: mov %edi,-0x14(%rbp)
0x0000000000001135 <+12>: mov $0x0,-0x18(%rbp)
0x000000000000113c <+19>: mov -0x18(%rbp),%eax
0x000000000000113f <+22>: add $0x1,%eax
0x0000000000001142 <+25>: mov %eax,-0x18(%rbp)
0x0000000000001145 <+28>: mov -0x18(%rbp),%eax
0x0000000000001148 <+31>: pop %rbp
0x0000000000001149 <+32>: ret
=> 0x000000000000114a <+33>: (bad)
Cada línea tiene la dirección y el desplazamiento dentro de la función (<+28>). El
comando disassemble acepta variantes:
(gdb) disassemble /s main # con el código fuente intercalado
(gdb) disassemble 0x1129,0x1140 # un rango de direcciones
(gdb) disassemble /r main # con los bytes en crudo
La variante /s es la más útil: GDB pone el código C al lado del ensamblador, y ahí se ve
el mapeo línea a línea.
Los registros
(gdb) info registers
rax 0x1 0
rbx 0x0 0
rcx 0x7fffffffe320 0x7fffffffe320
rdx 0x0 0
rsi 0x0 0
rdi 0x1 0
rip 0x401136 0x401136 <main+13>
rsp 0x7fffffffe300 0x7fffffffe300
rbp 0x7fffffffe300 0x7fffffffe300
Y se pueden consultar de a uno, como si fueran variables:
(gdb) print $rax
$1 = 1
(gdb) print/x $rip
$2 = 0x401136
(gdb) print $rsp
$3 = (void *) 0x7fffffffe300
Los tres registros que importan.
ripes el puntero de instrucción: la dirección de la instrucción que se va a ejecutar.rspes el puntero de pila: el tope de la pila (crece hacia abajo).rbpes el puntero de marco: la base del frame actual. Con esos tres más el desensamblado, podés reconstruir a mano lo que la máquina está haciendo.
6.2 stepi y nexti: un paso a la vez
Si next y step avanzan una línea de C, stepi y nexti avanzan una instrucción de
máquina.
| Comando | Qué hace |
|---|---|
stepi (si) |
Ejecuta una instrucción y para |
nexti (ni) |
Ejecuta una instrucción, pero salta por encima de las llamadas |
stepi N |
Ejecuta N instrucciones y para |
La diferencia con next/step es la misma de siempre, pero un nivel más abajo: stepi entra
en la función llamada; nexti pasa por encima.
El flujo clásico para mirar una instrucción de cerca:
(gdb) stepi
0x0000000000001131 <main+8>: mov %edi,-0x14(%rbp)
(gdb) x/i $pc
=> 0x0000000000001135 <main+12>: mov $0x0,-0x18(%rbp)
(gdb) stepi
0x0000000000001135 <main+12>: mov $0x0,-0x18(%rbp)
Fijate en el patrón: stepi te dice en qué terminó la instrucción, y x/i $pc te muestra
cuál es la siguiente. Es la forma de "avanzar una instrucción y mirar qué viene".
El modo más cómodo: la TUI con dos ventanas.
layout srcylayout asmlado a lado. Constepivas viendo la línea de C y la instrucción avanzar al mismo tiempo. Es la forma en que de verdad se aprende a leer ensamblador: no en un manual, sino mirando tu propio código.
6.3 La convención de llamada: el acuerdo entre las funciones
Cuando una función llama a otra, hay un acuerdo sobre quién pone qué dónde. Ese acuerdo se llama convención de llamada (calling convention) y es lo que te permite entender el ensamblador de un vistazo.
En x86-64 con la convención System V (la de Linux y macOS):
| Rol | Registros |
|---|---|
| Argumentos enteros (1º a 6º) | rdi, rsi, rdx, rcx, r8, r9 |
| Valor de retorno | rax |
| Guardados por la función llamada | rbx, rbp, r12–r15, rsp |
| Guardados por quien llama | rax, rcx, rdx, rsi, rdi, r8–r11 |
| Puntero de pila | rsp |
| Puntero de marco | rbp |
| Puntero de instrucción | rip |
Y el esqueleto de una función se ve así:
dirección ALTA
┌───────────────────────────┐ ← direction de return (la guardó el CALL)
├───────────────────────────┤
│ variables locales │ ← %rbp - 4, %rbp - 8, ...
├───────────────────────────┤
│ argumentos (algunos) │ ← algunos args vienen en registros
├───────────────────────────┤
│ registros guardados │ ← %rbx, %rbp (el viejo)
├───────────────────────────┤
│ dirección de return │ ← la dirección de la instrucción siguiente
└───────────────────────────┘ ← %rsp
dirección BAJA
Ojo con esto. En ARM64 (AArch64), que es la arquitectura de tu teléfono y de la mayoría de las Raspberry Pi, los nombres son distintos: los argumentos van en
x0–x7, el retorno enx0, ysp/lrcumplen el papel dersp/dirección de retorno. El concepto es el mismo, los nombres cambian. GDB los abstracts, así que los comandos (print $x0) funcionan igual.
El corollary importante para depurar: si mirás rdi justo después de un call, y esperabas un
int, no lo vas a encontrar: los enteros de 32 bits viajan en la parte baja de rdi. En
GDB, print $rdi puede mostrar 0x00000000ffffffff y el valor real es 0xffffffff. Por eso
conviene usar p/x $rdi & 0xffffffff.
6.4 Parchear la memoria en caliente
Acá GDB deja de mirar y empieza a modificar. Y lo hace sin tocar el código fuente.
Cambiar una variable
(gdb) set var contador = 42
(gdb) print contador
$1 = 42
También funciona con miembros de estructuras:
(gdb) set var ficha.peso = 99.5
Cambiar un registro
(gdb) set $rax = 1
(gdb) set $rdi = 0
Escribir en una dirección de memoria
Acá está la forma más potente: se le dice "en esta dirección, escribí estos bytes".
(gdb) set {char}0x7fffffffe320 = 65
(gdb) set {int}0x7fffffffe320 = 12345
(gdb) set {int[4]}0x7fffffffe320 = {1, 2, 3, 4}
El {tipo} le dice a GDB cómo interpretar los bytes que escribís. Sin el tipo, GDB no sabe
qué poner.
Y para parchear código (reemplazar una instrucción por otra):
(gdb) x/3i $pc
=> 0x401136 <main+13>: mov %eax,(%rbp-0x4)
0x401139 <main+16>: add $0x1,%eax
0x40113c <main+19>: nop
(gdb) set {char}0x401139 = 0x90
0x90 es el byte de la instrucción nop (no operation) en x86: "no hagas nada". Acabas de
neutralizar una instrucción en caliente.
Para guardar: esto NO es una técnica de corrección. Parchear memoria es un instrumento de laboratorio: sirve para probar una hipótesis ("¿y si este valor fuera 0 en vez de -1?"). El programa sigue siendo el que es; lo único que cambia es lo que cree que vale. Jamás arregles un bug parcheando memoria en GDB: el bug sigue en el código, y el próximo
runlo vuelve a traer.
6.5 Cuándo no bajar al ensamblador
Esta es la parte más importante de la Parte 6, y es una regla, no una técnica.
| El bug es… | ¿Bajás al ensamblador? |
|---|---|
| Una condición mal escrita | No |
| Un puntero mal desreferenciado | No |
| Un arreglo que se desborda | No (para eso están ASan y x) |
| Una fuga de memoria | No (para eso están LSan y Valgrind) |
| Un error de convención de llamada entre TU código y una biblioteca | Sí |
| Un comportamiento que no coincide con lo que dice el manual del hardware | Sí |
| Un bug dentro del compilador o del runtime | Sí |
La regla de oro: siempre empezá por el nivel más alto que pueda explicar el bug. El ensamblador es el último recurso, no el primero.
Práctica de la Parte 6
Ejercicio 20.15 — El mismo bug en dos niveles
Conceptos · disassemble, x/i $pc, info registers, stepi, nexti, convención de
llamada, mapeo C ↔ ensamblador
Contexto · Ver el ensamblador de una función línea por línea es como ver el código C con un microscopio: entendés cosas que en el fuente no se ven (dónde vive cada variable, qué se copia y qué no).
Consigna · Escribí un programa dosniveles.c con una función que:
- reciba dos
intpor parámetro; - devuelva la suma;
- tenga dos variables locales.
Con -g3 -O0:
- Poné un breakpoint dentro de la función y hacé
disassemble /s <función>. - Identificá en el ensamblador: el
push %rbp, elsubque reserva espacio, losmovque copian los argumentos a las variables locales, y elret. - Anotá en qué offset del marco (
%rbp - N) vive cada variable local, y verificalo conp &variableyp/x $rbp. - Usá
stepipara avanzar instrucción por instrucción, y en cada pasox/i $pcpara ver la siguiente. - Con
info registers, fijate cómo van cambiandordi,rsiyrax.
Entrada · Ninguna. Salida · El ensamblador anotado, con el equivalente en C de cada instrucción importante.
Escenario de prueba
(gdb) disassemble /s sumar
0x0000000000001150 <+0>: push %rbp
0x0000000000001151 <+1>: mov %rsp,%rbp
0x0000000000001154 <+4>: sub $0x10,%rsp
0x0000000000001157 <+7>: mov %edi,-0x4(%rbp) ; a
0x000000000000115a <+10>: mov %esi,-0x8(%rbp) ; b
0x000000000000115d <+13>: mov -0x4(%rbp),%eax
0x0000000000001160 <+16>: add -0x8(%rbp),%eax
0x0000000000001163 <+19>: mov %eax,-0xc(%rbp) ; resultado
0x0000000000001166 <+22>: mov -0xc(%rbp),%eax
0x0000000000001169 <+25>: leave
0x000000000000116a <+26>: ret
Identificadores sugeridos · dosniveles.c, sumar, a, b, resultado
Restricciones/pistas
- El
sub $0x10,%rspreserva 16 bytes de pila: ese es el espacio de tus variables locales. Los offsets-0x4,-0x8,-0xcson direcciones negativas desde%rbp. - Con
p/x $rbpobtenés la dirección del marco;p/x &ate tiene que dar$rbp - 0x4(restando 4). - En ARM64 el ensamblador es otro (no hay
%rbp, hayx29/x30). Usá el desensamblado de tu máquina.
Criterios de aceptación
- El desensamblado con
/sestá capturado. - Los roles de
push %rbp,sub %rsp, losmovde argumentos yretestán identificados. - El offset de cada variable local está verificado con
p/x &var, no con suposición. stepise usó al menos cinco veces y el estado de los registros se anotó.- Hay un mapa "instrucción ↔ línea de C" para la función.
Ejercicio 20.16 — Parcheo en caliente: probar una hipótesis
Conceptos · set var, set $registro, set {tipo}dirección, x/i $pc, nop, hipótesis
Contexto · El parcheo en caliente no sirve para arreglar bugs: sirve para probar hipótesis. "¿Y si el problema es que este valor llega en cero?" Lo cambiás en vivo y mirás qué pasa.
Consigna · Escribí un programa hipotesis.c que tenga una función que divida dos números
tomados de la entrada y devuelva el cociente, sin validar el divisor.
- Corré el programa con un divisor en cero y anotá el síntoma.
- En GDB, poné un breakpoint después de la división y usá
set varpara forzar el divisor a un valor válido. Anotá qué cambia. - Ahora hacé lo mismo pero con
set {int}<direccion> = <valor>: escribí directamente en la memoria donde vive la variable. - Con
x/i $pcyset {char}<dir> = 0x90, neutralizá una instrucción de la función y observá qué deja de hacer el programa. - Escribí, para cada caso, la hipótesis que probaste y la conclusión.
Entrada · Dos números (por argv).
Salida · El cociente, o el síntoma del crash.
Escenario de prueba
(gdb) set var divisor = 5
(gdb) print divisor
$1 = 5
(gdb) p/x &divisor
$2 = 0x7fffffffe2f4
(gdb) set {int}0x7fffffffe2f4 = 7
(gdb) print divisor
$3 = 7
Identificadores sugeridos · hipotesis.c, dividir, dividendo, divisor, cociente
Restricciones/pistas
set varnecesita el nombre;set {int}<direccion>necesita la dirección. Por esop/x &variablees el paso previo obligatorio del segundo.- Cambiar un argumento no cambia el valor original de la variable del
main:set varactúa sobre el frame actual. Para cambiar el valor en el frame de arriba, hacéupprimero. - Neutralizar una instrucción cambia la longitud del código: si la reemplazás por un
nopde un byte cuando la instrucción original era de tres bytes, dejaste basura. Reemplazá por una instrucción del mismo tamaño.
Criterios de aceptación
- El síntoma original está documentado.
- El caso
set varcambió el comportamiento y el cambio está anotado. - El caso
set {int}<dir>se hizo usando la dirección real de la variable, mostrada conp/x &var. - La neutralización con
0x90usó una instrucción del mismo tamaño que la original, y se explica por qué. - Cada experimento tiene hipótesis, acción y conclusión escritos.
PARTE 7 — Automatización, multihilo y técnicas avanzadas
Hasta acá, GDB fue un herramienta interactiva: vos escribís, GDB responde. Ahora vamos a las tres cosas que vuelven a GDB una herramienta de ingeniería: automatizarlo, depurar programas con varios hilos, y acoplarte a un programa que ya está corriendo.
7.1 .gdbinit: tu configuración personal
GDB lee un archivo llamado .gdbinit al arrancar y ejecuta lo que haya ahí. Es tu archivo de
configuración: aliases, ajustes, layouts guardados.
| Sistema | Ubicación |
|---|---|
| Linux / macOS | ~/.gdbinit (o $HOME/.gdbinit) |
| Windows (MSYS2) | ~/.gdbinit en tu home de MSYS2 |
Un .gdbinit típico:
# --- Presentación ---
set history save on
set history filename ~/.gdb_history
set history size 1000
set print pretty on
set print elements 0
set confirm off
set pagination off
set step-mode on
set disassembly-flavor intel
# --- Alias útiles ---
define pf
print $arg0
end
define pv
print *($arg0)
end
define btfull
bt full
end
define reg
info registers
end
Fijate en tres cosas:
define <nombre> ... enddefine un comando propio.pf totales lo mismo queprint total.set step-mode onhace quenextystepse comporten como en algunos depuradores comerciales.set confirm offhace que GDB no pregunte antes de borrar todos los breakpoints o de salir. Imprescindible en scripts, incómodo a mano.
Para saltarlo:
$ gdb -nx ./demo # ignora el .gdbinit
$ gdb -nx -q ./demo # además, no muestra el banner
Ojo con esto. Un
.gdbinitmal escrito puede romper GDB al arrancar (un error de sintaxis te puede dejar sin prompt). Guardá siempre una copia:cp ~/.gdbinit ~/.gdbinit.bak.
7.2 Automatizar: GDB sin interacción
GDB se puede usar como una herramienta por lotes: le das una lista de comandos y él trabaja solo. Es la base de los "scripts de regresión" que usan muchas empresas.
$ gdb -batch -x script.gdb ./demo
| Bandera | Qué hace |
|---|---|
-batch |
Ejecuta todo y sale al terminar (sin preguntar) |
-x archivo |
Ejecuta los comandos del archivo |
-ex "comando" |
Ejecuta un comando (se puede repetir) |
--args prog a b c |
Programa y sus argumentos |
-nx |
Ignora el .gdbinit |
Un script típico:
# script.gdb
set confirm off
set pagination off
break main
run
printf "--- dentro de main ---\n"
info locals
bt
info registers rip
continue
quit
Y el mismo script en una línea, sin archivo:
$ gdb -batch -ex "break main" -ex run -ex "info locals" -ex "bt" ./demo
El truco de
logging. Si además querés un registro de todo lo que pasó:(gdb) set logging file informe.txt (gdb) set logging onEl archivo
informe.txtva a tener la salida del programa, los comandos y los resultados. Eso es evidencia entregable.
Y si querés automatizar de verdad (loops, condiciones, funciones), GDB tiene un intérprete de Python embebido:
(gdb) python print("hola desde Python")
hola desde Python
(gdb) python print(gdb.parse_and_eval("total"))
$1 = 42
Con python podés escribir bucles, recorrer estructuras y generar comandos. Es el nivel
avanzado de la automatización de GDB.
7.3 Depurar programas con hilos
Un programa con pthread tiene varios hilos de ejecución que comparten la memoria. GDB los
gestiona, pero hay que entender cómo.
Ver los hilos
(gdb) info threads
Id Target Id Frame
* 1 Thread 0x7ffff7fc4e40 (LWP 12345) sumar () at math.c:5
2 Thread 0x7ffff7fc4e40 (LWP 12346) main () at demo.c:14
El * marca el hilo actual. Cada hilo tiene su propio frame (su propia pila), pero
comparte las variables globales y el heap.
Cambiar de hilo
(gdb) thread 2
[Switching to thread 2 (LWP 12346)]
(gdb) bt
#0 main () at demo.c:14
Y para ver todos los hilos de un saque:
(gdb) thread apply all bt
Thread 1 (LWP 12345):
#0 sumar (x=5) at math.c:5
#1 0x401136 in main () at demo.c:14
Thread 2 (LWP 12346):
#0 main () at demo.c:14
Ese thread apply all bt es el comando que resuelve el 90 % de los bugs de concurrencia: te
muestra el estado de cada hilo en el mismo instante.
Congelar los otros hilos
Cuando depurás un programa con hilos, el problema es que los otros hilos siguen corriendo mientras vos mirás. Para evitarlo:
(gdb) set scheduler-locking on
| Valor | Qué hace |
|---|---|
off |
Todos los hilos corren (default) |
on |
Solo corre el hilo actual; los demás están congelados |
step |
Congela los demás mientras ejecutás una instrucción, y los deja correr en un continue |
Truco de laboratorio. Depurá hilos con
set scheduler-locking onythread apply all bt. Es la combinación que convierte el "mi programa con hilos hace cosas raras" en un diagnóstico concreto.
Y para los programas que usan hilos POSIX con funciones bloqueantes, existe el modo non-stop, que deja que los hilos sigan corriendo mientras vos estás parado:
(gdb) set non-stop on
7.4 Depurar procesos hijo (fork)
Cuando un programa llama a fork(), se crean dos procesos ("inferiores" en la terminología de
GDB). Por defecto, GDB sigue al padre y se desconecta del hijo.
Para verlos:
(gdb) info inferiors
Num Description
* 1 program ./demo
Y para cambiar el comportamiento:
| Comando | Qué hace |
|---|---|
set follow-fork-mode parent |
Sigue al padre (default) |
set follow-fork-mode child |
Sigue al hijo |
set follow-fork-mode child + set detach-on-fork off |
Sigue al hijo pero conserva el padre |
set schedule-multiple on |
Ejecución concurrente de todos los inferiores |
El combo clásico para depurar un fork():
(gdb) set follow-fork-mode child
(gdb) set detach-on-fork on
(gdb) run
Con eso, cuando el programa hace fork(), GDB suelta al padre y se queda pegado al hijo, que
es el que te interesa.
7.5 Acoplarse a un proceso que ya está corriendo (attach)
Esta es la capacidad más útil en el mundo real: el programa ya está corriendo (en un servidor, en un entorno de pruebas, atascado en producción) y vos te le pegás.
$ gdb -p 12345
o, desde adentro de GDB:
(gdb) attach 12345
Al conectarte, GDB frena el proceso. Es como ponerle un freno de mano: el programa queda congelado y vos lo podés inspeccionar.
Después de inspeccionar, te soltás sin matarlo:
(gdb) detach
Cuadro de mando del
attach.
Acción Comando Efecto Conectarse gdb -p PID/attach PIDEl proceso se frena Ver el estado info threads,bt,info proc mappingsFoto del proceso Soltar detachEl proceso sigue corriendo Salir quitGDB se va (el proceso puede quedar vivo o morir: según cómo salgas) Regla de oro:
detach, nuncakill. Si te colgaste con unattachy cerrás GDB conquitrespondiendo "sí" a la pregunta de matar el proceso, mataste el programa que estabas investigando.
Los permisos
En Linux, engancharse a un proceso ajeno requiere permiso de ptrace. El sistema lo controla con
/proc/sys/kernel/yama/ptrace_scope:
| Valor | Significado |
|---|---|
0 |
Cualquiera puede engancharse (configuración de laboratorio) |
1 |
Solo un proceso que sea ancestro puede engancharse |
2 |
Solo con privilegios de administrador |
Si attach te dice "operation not permitted", no es un problema de GDB: es del sistema. Las
salidas son sudo gdb -p PID, o poner ptrace_scope en 0 (solo en una máquina de pruebas).
Dos trampas reales
La primera: el shell en el medio. gdb -p PID a veces se engancha al shell que lanzó el
programa en vez de al programa. Se arregla con:
(gdb) set startup-with-shell off
La segunda: la aleatorización de direcciones. Linux aleatoriza la base de los binarios para que un exploits no sirva para otro proceso (ASLR). Cuando te colgás a un proceso ya corriendo, GDB necesita esa aleatorización desactivada para que sus datos coincidan:
(gdb) set disable-randomization on
Esta segunda trampa es la más frustrante, porque el síntoma es "
pme dice que la variable vale 0 y claramente vale 42". Si te pasa, agregá esa línea antes delattach.
7.6 Depuración remota
Para depurar un programa que corre en otra máquina (un dispositivo embebido, un servidor, un Android), GDB se separa en dos partes: el cliente (tu máquina, con la pantalla) y el servidor (la máquina del programa).
El servidor es gdbserver:
# En la máquina remota:
$ gdbserver :2345 ./demo
Y en tu máquina:
$ gdb ./demo
(gdb) target remote :2345
O en un solo paso:
$ gdb --remote=:2345 ./demo
Dos variantes que vas a necesitar:
| Variante | Qué hace | Cuándo |
|---|---|---|
target remote host:puerto |
El remoto es un proceso en vivo | Tu programa normal, remoto |
target extended-remote host:puerto |
Permite reanudar el proceso remoto | Depuración fancy, GDB scripts |
Y por cable serie (un microcontrolador, un Arduino con GDBStub):
(gdb) target remote /dev/ttyUSB0
Ojo con esto. La conexión remota es una puerta abierta al servidor: cualquiera que conecte puede tomar el control del proceso. En una red real,
gdbserverse usa por SSH (o por túnel), nunca expuesto a la red abierta.
Práctica de la Parte 7
Ejercicio 20.17 — Tu .gdbinit
Conceptos · .gdbinit, define/end, set history, set confirm, alias, set print,
-nx
Contexto · Los mismos tres comandos repetidos 200 veces por día son una pérdida de tiempo. Un
.gdbinit bien armado te los da de un tiro.
Consigna · Creá tu archivo .gdbinit (con ~/.gdbinit como destino) que contenga:
- Ajustes de presentación: historial persistente,
print pretty,print elements 0,pagination off. - Tres comandos propios con
define/end, que sean de tu uso real:- uno que imprima las variables locales con formato (
info locals+pde cada una); - uno que muestre la pila completa con los valores de los argumentos (
bt full); - uno que muestre el estado de la memoria alrededor de un puntero que le pases.
- uno que imprima las variables locales con formato (
- Un alias con
alias(no condefine) para el comando que uses más.
Después probá que:
gdb ./democarga tu configuración;gdb -nx ./demono la carga (y eso está bien, es lo que querés para depurar con settings limpios);- un
.gdbinitcon un error de sintaxis no te deja sin prompt (probalo, y reparalo).
Entrada · Ninguna.
Salida · El contenido final de tu .gdbinit, documentado.
Escenario de prueba
$ gdb -nx ./demo
(gdb) show print pretty
Pretty printing is off.
Identificadores sugeridos · .gdbinit, locals, btfull, around
Restricciones/pistas
aliascrea un alias de un comando existente;definecrea un comando nuevo con varias líneas.- Dentro de un
define,$arg0,$arg1, … son los argumentos que le pasaste. ~/.gdbinitno se lee si GDB se arranca con-nx. Es tu escape hatch para los días que necesitás settings limpios.- En Windows con MSYS2, el home es el de MSYS2 (el que te muestra
echo $HOMEen la terminal de MSYS2), no el de Windows.
Criterios de aceptación
- El
.gdbinittiene los cuatro bloques pedidos (presentación, 3define, 1alias). - Los tres comandos propios se ejecutan sin error y se verificó su salida.
- Se comprobó la diferencia entre
gdb ./demoygdb -nx ./demo. - El archivo tiene comentarios que explican qué hace cada bloque.
- Hay una copia de seguridad (
~/.gdbinit.bak).
Ejercicio 20.18 — Hilos: quién se está trabando
Conceptos · info threads, thread N, thread apply all bt, set scheduler-locking,
deadlock, condición de carrera, TSan
Contexto · Un programa con hilos que "se cuelga" casi nunca está usando el 100 % de la CPU: está esperando. Y esperar, en un programa concurrente, casi siempre significa un deadlock (dos hilos se esperan mutuamente) o una condición de carrera mal resuelta.
Consigna · Escribí un programa hilos.c con dos hilos y una variable compartida:
- Cada hilo incrementa un contador compartido en un bucle de 100.000 veces.
- Cada hilo imprime su progreso cada 10.000 iteraciones.
- El
mainespera a que terminen (conpthread_join) e imprime el total.
Después:
- Corré el programa sin depurador: ¿el total es correcto? ¿ siempre?
- En GDB, poné un breakpoint en la línea del incremento y usá
thread apply all btpara ver los dos hilos. - Usá
set scheduler-locking ony observá que solo avanza el hilo actual. - Cambiá a
thread 2y verificá que estás en otro frame, con otras variables locales. - Variante: modificá el programa para que los dos hilos se esperen mutuamente (deadlock) y
diagnosticá dónde está cada uno con
thread apply all bt.
Entrada · Ninguna. Salida · El diagnóstico del deadlock (qué espera cada hilo y por qué nunca se resuelve).
Escenario de prueba
(gdb) thread apply all bt
Thread 1 (LWP 12345):
#0 nanosleep () at ../sysdeps/nptl/nptl/pthread_cancel.c:36
#1 esperar_al_otro () at hilos.c:22
Thread 2 (LWP 12346):
#0 nanosleep () at ../sysdeps/nptl/nptl/pthread_cancel.c:36
#1 esperar_al_otro () at hilos.c:24
Identificadores sugeridos · hilos.c, hilo1, hilo2, contador, compartido,
esperar_al_otro
Restricciones/pistas
- La carrera de datos (lectura y escritura simultáneas sin sincronización) no siempre se
manifiesta: por eso el total "a veces" está mal. Para verla de forma determinista, compilá con
-fsanitize=thread(ThreadSanitizer), que sí la detecta siempre. thread apply all btcongela el estado de todos los hilos en el mismo instante: por eso sirve para ver un deadlock.set scheduler-locking onhace que los otros hilos no avancen mientras vos hacésnext.
Criterios de aceptación
info threadsdistingue el hilo actual (con*) de los demás.thread apply all btmuestra el estado de los dos hilos.set scheduler-locking onse comprobó (los otros hilos no avanzan).- El deadlock se diagnosticó identificando qué espera cada hilo.
- El rol de ThreadSanitizer en la detección de carreras está explicado.
Ejercicio 20.19 — Acoplarte a un proceso vivo
Conceptos · attach, gdb -p, detach, ptrace_scope, set startup-with-shell off,
set disable-randomization on, info proc mappings
Contexto · El programa ya está corriendo y no lo podés modificar: no tenés los fuentes a mano
o el binario es de otro servidor. attach te deja entrar y mirar.
Consigna · Escribí un programa lento.c que:
- entre en un bucle que duerme un poco y va incrementando un contador;
- imprima el contador cada medio segundo, indefinidamente.
Y hacé esto:
- Lanzalo en segundo plano (
./lento &) y anotá su PID. - Conectate con
gdb -p <PID>. - Verificá que el proceso se frenó (mirá con
psque su estado cambió). - Inspeccioná:
bt,info threads,info locals,info proc mappings. - Desconectate con
detachy verificá que el proceso sigue vivo. - Conectate de nuevo y probá
set {int}<direccion> = <valor>para cambiar el contador en vivo. - Salí sin matar el proceso.
Entrada · Ninguna. Salida · La evidencia de cada paso, y la confirmación de que el proceso sobrevivió.
Escenario de prueba
$ ./lento &
[1] 12345
$ gdb -p 12345
...
(gdb) info locals
contador = 37
(gdb) detach
Detaching from program: 12345, Remote target
[Inferior 1 (process 12345) detached]
$ ps -p 12345
PID TTY STAT TIME COMMAND
12345 pts/0 S 0:00.04 ./lento
Identificadores sugeridos · lento.c, contador, pid
Restricciones/pistas
attachfrena el proceso. Si te parece que el servidor "se colgó" justo después de tu `attach", es que lo frenaste vos.detachlo suelta y sigue.quitcon "sí" a la pregunta de matar lo mata.- Si
attachfalla con "operation not permitted", es elptrace_scopedel sistema: usásudo, o pedile a quien administra la máquina que lo ajuste. - La aleatorización de direcciones (ASLR) puede hacer que GDB muestre valores raros:
set disable-randomization on(antes de conectar) lo evita.
Criterios de aceptación
- El proceso se conectó y se verificó que se frenó al conectar.
bt,info threads,info localseinfo proc mappingsse usaron y su salida está capturada.- Tras el
detach, el proceso sigue corriendo (verificado conps). - La modificación de memoria en vivo se verificó viendo el cambio en la salida del programa.
- El proceso no fue matado al final.
Ejercicio 20.20 — Automatización: un script que trabaja solo
Conceptos · -batch, -x, -ex, set confirm off, set logging, define, python,
automatización
Contexto · Si tenés que repetir la misma secuencia de diagnóstico 50 veces (por ejemplo, en 50 binarios de una tanda de pruebas), no la vas a repetir a mano. GDB se automatiza.
Consigna · Escribí un script reporte.gdb que, sobre un programa cualquiera:
- ponga un breakpoint en
main; - ejecute el programa;
- imprima un separador y el backtrace completo;
- imprima las variables locales;
- imprima el valor de los argumentos de
main; - genere un informe con
set loggingen un archivoreporte.txt; - termine solo (sin pedir confirmación).
Y ejecutalo de las dos formas:
$ gdb -batch -x reporte.gdb ./demo
$ gdb -batch -ex "break main" -ex run -ex "bt full" ./demo
Entrada · Ninguna.
Salida · El archivo reporte.txt generado.
Escenario de prueba
$ gdb -batch -x reporte.gdb ./demo
Breakpoint 1 at 0x1129: file demo.c, line 8.
Breakpoint 1, main () at demo.c:8
--- INFORME ---
#0 main () at demo.c:8
argc = 1
argv = 0x7fffffffe0c8
contador = 0
$ cat reporte.txt
Identificadores sugeridos · reporte.gdb, reporte.txt, informe
Restricciones/pistas
- Sin
set confirm off, GDB pregunta antes de salir en modo batch y la ejecución se queda esperando una respuesta. set logging ondebe ir después deset logging file, yset logging offal final, para que el archivo no quede abierto.bt fullmuestra los argumentos de todos los frames, no solo del actual.- Con
pythonpodés ir más lejos: bucles, listas, formateo. Si tu versión de GDB no tiene Python (algunas compilaciones minimalistas), seguí con los comandos nativos.
Criterios de aceptación
- El script corre sin interacción y produce el informe completo.
reporte.txtcontiene el backtrace, las locales y los argumentos.- Las dos formas de invocación funcionan y se comparan.
set confirm offyset logging on/offestán en el script, con su razón.- El script es reutilizable: funciona con un programa distinto sin cambiarse.
PARTE 8 — Taller práctico integrador
Acá juntamos todo. No es un ejercicio más: es un caso, con su método.
8.1 El caso
Tenés un programa que anda en las pruebas y falla en producción. El síntoma es triple:
- Consume memoria sin parar (el monitor de memoria del servidor se quejó).
- A veces se cuelga con
Segmentation fault(pero solo en la versión de producción). - A veces no termina: el proceso queda vivo, sin hacer nada.
El programa es un gestor de inventario: guarda una lista de artículos, cada artículo con su
nombre, cantidad y precio. Tiene un main que arma la lista, la filtra y muestra un resumen.
Y acá viene la parte importante: no te vamos a dar el diagnóstico. Te vamos a dar el código, el síntoma y el método. El trabajo es tuyo.
8.2 El método: hipótesis, evidencia, conclusión
Depurar no es probar comandos al azar. Es un método:
┌──────────────┐
│ SÍNTOMA │ "se cuelga", "se cuelga", "fuga"
└──────┬───────┘
▼
┌──────────────┐
│ ACOTAR │ reducir el espacio de búsqueda con herramientas
└──────┬───────┘ (breakpoints, watchpoints, sanitizers, logs)
▼
┌──────────────┐
│ HIPÓTESIS │ "creo que X se rompe porque Y"
└──────┬───────┘
▼
┌──────────────┐
│ EVIDENCIA │ ¿la herramienta confirma o descarta la hipótesis?
└──────┬───────┘
▼
┌──────────────┐
│ CONCLUSIÓN │ causa raíz + corrección
└──────────────┘
Y hay una regla que hace todo el trabajo:
Cada paso tiene que reducir la incertidumbre. Si un experimento no te deja más seguro de dónde está el problema (aunque te descarte una hipótesis), está bien hecho. Si te deja igual, el experimento estaba mal planteado.
8.3 Las cinco herramientas del taller
| Herramienta | Qué te da | Cuándo la usás |
|---|---|---|
| ASan / LSan | Archivo y línea exactos de un error de memoria | Sospechás de memoria |
| Watchpoint | El instante exacto en que un valor cambia | Sospechás corrupción de datos |
| Breakpoint condicional | Filtrar las paradas para no ahogarte en el ruido | El bucle tiene muchas iteraciones |
bt + frames |
La historia de la llamada | Sospechás de un Segfault |
thread apply all bt |
El estado de todos los hilos | Sospechás de un cuelgue |
8.4 Qué hay que entregar
Un informe de incidente, con esta estructura:
1. SÍNTOMA
Qué se observó, cómo se reproduce, con qué frecuencia.
2. DIAGNÓSTICO
La causa raíz, con la evidencia que la sostiene.
3. EVIDENCIA
El comando exacto de GDB (o sanitizer) y su salida relevante.
Un fragmento por cada afirmación del diagnóstico.
4. CORRECCIÓN
Qué se cambió y por qué esa corrección ataca la causa y no el síntoma.
5. VERIFICACIÓN
Cómo se comprobó que el problema se resolvió (el programa ya no se cuelga,
ASan no reporta fugas, el bucle termina, ...).
Por qué insistimos en el informe. Porque en el trabajo real nadie te va a preguntar qué comandos usaste: te van a preguntar qué encontraste y por qué estás seguro. El informe es la forma de comunicar la evidencia. Y escribirlo es lo que convierte "toqué unos comandos" en "diagnostiqué un bug".
Práctica de la Parte 8
Ejercicio 20.21 — (JEFE) El taller: fuga, Segfault y bucle infinito
Conceptos · todo el capítulo: ASan/LSan, watchpoints, breakpoints condicionales, bt,
frames, x, core dumps, thread apply all bt, método de diagnóstico
Contexto · Un gestor de inventarios que, según el reporte de producción, "consume memoria sin parar, a veces se cuelga y a veces no termina". El código está abajo. Los errores están ahí: tu trabajo es encontrarlos, probarlos y arreglarlos.
Consigna · Diagnostica los tres problemas del programa y entregá el informe de incidente con la estructura de 8.4.
El programa (el material del ejercicio):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char nombre[32];
int cantidad;
float precio;
} Articulo;
static Articulo *inventario = NULL;
static int total_articulos = 0;
/* Registra un articulo en el inventario. */
static void agregar(const char *nombre, int cantidad, float precio) {
/* Copia del nombre para el registro de auditoria. */
char *registro = malloc(strlen(nombre) + 1);
if (registro == NULL) {
exit(1);
}
strcpy(registro, nombre);
Articulo **tmp = realloc(inventario, (size_t)(total_articulos + 1) * sizeof(Articulo));
if (tmp == NULL) {
exit(1);
}
inventario = tmp;
strncpy(inventario[total_articulos].nombre, nombre,
sizeof(inventario[total_articulos].nombre));
inventario[total_articulos].cantidad = cantidad;
inventario[total_articulos].precio = precio;
total_articulos++;
printf("[agregado] %s\n", registro);
}
/* Devuelve una copia de los articulos; con solo_con_stock, solo los que tienen stock. */
static Articulo *copiar_filtrados(int solo_con_stock) {
int n = 0;
for (int i = 0; i < total_articulos; i++) {
if (solo_con_stock && inventario[i].cantidad == 0) {
continue;
}
n++;
}
Articulo *copia = malloc((size_t)n * sizeof(Articulo));
if (copia == NULL) {
exit(1);
}
int k = 0;
for (int i = 0; i < total_articulos; i++) {
if (solo_con_stock && inventario[i].cantidad == 0) {
continue;
}
copia[k] = inventario[i];
k++;
}
return copia;
}
/* Avisa por pantalla los articulos agotados. */
static void avisar_agotados(const Articulo *arts, int n) {
int i = 0;
while (i < n) {
printf(" revisando: %s\n", arts[i].nombre);
if (arts[i].cantidad == 0) {
printf(" agotado: %s\n", arts[i].nombre);
}
n++;
}
}
/* Imprime la lista de articulos. */
static void imprimir(const Articulo *arts, int n) {
for (int i = 0; i < n; i++) {
printf("%-16s %5d %8.2f\n", arts[i].nombre, arts[i].cantidad, arts[i].precio);
}
}
int main(void) {
agregar("Tornillo M8", 120, 1.5f);
agregar("Tuerca M8", 300, 0.8f);
agregar("Tornillo M12", 40, 3.1f);
agregar("Arandela", 0, 0.2f);
Articulo *con_stock = copiar_filtrados(1);
Articulo *todos = copiar_filtrados(0);
char resumen[64];
snprintf(resumen, sizeof(resumen), "Inventario: %d articulos", total_articulos);
printf("%s\n", resumen);
avisar_agotados(todos, total_articulos);
imprimir(con_stock, total_articulos);
imprimir(todos, total_articulos);
free(con_stock);
free(todos);
free(inventario);
return 0;
}
Entrada · Ninguna: el programa se ejecuta solo. Salida · El informe de incidente con los tres diagnósticos, sus evidencias y las correcciones aplicadas.
Escenario de prueba
$ gcc -g3 -O0 -Wall -Wextra -o inventario inventario.c
$ ./inventario
[agregado] Tornillo M8
[agregado] Tuerca M8
[agregado] Tornillo M12
[agregado] Arandela
Inventario: 4 articulos
revisando: Tornillo M8
revisando: Tornillo M8
revisando: Tornillo M8
... (no termina nunca)
$ gcc -g3 -O0 -fsanitize=address -o inventario_asan inventario.c
$ ./inventario_asan
=================================================================
==4242==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000010
READ of size 40 at 0x602000000010 thread T0
#0 0x4012f7 in imprimir /home/alumno/inventario.c:78
#1 0x401305 in main /home/alumno/inventario.c:96
...
Ojo con el orden. Los tres problemas no se ven todos a la vez: el cuelgue tapa a los otros dos. El camino natural es primero destrabar el cuelgue, después recién aparecen los síntomas de memoria.
No te damos la lista de errores. Eso es lo que el capítulo te enseñó a hacer: acotar con herramientas hasta que la evidencia te imponga la respuesta. Usá el método de 8.2 y el informe de 8.4.
Identificadores sugeridos · inventario.c, agregar, copiar_filtrados, inventario,
total_articulos
Restricciones/pistas
- El
-Wall -Wextradel comando de compilación avisa de cosas: leé los avisos con atención, son pistas gratis. - El síntoma de "consume memoria sin parar" y el de "se cuelga" pueden venir del mismo lugar o de lugares distintos. Investigá cada uno por separado y anotá de dónde sale cada evidencia.
- Para el bucle infinito:
Ctrl+Cinterrumpe el programa, y GDB te muestra dónde estabas. Ese es tu punto de partida. - Para el
Segfault: activá los core dumps y analizá el post-mortem, o usá ASan. - El reporte de fugas de LSan aparece cuando el programa termina. Si el programa se cuelga, no hay salida y no hay reporte: arreglá el cuelgue primero.
- La fuga se repite por cada artículo registrado. Por eso el reporte de producción dice "consume memoria sin parar": acá solo se registran cuatro, así que el número es chico, pero la cuenta es la misma. Fijate en qué se repite, no solo cuánto suma.
- Cuando encontrás el bug, no te conformes con el síntoma: preguntate si el programa puede fallar por otra razón con los mismos datos. Buscá el error de clase, no el de instancia.
Criterios de aceptación
- Los tres problemas están diagnosticados, cada uno con evidencia citada (el comando exacto y su salida).
- El informe sigue la estructura de 8.4 (síntoma → diagnóstico → evidencia → corrección → verificación).
- Cada corrección está verificada con una ejecución posterior (el programa ya no se cuelga, ASan no reporta fugas, el bucle termina).
- El informe explica por qué la memoria crecía con el uso: qué hace que la fuga se repita y cuántas veces ocurre.
- El código corregido compila sin avisos con
-Wall -Wextra, o cada aviso restante está justificado en el informe. - Se identificó al menos un riesgo adicional que el enunciado no pedía (por ejemplo, un error de la misma clase en otro lugar del código), o se justificó por qué no lo hay.
- El diagnóstico del bucle infinito incluye dónde estaba el programa cuando lo interrumpiste.
20.22 Errores comunes
- Depurar con
-O2. Las variables desaparecen (<optimized out>), el código se reordena ynextsalta. Depurá con-O0 -g. - Olvidar
-gpor completo. GDB no tiene ni la tabla de líneas ni las variables locales: solo podés parar en nombres de función. - Confundir
pcon*p.p pmuestra la dirección;p *pmuestra el valor. La mitad de losSegfaultde principiante son esto. breaken una línea que no es ejecutable. Comments,}y declaraciones de función no admiten breakpoints: GDB te ofrece moverlo.- Ignorar la ventana equivocada. Si escribís un comando y no pasa nada, es que el foco no está
en
cmd.tui focus cmd. - Apilar 20 breakpoints y no saber cuál disparó.
info breakpointscon el campoWhates tu mapa. Usácommandsconsilentpara que solo se anuncie el que importa. - No usar
set print elements 0. Los arreglos grandes se muestran truncados a 200 elementos, y el...hace creer que el arreglo es más corto. pde unchar[]sin'\0'. GDB imprime basura y parece un bug del depurador: es un bug del programa.- Confundir el frame actual. Después de un
up,print totalmuestra otra variabletotal. Volvé aframe 0cuando dudes. - Olvidar
detachdespués de unattach. Salís de GDB y mataste el proceso que estabas investigando. ulimit -cen 0 y esperar un core. El mensaje "core dumped" aparece igual, pero no se genera archivo.- Confiar en
corecuando el sistema usasystemd-coredump. No hay archivo en tu carpeta: usácoredumpctl. abort_on_errorsinASAN_OPTIONS. El sanitizer imprime el reporte y termina: para pararlo en GDB hay que pedirlo explícitamente.- Parchear memoria y creer que arreglaste el bug. El bug sigue en el código; el próximo
runlo vuelve a traer. stepen vez denexty perderse en la biblioteca estándar.nextno entra a las funciones;stepsí. Si te perdiste dentro deprintf, usáfinish.- Correr
runmil veces. Si el bug es intermitente, el problema no es el debugger: es que no tenés una hipótesis. Vuelvé a 8.2. - No guardar la evidencia.
set logging onte salva la sesión entera en un archivo, y ese archivo es lo que entregás.
20.23 Cierre
Si tuvieras que quedarte con una sola idea, que sea esta: un depurador no te da la respuesta, te da la evidencia. Tu trabajo es mirar la evidencia y razonar.
- Entendiste que un binario sin información de depuración es un libro en clave: los símbolos
(
DWARF) son la llave, y-g3 -O0es la cerradura. Sabés por qué-O2rompe la depuración y por qué hay que depurar y medir por separado. - Conocés la TUI: las ventanas (
src,asm,regs,stack,threads,cmd), los layouts (incluido los que vos creás contui new-layout), el foco y el redibujado. - Manejás el control del flujo (
next,step,finish,continue,until) y la inspección (print,ptype,info locals,display), y sabés cuándo usar cada uno. - Ves direcciones y desreferencias (
&,*), arreglos (incluido el arreglo artificialp *arr@10), structs (p s,p s->campo) y sabés quep xyp *pson cosas distintas. - Usás breakpoints condicionales,
ignoreycommandspara filtrar el ruido, y watchpoints para encontrar el instante exacto en que un valor se corrompe. - Recorrés la pila de llamadas (
bt,frame,up,down) y entendés por qué la misma variable local tiene distinto valor en cada frame. - Diagnosticás errores de memoria: activás core dumps, analizás el post-mortem con
bt+info registers+x/i $pc, auditás memoria cruda conx/NFU, y complementás con sanitizers (ASan, UBSan, TSan) y Valgrind. - Bajaste al ensamblador:
disassemble,stepi,nexti, la convención de llamada, y el parcheo en caliente para probar hipótesis. - Automatizaste:
.gdbinit, scripts con-batch/-x/-ex,logging, hilos conthread apply all bt,forkconfollow-fork-mode,attacha procesos vivos y depuración remota congdbserver.
Resumen de la caja de herramientas.
| Quiero… | Comando | Idea en una frase |
|---|---|---|
| Cargar el programa | gdb ./demo |
GDB no lo ejecuta hasta que vos lo decís |
| Correr hasta una parada | run, start, starti |
start frena adentro de main |
| Parar en una línea | break demo.c:42 |
El breakpoint frena antes de esa instrucción |
| Parar una sola vez | tbreak demo.c:42 |
Se borra solo al dispararse |
| Parar solo a veces | break ... if x > 5, ignore N 500 |
Filtrar las paradas |
| Ver los breakpoints | info breakpoints |
El campo What es tu mapa |
| Automatizar un breakpoint | commands … end |
Instrumentar sin tocar el código |
| Avanzar una línea | next / step |
next no entra a las funciones; step sí |
| Salir de la función | finish |
Vuelve a quien llamó |
| Ver un valor | print x, p/x x, p/c x |
Formatos para cada tipo |
| Ver el tipo | ptype x |
La definición completa |
| Ver todas las locales | info locals, info args |
El volcado del frame |
| Ver siempre un valor | display x |
Se reevalúa en cada parada |
| Frenar si cambia | watch x |
Hardware watchpoint sobre el valor |
| Ver una dirección | p &x, p *p |
Dirección y valor son cosas distintas |
| Ver un arreglo | p *arr@10 |
El arreglo artificial |
| Ver una struct | p s, p s->campo, ptype /o struct S |
Hasta los offsets |
| Ver la pila | bt, frame N, up, down |
La historia de la llamada |
| Mirar la memoria cruda | x/4xw $sp, x/s buf, x/i $pc |
Formato N elementos, F formato, U tamaño |
| Analizar un crash | gdb ./demo core |
Post-mortem: bt, info registers, x/i $pc |
| Detectar errores de memoria | -fsanitize=address |
Te da archivo y línea exactos |
| Ver el ensamblador | disassemble /s main |
La instrucción al lado del C |
| Avanzar una instrucción | stepi, nexti |
Un paso de máquina |
| Cambiar una variable | set var x = 42 |
Probar hipótesis |
| Escribir en memoria | set {int}0x... = 42 |
Parcheo en caliente |
| Ver los hilos | info threads, thread apply all bt |
El estado de todos a la vez |
| Congelar los otros hilos | set scheduler-locking on |
Depuración determinista |
| Conectarse a un proceso vivo | gdb -p PID, detach |
Entrar y salir sin matar |
| Depurar remoto | gdbserver :2345, target remote |
Cliente y servidor separados |
| Automatizar | -batch -x script.gdb, set logging on |
El script trabaja solo |
El puente a lo que sigue. Ya podés depurar cualquier programa de C: mirar variables, recorrer la pila, encontrar la línea del bug, analizar un crash, automatizar el diagnóstico y pegarte a un proceso vivo. Eso es una competencia profesional, no un truco.
Y el próximo paso natural es combinarla con lo que ya sabés: sanitizers para los errores de memoria (este capítulo), el capítulo
09para entender la memoria dinámica que estás depurando,15para los archivos y12para las arenas. Un gestor de inventarios con fugas, con arenas, con archivos y con depurador es un programa de verdad — y a esta altura ya tenés las herramientas para construirlo sin adivinar.