Files
2026-10-02 21:03:47 +02:00

132 lines
5.5 KiB
Markdown

# Depurar: *segfault* y gdb
## Qué es un *segmentation fault*
El programa accede a una dirección que no tiene mapeada en su espacio
virtual, o la usa sin permiso (por ejemplo, escribe en memoria de solo
lectura). La MMU no encuentra la traducción, el procesador avisa al núcleo, y
el núcleo envía al proceso la señal `SIGSEGV`, que por defecto lo mata (ver
[03-process-memory.md](03-process-memory.md)).
- La shell lo muestra como `Segmentation fault (core dumped)`.
- El código de salida es **139** = 128 + 11 (`SIGSEGV` es la señal 11). Es la
misma cuenta que el 137 de `SIGKILL` en el OOM killer.
- El caso más típico es escribir o leer a través de un puntero `NULL`. La
página de la dirección 0 nunca se mapea, precisamente para que este error se
detecte siempre.
Ojo: no todo acceso indebido da *segfault*. Si te sales de un bloque pero
caes en memoria que sí es del proceso, no pasa nada visible: corrompes datos
en silencio. Para eso están Valgrind y AddressSanitizer.
**En Go**: el mismo fallo da `panic: runtime error: invalid memory address or
nil pointer dereference`, con la traza de la pila. Go captura el `SIGSEGV` y lo
convierte en un `panic`. En C no hay traza: hay que pedírsela a gdb.
## `free(): invalid pointer` y *Aborted*
glibc comprueba algunas cosas al hacer `free`. Si el puntero no es el inicio de
un bloque que haya dado `malloc`, `calloc` o `realloc`, imprime
`free(): invalid pointer` y llama a `abort()`.
- La shell muestra `Aborted (core dumped)`, con código **134** = 128 + 6
(`SIGABRT`).
- Causas típicas: hacer `free` de la dirección de una variable local (`&x`),
de un literal (`"hola"`), de un puntero al **interior** de un bloque, o de un
bloque ya liberado (*double free*).
**Para depurarlo, mejor Valgrind que gdb.** gdb te dice dónde se cae (en el
`free`), pero el error está antes, donde el puntero recibió un valor malo.
Valgrind te dice **de dónde viene** la dirección:
```
Invalid free() / delete / delete[] / realloc()
at free
by escpos_buffer_free (buffer.c:36)
by main (hello.c:22)
Address 0x1ffefff544 is on thread 1's stack
```
"Is on thread 1's stack": la dirección es de **la pila**, así que nunca la dio
`malloc`. Después busca cada línea que asigna un valor a ese puntero
(`grep -n "data =" src/*.c`) y mira cuál le da una dirección de la pila.
### Método general
1. Lee el mensaje. Valgrind casi siempre dice qué pasó (`Invalid write`,
`Invalid free`), dónde y de qué tipo es la dirección (pila, N bytes después
de un bloque, bloque ya liberado...).
2. El síntoma (donde se cae) no suele ser la causa. Pregúntate de dónde salió
el valor malo.
3. Busca todas las asignaciones de esa variable y descarta una a una.
## gdb: lo mínimo
Hace falta compilar con `-g`. El Makefile ya lo hace.
```
gdb ./build/hello
```
| Comando de gdb | Qué hace |
|---|---|
| `run` (`r`) | Ejecuta el programa. Si se cuelga, gdb se para en la línea exacta |
| `bt` (*backtrace*) | Muestra la pila de llamadas: quién llamó a quién hasta llegar aquí |
| `print expr` (`p`) | Muestra el valor de una variable o expresión: `p buffer->data` |
| `break fichero.c:línea` (`b`) | Pone un punto de parada |
| `next` (`n`) | Ejecuta la línea actual sin entrar en las funciones |
| `step` (`s`) | Ejecuta la línea actual entrando en las funciones |
| `continue` (`c`) | Sigue hasta el siguiente punto de parada |
| `quit` (`q`) | Sale |
### Programa que no termina (bucle infinito)
1. `gdb ./build/hello` y `run`.
2. Cuando lleve un rato colgado, pulsa **Ctrl+C**: gdb detiene el programa
donde esté y te muestra la línea.
3. `bt` para ver en qué función está, y `print` de las variables del bucle
(`print new_cap`). Si al repetir `next` varias veces la variable no cambia,
has encontrado el problema.
Ejemplo real: un bucle `while (new_cap < min_cap) { new_cap *= 2; }` con
`new_cap` empezando en 0. Cero por dos es cero, así que el bucle no termina
nunca. `print new_cap` da `0` en cada vuelta.
Al arrancar, Fedora pregunta si quieres descargar símbolos de depuración del
sistema (*debuginfod*). Para nuestro código no hacen falta: responde `n`.
**En Go** el equivalente es Delve (`dlv debug`), con comandos casi iguales.
## Depurar en VS Code
La extensión C/C++ (`ms-vscode.cpptools`) lanza **el mismo gdb** y le envía
órdenes por su protocolo de texto (gdb/MI). Cada clic equivale a un comando:
| VS Code | gdb |
|---|---|
| Clic a la izquierda del número de línea (punto rojo) | `break fichero.c:línea` |
| F5 | `run` / `continue` |
| F10 | `next` |
| F11 | `step` |
| Shift+F11 | `finish` (salir de la función actual) |
| Panel *Variables*, pasar el ratón por encima o panel *Watch* | `print` |
| Panel *Call Stack* | `bt` |
| Botón de pausa | Ctrl+C |
Configuración del proyecto, en `.vscode/`:
- `tasks.json`: tareas `make`, `make clean`, `make (clang)` y `valgrind`.
Se lanzan con *Terminal → Run Task*, y Ctrl+Shift+B ejecuta `make`.
- `launch.json`: depura `build/hello` con gdb y ejecuta `make` antes. Tiene ya
los valores para Windows (MSYS2 UCRT64).
- `c_cpp_properties.json`: dice a IntelliSense dónde están las cabeceras
(`include/`) y que el estándar es C17, para que el editor avise de lo mismo
que gcc.
- `settings.json`: salto de línea final, sin espacios al final de línea, y
tabuladores reales en el Makefile.
Para ver un puntero como array en el panel *Watch*: `*buffer->data@10` (los 10
primeros bytes de `data`). Es sintaxis de gdb y funciona igual en su consola.
**En Go**, la extensión de Go hace lo mismo con Delve.