132 lines
5.5 KiB
Markdown
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.
|