diff --git a/book/20-gdb.md b/book/20-gdb.md new file mode 100644 index 0000000..e9b9a5e --- /dev/null +++ b/book/20-gdb.md @@ -0,0 +1,3778 @@ +# 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 Fault` y 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.** +> +> 1. **GDB es una herramienta de Unix.** En Linux funciona tal cual. En macOS funciona, pero el +> depurador del sistema es `LLDB` y hay que firmar el binario de `gdb` para que el sistema lo +> deje ejecutar. En Windows, `gdb` existe vía **MSYS2 / MinGW-w64** y funciona bien para +> programas C, pero sin `ulimit -c` (o sea: sin core dumps automáticos) y con otras +> diferencias que vamos a señalar cuando aparezcan. +> 2. **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. + +```text + ./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`: + +```bash +$ 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 `.symtab` y `.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 `.symtab` +> podés poner un breakpoint en `main`; sin `.debug_info` **no 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, `total` es la dirección `0x7ffd...`. +> DWARF es el **libro de anotaciones** que dice: "en la dirección `0x1129` hay código de la línea +> 42 del archivo `main.c`; en ese punto, la variable `total` está en el offset `-0x18` del marco de +> la pila y es de tipo `int`". + +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: + +1. ¿Guardo **información de depuración**? → `-g`, `-g3` +2. ¿**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 `typedef`s** | **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ó. + +```bash +# 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": + +```text + 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 `` | +| `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: +> +> ```text +> (gdb) print counter +> $1 = +> ``` +> +> 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`: + +```bash +$ 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 + +```bash +$ 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.sh` junto 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: + +```bash +$ 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. + +```bash +$ 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). + +```bash +$ objdump -d ./demo +... +0000000000001129
: + 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. + +```bash +$ 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 `demo` que vos depurás **tiene que** tener `-g`. La forma sana de hacerlo +> es **distribuir los símbolos aparte** (el paquete `dbgsym` en Linux, los archivos `.dSYM` en +> 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ó. + +```text + [ ] 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**: + +1. `gcc -o demo_stripped demo.c` +2. `gcc -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** + +```text +$ 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 `-g` **no** tiene por qué fallar: sigue funcionando perfectamente. Lo que + pierde es **la capacidad de ser depurado en detalle**. +- `readelf -S` muestra muchísimas secciones: a vos te interesan `.symtab`, `.strtab` y las que + empiezan con `.debug_`. +- `nm` **no** 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. +- [ ] `file` reporta 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é `nm` da 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: + +1. Compilá un programa **sin** `-g`. +2. 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). +3. Leé el mensaje de error de GDB **entero** y anotá qué te dice. +4. Usá `info line :` y `info functions` para juntar evidencia. +5. Recompilá **con** `-g3 -O0` y repetí el paso 2: ahora debería funcionar. + +**Entrada** · Ninguna. +**Salida** · Un diagnóstico escrito: **síntoma → evidencia → causa → solución**. + +**Escenario de prueba** + +```text +$ gcc -o demo demo.c +$ gdb ./demo +(gdb) break demo.c:10 +No source file named demo.c. +Make breakpoints pending? (y or n) +``` + +```text +$ 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 en `demo.c`. +- `info functions` responde 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 line` y `info functions` como 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: + +```bash +$ 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: + +```text +(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: + +```text +GNU gdb (GDB) 14.2 +Copyright (C) 2023 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later +... +(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: + +```text +(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 # el autocompletado completa a "break main" +``` + +> **Momento ajá.** `apropos` y el autocompletado con `TAB` son los dos atajos que hacen que GDB +> pase de "mando a ciegas" a "herramienta de trabajo". `apropos` es tu buscador: si sabés lo que +> querés pero no cómo se llama el comando, `apropos` te 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. + +```text ++-----------------------------------------------------------+ +| 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ás `Ctrl+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`, `threads` y `breakpoints` son las que más rinden en el día a día. +> `regs` y `asm` las vas a necesitar en la Parte 6. `cmd` está 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: + +```text +(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 + +```text +(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: + +```text +(gdb) tui next-layout +``` + +Y para elegir uno por nombre: + +```text +(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: +> +> ```text +> (gdb) tui new-layout depurar +> ``` +> +> GDB **guarda esa disposición con nombre**. Después la recuperás cuando quieras con +> `tui select-layout depurar`. Es un `.gdbinit` hecho 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: + +```text +(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: + +```text ++---------------------------+-------------------------+ +| 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 `ascii` es 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 ascii` los reemplaza por `+`, `-`, `|`. Se ve menos elegante pero se lee +> perfecto. + +> **La trampa N.º 1 de la TUI: perdiste el foco.** Si escribís `print total` y en pantalla no pasa +> nada, casi seguro estás en la ventana equivocada. `tui focus cmd` y 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: + +```text +(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: + +```bash +$ shell ls -la # ejecutás un comando de shell SIN salir de GDB +``` + +```text +(gdb) set logging file session.txt +(gdb) set logging on +(gdb) run +(gdb) set logging off +``` + +> **Truco de laboratorio: grabá tus sesiones.** `set logging on` escribe **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: + +1. Poné la ventana de **código fuente** (`layout src`) y la de **comandos** (`layout cmd`). +2. Recorré los layouts prefabricados con `tui next-layout` y anotá cuáles existen en tu versión + de GDB (el comando `help tui` te los lista). +3. 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 `. +4. Recuperala con `tui select-layout `. +5. Practicá el foco: `tui focus src`, `tui focus cmd`, `tui focus next`. +6. Provocá un desborde: hacé `tui disable` y `tui enable` y 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** + +```text +(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 enable` deja 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: + +1. Ponela en pantalla con `layout `. +2. Poné el foco en ella con `tui focus `. +3. **Escribí** qué información muestra y **para qué te sirve**. +4. Buscá un momento del programa donde esa ventana tenga contenido **interesante** (por ejemplo: + la ventana `stack` con varias funciones en la pila, o la ventana `regs` con valores distintos + de cero). + +Completá esta tabla en tu cuaderno o en un archivo de texto: + +```text +ventana │ ¿qué muestra? │ ¿cuándo me sirve? +─────────────┼──────────────────────────────────────┼────────────────────────── +src │ │ +asm │ │ +regs │ │ +stack │ │ +threads │ │ +breakpoints │ │ +status │ │ +cmd │ │ +``` + +**Entrada** · Ninguna. +**Salida** · La tabla completa. + +**Escenario de prueba** + +```text +(gdb) layout regs +(gdb) tui focus regs +(gdb) info registers +rax 0x1 0 +rbx 0x0 0 +rip 0x401136 0x401136 +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 tui` lista 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 + `stack` se 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` | + +```text +(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** en `main` y 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 `run` y `start`, en una frase.** `run` deja el programa correr hasta que +> pase algo interesante (un breakpoint, un crash). `start` te deja **dentro** de `main`, listo para +> que examines las variables locales desde el primer momento. Si vas a depurar una función +> concreta, `start` + `break ` + `continue` es 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: + +```text +(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: + +```text +(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. + +```text +(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 `break` se pone **en una línea ejecutable**. Si ponés `break demo.c:7` y +> 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 + +```text +(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 + +```text +(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 2` lo apaga sin perderlo. `delete` se 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. + +```text +(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": + +```text +(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: + +```c +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 `next` en la línea 4, el próximo `next` te deja en la **línea 6**: `sumar()` se ejecutó + entera, pero no te preocupaste por lo que pasó adentro. +- Con `step` en la línea 4, el próximo `step` te mete **dentro de `sumar()`**. + +> **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. + +```text +(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: + +```text +(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) + +```text +(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. + +```text +(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: + +```text +(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` | + +```text +(gdb) print/x total +$1 = 0x0 +(gdb) print/c letra +$2 = 65 'A' +``` + +### Los otros comandos de inspección + +```text +(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`): + +```text +(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: + +```text +(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 + +```text +(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: + +```text +(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` + `next` en 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. + +```text +(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: + +1. Poné un punto de interrupción **por función** en las tres (`break main`, `break tic`, + `break tac`). +2. Poné un punto de interrupción **por archivo y línea** dentro de `tic`. +3. Poné un punto de interrupción **temporal** en `tac`. +4. Listalos con `info breakpoints` y **anotá** qué muestra cada columna. +5. Deshabilitá uno, habilitalo de nuevo, y borrá otro. +6. Dejá **un** punto de interrupción con `commands` que 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** + +```text +(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 `Enb` te dice si el breakpoint está activo (`y`) o apagado (`n`). +- El `What` te 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 breakpoints` está capturado y anotado. +- [ ] Se demuestra el ciclo `disable` → `enable` y el `delete`. +- [ ] El breakpoint con `commands` imprime 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`. + +1. Poné un breakpoint en `encolar` con `commands` que imprima el valor recibido y el índice de la + posición de escritura, y que **siga** automáticamente. +2. Corré el programa con **cinco** encolados y capturá la salida completa. +3. Ahora hacé lo mismo con `desencolar`. +4. Repetí el punto 1 pero **sin** el `continue` final: 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** + +```text +(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 `printf` de GDB usa **la misma sintaxis** que el `printf` de C: `%d`, `%s`, `%x`… +- Los argumentos de `printf` en GDB se evalúan **en el contexto del breakpoint**: es decir, en el + frame de la función donde estás. +- Si `printf` dice `No 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 `commands` se dispara **automáticamente** en cada llamada. +- [ ] La salida de GDB y la del programa se distinguen claramente. +- [ ] El caso sin `continue` está explicado (el programa queda parado en el primer disparo). +- [ ] El `printf` usa 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. + +1. Poné un breakpoint **dentro** de la función. +2. Usá `ptype` para imprimir la definición del tipo de la `struct` y de cada campo. +3. Usá `print` para mostrar la `struct` **completa**, y después **campo por campo**. +4. Usá `print` sobre el **puntero** a la struct, y sobre el puntero **desreferenciado**. +5. Activá `set print pretty on` y compará cómo cambia la salida. +6. Usá `info locals` e `info args` para 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** + +```text +(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** +- `print` de una `struct` con un `char[]` muestra el texto **hasta el primer `'\0'`**. Si ves + basura, el array no está terminado — y eso puede ser **el bug**. +- `ptype` acepta un **tipo** (`ptype struct Ficha`) o una **expresión** (`ptype ficha`). +- Con `set print pretty on` la struct se muestra en varias líneas, que es más legible cuando + tiene muchos campos. + +**Criterios de aceptación** +- [ ] `ptype` devuelve 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 on` cambió 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: + +```text +(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: + +```text +(gdb) print *0x7fffffffe320 +$1 = 0 +``` + +GDB acepta direcciones literales. Y si la variable es un puntero: + +```text +(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.** `p` es una **ficha** con la dirección de un sobre. `*p` es **abrir** el sobre y +> ver lo que hay adentro. `&total` es la dirección **del** sobre. Confundir `p` con `*p` es el +> error conceptual más común de la primera hora — y es exactamente el error que produce el +> `Segmentation Fault` del capítulo siguiente. + +Y para ver los **tamaños**: + +```text +(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: + +```text +(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: + +```text +(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: + +```text +(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!). + +```text +(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: + +```text +(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 `.`: + +```text +(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**: + +```text +(gdb) print *fichas@3 +$1 = {{id = 1, ...}, {id = 2, ...}, {id = 3, ...}} +``` + +> **La trampa N.º 1 de las structs.** `print ficha` y `print *pf` muestran lo mismo, pero +> `print ficha.id` **no compila** si `ficha` es un puntero, y `print pf->id` **no compila** si +> `pf` no 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): + +```text +(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`): + +```text +(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**: + +```text +(gdb) break procesar +(gdb) condition 1 i == 500 +``` + +O todo de una: + +```text +(gdb) break procesar if i == 500 +``` + +Y hay un comando gemelo que es todavía más útil: **`ignore`**. + +```text +(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: + +```text +(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` + `commands` es, 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`". + +```text +(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: + +```text +(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: + +```text +(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*): + +```text +(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í: + +- `#0` es el frame **actual**: estás adentro de `sumar`, con `x = 5`. +- `#1` es quien la llamó: `main`, en la línea 12. +- `#2` y `#3` son 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 + +```text +(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: + +```text +(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 `#0` y mirás `total`, ves el `total` de `sumar`. +> Cuando hacés `up` y mirás `total`, ves **otro** `total`: el de `main`. 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ítulo `05`), y por eso `up` / `down` son la herramienta para cazarlos. + +### La pila crece hacia abajo + +```text + 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: + +1. declare una variable local `int numero = 42;` +2. declare un puntero `int *p = №` +3. haga un `malloc` de un `int`, le escriba un valor, y lo recorra con un puntero. + +Compilá con `-g3 -O0` y, con un breakpoint en el `main`: + +1. Imprimí `&numero`, `numero`, `p` y `*p`. +2. Verificá que `p` y `&numero` son **el mismo** número. +3. Imprimí el puntero del `malloc`, su contenido (`*ptr`) y el `sizeof` de ambas cosas. +4. Mirá la **diferencia entre el puntero y la variable**: `print p` y `print *p` dan cosas + distintas. Anotá cuál es cuál. +5. 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** + +```text +(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 `malloc` devuelve `void *`: en C hay que convertirlo (capítulo `07`). GDB puede mostrarlo + como `(int *) 0x...`. +- El puntero liberado **no se vuelve `NULL`**: `free` no lo toca. Volver a leerlo es + *undefined behavior* (capítulo `09`) y con suerte vas a ver el valor viejo. +- Usá `p/x` para 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 `p` y `&numero` coinciden. +- [ ] 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 `malloc` **sí** se liberó (con `free`). + +--- + +### 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ó. + +1. Corré el programa **sin** depurador y anotá el índice que devuelve. +2. En GDB, poné un breakpoint en la línea donde se informa el índice y usá `print` para ver el + valor en cada iteración. +3. Ahora reemplazá ese breakpoint por uno **condicional** que solo se dispare cuando el valor + actual sea el que buscás. +4. Repetí con `ignore` para saltar directo a la iteración que te interesa. +5. Sumá `commands` + `silent` + `continue` para 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** + +```text +$ ./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. +- `ignore` cuenta **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 `ignore` también llega al mismo lugar. +- [ ] La variante con `commands` + `silent` + `continue` imprime 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: + +1. tenga una variable global `int saldo = 100;` +2. 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). +3. un `main` que las llame en un orden dado. + +Con un breakpoint en el `main`, hacé: + +1. `watch saldo` y seguí la ejecución hasta que se dispare. +2. Anotá el **valor viejo** y el **valor nuevo**, y **en qué línea** se dispara. +3. Repetí hasta identificar **cuál de las cuatro funciones** rompe el saldo. +4. Con `awatch` en lugar de `watch`, observá también las **lecturas**. +5. Con `info watchpoints` listá los que definiste. + +**Entrada** · Ninguna. +**Salida** · La identificación de la función culpable, con la evidencia del watchpoint. + +**Escenario de prueba** + +```text +(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 `-O0` y evitás el problema. +- `watch` sobre 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). +- [ ] `awatch` mostró también las lecturas, y la diferencia con `watch` está 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`: + +1. `bt` — dibujá la escalera completa. +2. `print valor` — ¿de qué `valor` es este? +3. `up` → `print valor` → `up` → `print valor` → repetí hasta `main`. +4. Anotá **el valor de `valor` en cada nivel**. ¿Son el mismo? ¿Por qué no? +5. `info args` e `info locals` en dos niveles distintos. +6. Usá `finish` para volver al frame de arriba y `down` para bajar otra vez. +7. `info frame` en el frame actual: ¿qué datos da? + +**Entrada** · Ninguna. +**Salida** · La escalera anotada, con el valor de `valor` en cada escalón. + +**Escenario de prueba** + +```text +(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. +- `up` y `down` cambian el frame **actual**: a partir de ahí, `print`, `info locals` y `info args` + miran a ese frame. +- `finish` **ejecuta** el resto de la función: el programa avanza hasta que `nivel4` retorna. + +**Criterios de aceptación** +- [ ] La escalera completa (`bt`) está dibujada con los argumentos de cada nivel. +- [ ] El valor de `valor` en **cada** escalón está anotado y se ve que **difieren**. +- [ ] Se explicó por qué difieren (cada frame tiene su propia copia). +- [ ] `info args` e `info locals` se usaron en **dos** frames distintos. +- [ ] `up`, `down` y `finish` se 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: + +```text +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: + +```text +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`: + +```bash +$ 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 tu `ulimit -c` está 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: + +```bash +$ 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.` (ú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`: + +```bash +$ coredumpctl list +$ coredumpctl gdb +``` + +> **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 con `ulimit -c unlimited` +> y se llaman `core.`. 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**: + +```bash +$ 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: + +```text +(gdb) bt +#0 0x0000000000001150 in main () at demo.c:12 +#1 0x0000000000001196 in __libc_start_main () at ../csu/libc-start.c:308 +``` + +```text +(gdb) info registers +rax 0x0 0 +rbx 0x7fffffffe3a0 0x7fffffffe3a0 +rip 0x555555554150 0x555555554150 +rsp 0x7fffffffe300 0x7fffffffe300 +``` + +```text +(gdb) x/i $pc +=> 0x555555554150 : mov %eax,(%rbp-0x4) +``` + +Leélo así: + +- `bt` te dice **dónde** se rompió: en qué función y en qué línea. +- `info registers` te dice **el estado** en ese instante: qué había en los registros. +- `x/i $pc` te 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: + +```text +(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: + +```text + 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 + +```text +(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 : 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: `x` para auditar un buffer.** Si un programa llena un `char buf[32]` con un +> texto de 40 caracteres, `x/40cb buf` te 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. + +```bash +$ 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 + +```text +==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 + +```text +================================================================= +==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. + +```bash +$ 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**: + +```text +(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: + +```bash +$ 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: + +```bash +$ 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 +> `memcheck` sobre 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. + +1. Verificá `ulimit -c` y ponelo en `unlimited`. +2. Corré el programa y confirmá que se cuelga. Capturá el mensaje **exacto**. +3. Localizá el archivo del core (`ls`, `cat /proc/sys/kernel/core_pattern`). +4. Abrilo con `gdb ./caja core` (o `core.core`). +5. En el post-mortem, reuní el **triángulo forense**: `bt`, `info registers` y `x/i $pc`. +6. 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** + +```text +$ 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-coredump` o `apport`, no vas a ver el archivo en tu carpeta: usá + `coredumpctl list` y `coredumpctl gdb`. +- En el post-mortem, `run` **no** funciona: el programa ya terminó. Solo `bt`, `info registers`, + `x/`, `print`. +- Para el diagnóstico del puntero nulo: `p nulo` te muestra el valor, y `info registers` te + 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é `run` no 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: + +1. declare un `char buffer[32]`; +2. lo llene con un texto **más corto** que 32 usando `strcpy`; +3. declare un `int numeros[8]` y lo llene con valores conocidos; +4. declare un `char *texto = "hola mundo";`. + +Con un breakpoint después de las cargas, usá `x` para: + +1. Mostrar los 32 bytes del buffer en hexadecimal (`x/32xb buffer`). +2. Mostrar el buffer como caracteres (`x/32cb buffer`) y **apuntar dónde está el `'\0'`**. +3. Mostrar el arreglo de enteros en palabras de 4 bytes (`x/8xw numeros`) y **verificar que + coincide** con lo que cargaste. +4. Mostrar la cadena con `x/s texto` y con `x/12cb texto`. +5. Mirar los primeros bytes de la pila con `x/8xw $sp`. +6. 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** + +```text +(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 como `0x00`. + Si el texto tiene 10 caracteres, el `0x00` está en la posición 11 (índice 10). +- `x/s` lee **hasta el primer `'\0'`**: si el buffer no está terminado, `x/s` se va de largo y + te muestra basura (o se cuelga). Eso es información, no un bug del comando. +- El tamaño `w` son **4 bytes** (32 bits) en x86-64; `g` son 8 bytes. + +**Criterios de aceptación** +- [ ] Se usaron **al menos** tres formatos (`x`, `d`, `c` o `s`) 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/s` y `x/Ncb` sobre la misma cadena dan **resultados distintos**, y la diferencia está + explicada. +- [ ] `x/i $pc` se 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: + +1. tenga una función `crear_lista(int n)` que arma una lista de `n` elementos con `malloc` y la + devuelve; +2. tenga una función `filtrar(Lista *l)` que cree una **nueva** lista con los elementos pares y la + devuelva; +3. el `main` llame a las dos y **no** libere la lista original. + +Después: + +1. Compilá **sin** sanitizer y corré el programa con un `n` grande: observá que **no** falla. +2. Recompilá con `-fsanitize=address -g3 -O0` y corré el mismo programa. +3. Leé el reporte de ASan: ¿qué dice? ¿de dónde salió la memoria perdida? +4. Recompilá con `ASAN_OPTIONS=abort_on_error=1` y metelo en GDB: ¿dónde se detiene? +5. **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** + +```text +$ 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=address` **ya incluye** el detector de fugas: no hace falta + `-fsanitize=leak` aparte. +- 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=1` se 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 + +```text +(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: + +```text +(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 + +```text +(gdb) info registers +rax 0x1 0 +rbx 0x0 0 +rcx 0x7fffffffe320 0x7fffffffe320 +rdx 0x0 0 +rsi 0x0 0 +rdi 0x1 0 +rip 0x401136 0x401136 +rsp 0x7fffffffe300 0x7fffffffe300 +rbp 0x7fffffffe300 0x7fffffffe300 +``` + +Y se pueden consultar **de a uno**, como si fueran variables: + +```text +(gdb) print $rax +$1 = 1 +(gdb) print/x $rip +$2 = 0x401136 +(gdb) print $rsp +$3 = (void *) 0x7fffffffe300 +``` + +> **Los tres registros que importan.** `rip` es el **puntero de instrucción**: la dirección de la +> instrucción que se va a ejecutar. `rsp` es el **puntero de pila**: el tope de la pila (crece +> hacia abajo). `rbp` es 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: + +```text +(gdb) stepi +0x0000000000001131 : mov %edi,-0x14(%rbp) +(gdb) x/i $pc +=> 0x0000000000001135 : mov $0x0,-0x18(%rbp) +(gdb) stepi +0x0000000000001135 : 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 src` y `layout asm` lado a lado. Con +> `stepi` vas 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í: + +```text + 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 en +> `x0`, y `sp`/`lr` cumplen el papel de `rsp`/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 + +```text +(gdb) set var contador = 42 +(gdb) print contador +$1 = 42 +``` + +También funciona con miembros de estructuras: + +```text +(gdb) set var ficha.peso = 99.5 +``` + +### Cambiar un registro + +```text +(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**". + +```text +(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): + +```text +(gdb) x/3i $pc +=> 0x401136 : mov %eax,(%rbp-0x4) + 0x401139 : add $0x1,%eax + 0x40113c : 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 `run` lo +> 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: + +1. reciba dos `int` por parámetro; +2. devuelva la suma; +3. tenga **dos** variables locales. + +Con `-g3 -O0`: + +1. Poné un breakpoint **dentro** de la función y hacé `disassemble /s `. +2. Identificá en el ensamblador: el `push %rbp`, el `sub` que reserva espacio, los `mov` que + copian los argumentos a las variables locales, y el `ret`. +3. Anotá **en qué offset del marco** (`%rbp - N`) vive cada variable local, y **verificalo** con + `p &variable` y `p/x $rbp`. +4. Usá `stepi` para avanzar instrucción por instrucción, y en cada paso `x/i $pc` para ver la + siguiente. +5. Con `info registers`, fijate cómo van cambiando `rdi`, `rsi` y `rax`. + +**Entrada** · Ninguna. +**Salida** · El ensamblador anotado, con el equivalente en C de cada instrucción importante. + +**Escenario de prueba** + +```text +(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,%rsp` reserva 16 bytes de pila: ese es el espacio de tus variables locales. Los + offsets `-0x4`, `-0x8`, `-0xc` son **direcciones negativas desde `%rbp`**. +- Con `p/x $rbp` obtenés la dirección del marco; `p/x &a` te tiene que dar + `$rbp - 0x4` (restando 4). +- En ARM64 el ensamblador es **otro** (no hay `%rbp`, hay `x29`/`x30`). Usá el desensamblado de + **tu** máquina. + +**Criterios de aceptación** +- [ ] El desensamblado con `/s` está capturado. +- [ ] Los roles de `push %rbp`, `sub %rsp`, los `mov` de argumentos y `ret` están identificados. +- [ ] El offset de cada variable local está verificado **con `p/x &var`**, no con suposición. +- [ ] `stepi` se 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. + +1. Corré el programa con un divisor en cero y anotá el síntoma. +2. En GDB, poné un breakpoint **después** de la división y usá `set var` para forzar el divisor a + un valor válido. Anotá qué cambia. +3. Ahora hacé lo mismo pero con `set {int} = `: escribí directamente en la + memoria donde vive la variable. +4. Con `x/i $pc` y `set {char} = 0x90`, neutralizá **una instrucción** de la función y + observá qué deja de hacer el programa. +5. 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** + +```text +(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 var` necesita el **nombre**; `set {int}` necesita la **dirección**. Por eso + `p/x &variable` es el paso previo obligatorio del segundo. +- Cambiar un **argumento** no cambia el valor original de la variable del `main`: `set var` actúa + sobre el **frame actual**. Para cambiar el valor en el frame de arriba, hacé `up` primero. +- Neutralizar una instrucción **cambia la longitud del código**: si la reemplazás por un `nop` de + 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 var` cambió el comportamiento y el cambio está anotado. +- [ ] El caso `set {int}` se hizo **usando la dirección real** de la variable, mostrada con + `p/x &var`. +- [ ] La neutralización con `0x90` usó 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: + +```text +# --- 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 ... end` define un **comando propio**. `pf total` es lo mismo que + `print total`. +- `set step-mode on` hace que `next` y `step` se comporten como en algunos depuradores + comerciales. +- `set confirm off` hace que GDB **no pregunte** antes de borrar todos los breakpoints o de + salir. **Imprescindible en scripts**, incómodo a mano. + +Para saltarlo: + +```bash +$ gdb -nx ./demo # ignora el .gdbinit +$ gdb -nx -q ./demo # además, no muestra el banner +``` + +> **Ojo con esto.** Un `.gdbinit` mal 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. + +```bash +$ 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: + +```text +# 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: + +```bash +$ 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ó: +> +> ```text +> (gdb) set logging file informe.txt +> (gdb) set logging on +> ``` +> +> El archivo `informe.txt` va 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: + +```text +(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 + +```text +(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 + +```text +(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: + +```text +(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: + +```text +(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 on` y `thread 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: + +```text +(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: + +```text +(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()`: + +```text +(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. + +```bash +$ gdb -p 12345 +``` + +o, desde adentro de GDB: + +```text +(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: + +```text +(gdb) detach +``` + +> **Cuadro de mando del `attach`.** +> +> | Acción | Comando | Efecto | +> |---|---|---| +> | Conectarse | `gdb -p PID` / `attach PID` | El proceso se frena | +> | Ver el estado | `info threads`, `bt`, `info proc mappings` | Foto del proceso | +> | Soltar | `detach` | El proceso **sigue corriendo** | +> | Salir | `quit` | GDB se va (el proceso puede quedar vivo o morir: según cómo salgas) | +> +> **Regla de oro: `detach`, nunca `kill`.** Si te colgaste con un `attach` y cerrás GDB con +> `quit` respondiendo "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: + +```text +(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: + +```text +(gdb) set disable-randomization on +``` + +> **Esta segunda trampa es la más frustrante**, porque el síntoma es "`p` me dice que la variable +> vale 0 y claramente vale 42". Si te pasa, agregá esa línea **antes** del `attach`. + +## 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`: + +```bash +# En la máquina remota: +$ gdbserver :2345 ./demo +``` + +Y en tu máquina: + +```bash +$ gdb ./demo +(gdb) target remote :2345 +``` + +O en un solo paso: + +```bash +$ 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): + +```text +(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, `gdbserver` se 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: + +1. **Ajustes de presentación**: historial persistente, `print pretty`, `print elements 0`, + `pagination off`. +2. **Tres comandos propios** con `define`/`end`, que sean de **tu** uso real: + - uno que imprima las variables locales con formato (`info locals` + `p` de 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. +3. **Un alias** con `alias` (no con `define`) para el comando que uses más. + +Después probá que: +- `gdb ./demo` carga tu configuración; +- `gdb -nx ./demo` **no** la carga (y eso está bien, es lo que querés para depurar con settings + limpios); +- un `.gdbinit` con 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** + +```text +$ gdb -nx ./demo +(gdb) show print pretty +Pretty printing is off. +``` + +**Identificadores sugeridos** · `.gdbinit`, `locals`, `btfull`, `around` +**Restricciones/pistas** +- `alias` crea un alias de un **comando existente**; `define` crea un **comando nuevo** con + varias líneas. +- Dentro de un `define`, `$arg0`, `$arg1`, … son los argumentos que le pasaste. +- `~/.gdbinit` **no** 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 $HOME` en la terminal + de MSYS2), no el de Windows. + +**Criterios de aceptación** +- [ ] El `.gdbinit` tiene los cuatro bloques pedidos (presentación, 3 `define`, 1 `alias`). +- [ ] Los tres comandos propios **se ejecutan sin error** y se verificó su salida. +- [ ] Se comprobó la diferencia entre `gdb ./demo` y `gdb -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: + +1. Cada hilo incrementa un contador compartido en un bucle de 100.000 veces. +2. Cada hilo imprime su progreso cada 10.000 iteraciones. +3. El `main` espera a que terminen (con `pthread_join`) e imprime el total. + +Después: + +1. Corré el programa **sin** depurador: ¿el total es correcto? ¿ siempre? +2. En GDB, poné un breakpoint en la línea del incremento y usá `thread apply all bt` para ver los + dos hilos. +3. Usá `set scheduler-locking on` y observá que solo avanza el hilo actual. +4. Cambiá a `thread 2` y verificá que estás en **otro** frame, con **otras** variables locales. +5. **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** + +```text +(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 bt` **congela** el estado de todos los hilos en el mismo instante: por eso + sirve para ver un deadlock. +- `set scheduler-locking on` hace que los otros hilos no avancen mientras vos hacés `next`. + +**Criterios de aceptación** +- [ ] `info threads` distingue el hilo actual (con `*`) de los demás. +- [ ] `thread apply all bt` muestra el estado de **los dos** hilos. +- [ ] `set scheduler-locking on` se 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: + +1. entre en un bucle que duerme un poco y va incrementando un contador; +2. imprima el contador cada medio segundo, indefinidamente. + +Y hacé esto: + +1. Lanzalo en segundo plano (`./lento &`) y anotá su PID. +2. Conectate con `gdb -p `. +3. Verificá que el proceso **se frenó** (mirá con `ps` que su estado cambió). +4. Inspeccioná: `bt`, `info threads`, `info locals`, `info proc mappings`. +5. **Desconectate** con `detach` y verificá que el proceso **sigue vivo**. +6. Conectate de nuevo y probá `set {int} = ` para cambiar el contador en vivo. +7. 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** + +```text +$ ./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** +- `attach` **frena** el proceso. Si te parece que el servidor "se colgó" justo después de tu + `attach", es que lo frenaste vos. +- `detach` lo suelta y sigue. `quit` con "sí" a la pregunta de matar **lo mata**. +- Si `attach` falla con *"operation not permitted"*, es el `ptrace_scope` del 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 locals` e `info proc mappings` se usaron y su salida está + capturada. +- [ ] Tras el `detach`, el proceso **sigue corriendo** (verificado con `ps`). +- [ ] 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: + +1. ponga un breakpoint en `main`; +2. ejecute el programa; +3. imprima un separador y el **backtrace completo**; +4. imprima las **variables locales**; +5. imprima el **valor de los argumentos** de `main`; +6. genere un informe con `set logging` en un archivo `reporte.txt`; +7. termine solo (sin pedir confirmación). + +Y ejecutalo **de las dos formas**: + +```bash +$ 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** + +```text +$ 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 on` **debe** ir **después** de `set logging file`, y `set logging off` al final, + para que el archivo no quede abierto. +- `bt full` muestra los argumentos **de todos** los frames, no solo del actual. +- Con `python` podé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.txt` contiene el backtrace, las locales y los argumentos. +- [ ] Las dos formas de invocación funcionan y se comparan. +- [ ] `set confirm off` y `set logging on/off` está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: + +1. **Consume memoria sin parar** (el monitor de memoria del servidor se quejó). +2. **A veces se cuelga con `Segmentation fault`** (pero solo en la versión de producción). +3. **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: + +```text + ┌──────────────┐ + │ 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: + +```text +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): + +```c +#include +#include +#include + +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** + +```text +$ 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 -Wextra` del 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+C` interrumpe 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 (``), el código se reordena y + `next` salta. Depurá con `-O0 -g`. +- **Olvidar `-g` por completo.** GDB no tiene ni la tabla de líneas ni las variables locales: + solo podés parar en nombres de función. +- **Confundir `p` con `*p`.** `p p` muestra la **dirección**; `p *p` muestra el **valor**. La + mitad de los `Segfault` de principiante son esto. +- **`break` en 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 breakpoints` con el campo `What` es tu + mapa. Usá `commands` con `silent` para 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. +- **`p` de un `char[]` 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 total` muestra **otra** variable + `total`. Volvé a `frame 0` cuando dudes. +- **Olvidar `detach` después de un `attach`.** Salís de GDB y mataste el proceso que estabas + investigando. +- **`ulimit -c` en 0 y esperar un core.** El mensaje "core dumped" aparece igual, pero no se + genera archivo. +- **Confiar en `core` cuando el sistema usa `systemd-coredump`.** No hay archivo en tu carpeta: + usá `coredumpctl`. +- **`abort_on_error` sin `ASAN_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 + `run` lo vuelve a traer. +- **`step` en vez de `next` y perderse en la biblioteca estándar.** `next` no entra a las + funciones; `step` sí. Si te perdiste dentro de `printf`, usá `finish`. +- **Correr `run` mil 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 on` te 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 -O0` es la cerradura. Sabés por qué `-O2` rompe 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 con `tui 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 artificial + `p *arr@10`), **structs** (`p s`, `p s->campo`) y sabés que `p x` y `p *p` son cosas + distintas. +- Usás **breakpoints condicionales**, `ignore` y `commands` para 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 con `x/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 con + `thread apply all bt`, `fork` con `follow-fork-mode`, `attach` a procesos vivos y depuración + remota con `gdbserver`. + +**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 `09`** para entender la memoria dinámica que estás +> depurando, **`15`** para los archivos y **`12`** para 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. diff --git a/dist/20-gdb.pdf b/dist/20-gdb.pdf new file mode 100644 index 0000000..48d1031 Binary files /dev/null and b/dist/20-gdb.pdf differ