534 lines
23 KiB
Markdown
534 lines
23 KiB
Markdown
# Memoria dinámica: `malloc` y `free`
|
||
|
||
## Pila y montón
|
||
|
||
Un proceso tiene dos sitios principales donde guardar datos mientras se
|
||
ejecuta:
|
||
|
||
| | Pila (*stack*) | Montón (*heap*) |
|
||
|---|---|---|
|
||
| Qué va ahí | Variables locales de una función | Lo que pides con `malloc` |
|
||
| Cuándo se libera | Sola, al salir de la función | Cuando llamas a `free` |
|
||
| Tamaño | Fijo y pequeño (unos 8 MB en Linux) | Grande, crece bajo demanda |
|
||
| Coste | Casi gratis | Llamada a función. A veces, llamada al sistema |
|
||
|
||
El problema de la pila: al salir de la función, sus variables dejan de existir.
|
||
Si devuelves la dirección de una variable local, el que llama recibe un puntero
|
||
a memoria que ya no es suya (*dangling pointer*). Leerlo es **comportamiento
|
||
indefinido**: puede parecer que funciona, dar basura o colgar el programa.
|
||
|
||
### Dónde están físicamente
|
||
|
||
En la **misma RAM**. La pila no está en un chip especial ni el montón en
|
||
otro. La diferencia es de organización, no física:
|
||
|
||
- Cada proceso ve un **espacio de direcciones virtual** propio (ver
|
||
[03-process-memory.md](03-process-memory.md)). Dentro de ese espacio, el
|
||
sistema operativo coloca cada zona en un rango de direcciones distinto.
|
||
- La MMU traduce cada página virtual (4 KB) a un **marco físico cualquiera** de
|
||
la RAM. Páginas contiguas de la pila pueden estar en marcos dispersos, e
|
||
incluso en disco (*swap*) si falta memoria.
|
||
|
||
Disposición típica en Linux x86-64, de direcciones bajas a altas:
|
||
|
||
```
|
||
0x0000... código del programa (.text) r-x
|
||
datos globales (.data, .bss) rw-
|
||
[heap] ↓ crece hacia arriba rw- (malloc, brk)
|
||
... librerías y mmap ... (malloc grandes)
|
||
[stack] ↑ crece hacia abajo rw- (8 MB máx., ulimit -s)
|
||
0x7fff...
|
||
```
|
||
|
||
Se ve en vivo: `cat /proc/self/maps` muestra el mapa del propio `cat`, con
|
||
las líneas `[heap]` y `[stack]`. Para un proceso en marcha:
|
||
`cat /proc/<pid>/maps`.
|
||
|
||
**`[heap]` no es todo el montón.** La línea `[heap]` del mapa es solo la
|
||
zona clásica que `malloc` hace crecer con la llamada al sistema `brk`. Los
|
||
bloques grandes (más de 128 KB en glibc) los pide `malloc` con `mmap`, y
|
||
aparecen como zonas sin nombre. Chromium, V8 y el runtime de Go usan sus
|
||
propios gestores sobre `mmap` y pueden no tener `[heap]` en absoluto.
|
||
Comprobado con el proceso principal de VS Code: tiene `[stack]` pero no
|
||
`[heap]`.
|
||
|
||
### Ejemplo: dónde vive cada parte del buffer
|
||
|
||
Medido con gdb en `hello` (Fedora, 2026-10-06), en el punto de parada de
|
||
`main`:
|
||
|
||
| Qué | Expresión en gdb | Dirección | Zona |
|
||
|---|---|---|---|
|
||
| El papel: la variable local `buffer` de `main` | `p &buffer` | `0x7fffffffd520` | `[stack]` |
|
||
| La casa: el struct de 24 bytes | `p buffer` | `0x4052b0` | `[heap]` |
|
||
| Los bytes: el bloque de 128 | `p buffer->data` | `0x4052d0` | `[heap]` |
|
||
|
||
- Son **tres sitios**: el puntero está en la pila, y los dos bloques de
|
||
`malloc`/`realloc` están en el montón, separados entre sí.
|
||
- Los campos del struct sí van juntos y en orden: `&buffer->len` es
|
||
`0x4052b8`, 8 bytes después de `data`.
|
||
- El struct y los datos están a 32 bytes y no a 24: `malloc` redondea el tamaño
|
||
y guarda delante de cada bloque una cabecera con información para `free`.
|
||
- `info proc mappings` en gdb (o `/proc/<pid>/maps`) dice a qué zona pertenece
|
||
cada dirección.
|
||
|
||
**En Go** la variable `b := &Buffer{}` también puede vivir en la pila y el
|
||
struct en el montón, pero lo decide el compilador (*escape analysis*) y no lo
|
||
ves.
|
||
|
||
### Memoria virtual frente a memoria real
|
||
|
||
`/proc/<pid>/status` muestra dos cifras:
|
||
|
||
- `VmSize`: direcciones virtuales reservadas. Reservar direcciones no gasta
|
||
RAM.
|
||
- `VmRSS` (*Resident Set Size*): páginas que están de verdad en RAM porque
|
||
alguien ha escrito en ellas. Es lo que cuenta para el OOM killer y para el
|
||
límite de memoria de un contenedor.
|
||
|
||
Ejemplo real con el proceso principal de VS Code:
|
||
|
||
```
|
||
VmSize: 1519018808 kB ~1,4 TB
|
||
VmRSS: 293276 kB ~286 MB
|
||
```
|
||
|
||
V8 reserva un rango enorme de direcciones (su *sandbox*) para que los objetos
|
||
de JavaScript no puedan apuntar fuera de él. En x86-64 cada proceso tiene
|
||
128 TB de espacio virtual, así que 1,4 TB es un 1 %.
|
||
|
||
**Por qué la pila es tan rápida** si está en la misma RAM:
|
||
|
||
- Reservar en la pila es restar un número a un registro (`rsp`, el *stack
|
||
pointer*): una instrucción. Al salir de la función se suma de vuelta.
|
||
- Esas páginas se usan constantemente, así que están en la caché de la CPU.
|
||
- `malloc` en cambio tiene que buscar un hueco libre en sus listas, y a veces
|
||
pedir más memoria al núcleo con una llamada al sistema (`brk` o `mmap`).
|
||
|
||
**En Go**: la pila de cada goroutine **no** es la pila del sistema operativo.
|
||
El runtime la reserva en su propio montón: empieza pequeña (unos KB) y la
|
||
copia a un bloque mayor cuando se queda corta. Por eso puedes tener millones de
|
||
goroutines y en C no puedes tener millones de hilos.
|
||
|
||
## Qué hacía Go por mí
|
||
|
||
En Go puedes escribir esto sin problema:
|
||
|
||
```go
|
||
func newBuffer() *Buffer {
|
||
b := Buffer{}
|
||
return &b
|
||
}
|
||
```
|
||
|
||
El compilador hace *escape analysis*: ve que `b` sobrevive a la función y la
|
||
coloca en el montón en lugar de la pila. Luego el recolector de basura (GC) la
|
||
libera cuando nadie la referencia. Se ve con `go build -gcflags=-m`
|
||
(`moved to heap: b`).
|
||
|
||
En C nada de eso existe. **Tú decides** dónde va cada dato y **tú lo liberas**.
|
||
|
||
## `malloc` y `free`
|
||
|
||
Se declaran en `<stdlib.h>`.
|
||
|
||
- `malloc(n)` reserva `n` bytes en el montón y devuelve un puntero al primero.
|
||
- El contenido **no está inicializado**: es basura. En Go, `new` y `make`
|
||
ponen todo a cero; aquí no.
|
||
- Si no hay memoria, devuelve `NULL`. **Hay que comprobarlo siempre.**
|
||
- Devuelve `void *`, un puntero "a cualquier cosa". En C se convierte solo al
|
||
tipo de puntero que lo recibe, sin *cast*.
|
||
- `free(p)` devuelve esa memoria.
|
||
- Después, `p` sigue apuntando a la misma dirección, pero ya no es tuya. Usarla
|
||
(*use after free*) es comportamiento indefinido.
|
||
- Liberar dos veces lo mismo (*double free*) también lo es.
|
||
- `free(NULL)` no hace nada. Es seguro.
|
||
- `calloc(count, size)` es como `malloc`, pero pone a cero la memoria.
|
||
|
||
Forma idiomática de reservar un `struct`:
|
||
|
||
```c
|
||
struct escpos_buffer *buf = malloc(sizeof *buf);
|
||
```
|
||
|
||
`sizeof *buf` es "el tamaño de aquello a lo que apunta `buf`". Se calcula al
|
||
compilar, no lee nada en ejecución. Así, si cambia el tipo de `buf`, el tamaño
|
||
cambia solo.
|
||
|
||
## `realloc`: cambiar el tamaño de un bloque
|
||
|
||
`realloc(p, n)` cambia el tamaño del bloque `p` a `n` bytes y devuelve un
|
||
puntero al bloque redimensionado:
|
||
|
||
- Si hay sitio justo detrás, lo agranda **en el mismo sitio** y devuelve la
|
||
misma dirección.
|
||
- Si no, reserva un bloque nuevo, **copia** el contenido, libera el viejo y
|
||
devuelve la dirección **nueva**. Cualquier otro puntero al bloque viejo queda
|
||
colgando.
|
||
- `realloc(NULL, n)` equivale a `malloc(n)`. Por eso un buffer vacío puede
|
||
empezar con `data = NULL`.
|
||
- Si falla, devuelve `NULL` y **el bloque viejo sigue intacto y sigue siendo
|
||
tuyo**.
|
||
|
||
**Qué bloque se redimensiona.** En un buffer hay dos bloques:
|
||
|
||
```
|
||
buffer ──▶ [ data | len | cap ] struct: 24 bytes fijos, no crece nunca
|
||
│
|
||
└──▶ [ A A A A ... ] bytes: este es el que crece
|
||
```
|
||
|
||
Se hace `realloc` de `buffer->data`, nunca de `buffer`. Hacer `realloc` del
|
||
struct puede moverlo a otra dirección y liberar el viejo, y entonces el
|
||
puntero `buffer` que tiene el que llama queda colgando.
|
||
|
||
El error clásico:
|
||
|
||
```c
|
||
p = realloc(p, n); /* MAL: si falla, p pasa a NULL y el bloque viejo se pierde (fuga) */
|
||
```
|
||
|
||
Lo correcto es guardar el resultado en una variable temporal, comprobarlo y
|
||
solo entonces asignarlo.
|
||
|
||
**En Go** es exactamente lo que hace `append` por dentro. Por eso hay que
|
||
escribir `s = append(s, x)`: si el array subyacente se movió, el slice viejo
|
||
apunta al sitio antiguo.
|
||
|
||
**Colgante no es lo mismo que perdido.** Tras un `realloc` que mueve el bloque,
|
||
el puntero viejo no cambia: sigue con la dirección antigua, pero esa memoria ya
|
||
está liberada. Es un puntero colgante, y usarlo es comportamiento indefinido.
|
||
"Perdido" es lo contrario, una fuga: memoria reservada sin ningún puntero.
|
||
|
||
| | C (`realloc`) | Go (`append`) |
|
||
|---|---|---|
|
||
| ¿Copia al bloque nuevo? | Sí | Sí |
|
||
| ¿Libera el bloque viejo? | Sí, en el acto | No; el GC lo mantiene vivo mientras algo lo referencie |
|
||
| Puntero/slice viejo | Colgante: comportamiento indefinido | Válido pero desconectado: ya no ve los cambios |
|
||
| Síntoma del fallo | Cuelgue o corrupción del montón | Datos incorrectos, sin ningún error |
|
||
|
||
```go
|
||
a := make([]byte, 3) // len 3, cap 3: lleno
|
||
b := a // b comparte el array con a
|
||
a = append(a, 'X') // no cabe: nuevo array
|
||
a[0] = 'Z'
|
||
fmt.Println(b[0]) // 0: b sigue en el array viejo
|
||
```
|
||
|
||
Si `a` hubiera tenido capacidad libre, `append` no movería nada y `b[0]` valdría
|
||
`'Z'`. El resultado depende de `cap`.
|
||
|
||
**Qué hace `bytes.Buffer` por debajo.** Es el mismo algoritmo que
|
||
`escpos_buffer_append_byte`: 64 bytes iniciales (`smallBufferSize`) y duplicar
|
||
al llenarse. Diferencias: nunca crece en el sitio (siempre reserva y copia),
|
||
pone la memoria a cero (`malloc` no), redondea la capacidad al tamaño de clase
|
||
del asignador de Go, comprueba los índices y, si falla, hace `panic` en vez de
|
||
devolver un error. El struct `bytes.Buffer` puede vivir en la pila si no
|
||
escapa; solo los datos van al montón.
|
||
|
||
### Estrategia de crecimiento
|
||
|
||
Si el buffer creciera de byte en byte, cada byte nuevo podría provocar una
|
||
copia de todo lo anterior: añadir `n` bytes costaría del orden de `n²`. La
|
||
solución es **duplicar la capacidad** cada vez que se llena. Así hay pocas
|
||
copias, y el coste medio de añadir un byte es constante (*coste amortizado
|
||
O(1)*).
|
||
|
||
`append` en Go duplica hasta 256 elementos y a partir de ahí crece más despacio
|
||
(unos 1,25×).
|
||
|
||
## `memcpy`: copiar bloques de bytes
|
||
|
||
`memcpy(dst, src, n)` (en `<string.h>`) copia `n` bytes desde la dirección
|
||
`src` a la dirección `dst`. Su firma:
|
||
|
||
```c
|
||
void *memcpy(void *dst, const void *src, size_t n);
|
||
```
|
||
|
||
| Parte | Qué significa |
|
||
|---|---|
|
||
| `void *dst` | Dirección de destino. `void *` es "puntero a cualquier cosa": acepta un `uint8_t *`, un `int *` o un `struct x *` sin *cast* |
|
||
| `const void *src` | Dirección de origen. El `const` es la promesa: `memcpy` no modifica tus datos |
|
||
| `size_t n` | Cuántos **bytes** copiar. Se pasa aparte porque un puntero no sabe cuántos hay detrás |
|
||
| devuelve `void *` | El mismo `dst`. Casi nunca se usa |
|
||
|
||
Fíjate en que su firma resuelve el mismo problema que cualquier función que
|
||
recibe un bloque de datos: puntero, cantidad y `const` en lo que solo se lee.
|
||
|
||
`n` va en **bytes**, no en elementos. Con `uint8_t` coincide. Para copiar 10
|
||
`int`, `n` es `10 * sizeof(int)`, o sea, 40.
|
||
|
||
Comparado con el `copy(dst, src)` de Go:
|
||
|
||
- No sabe nada de tamaños: copia exactamente `n` bytes, quepan o no. Si en
|
||
`dst` no hay `n` bytes reservados, escribe fuera del bloque.
|
||
- Los dos bloques **no pueden solaparse**. Si se solapan, se usa `memmove`.
|
||
- Go copia `min(len(dst), len(src))` y devuelve cuántos copió: nunca se sale.
|
||
`memcpy` no puede comprobar nada porque no conoce los tamaños.
|
||
|
||
### `restrict`: la promesa de que no se solapan
|
||
|
||
Desde C99 la firma real lleva `restrict`:
|
||
|
||
```c
|
||
void *memcpy(void *restrict dst, const void *restrict src, size_t n);
|
||
```
|
||
|
||
`restrict` en un puntero es una **promesa del programador al compilador**:
|
||
mientras dura la función, a la memoria que se toca por ese puntero no se accede
|
||
por ningún otro. Aquí quiere decir que `dst` y `src` no se solapan.
|
||
|
||
- **Para qué sirve:** sabiendo que no se solapan, el compilador puede copiar en
|
||
bloques de 16 o 32 bytes, reordenar y no volver a leer `src` después de cada
|
||
escritura en `dst`. Sin la promesa, tendría que suponer que escribir en `dst`
|
||
puede cambiar `src`.
|
||
- **Quién lo comprueba:** nadie. Si rompes la promesa (con bloques que se
|
||
solapan), es comportamiento indefinido. Para bloques solapados está `memmove`,
|
||
que no lleva `restrict`.
|
||
- **Para quien llama no cambia nada:** se llama igual que en C89.
|
||
|
||
`man 3 memcpy` en Fedora lo escribe como `void dest[restrict .n]`. Es una
|
||
notación de la página de manual para decir que el bloque mide `n`. No es C
|
||
válido.
|
||
|
||
Versión del estándar: el `Makefile` compila con `-std=c17`, así que
|
||
`restrict` existe. Para comprobar qué estándar usa el compilador:
|
||
`echo | gcc -std=c17 -dM -E - | grep __STDC_VERSION__` da `201710L` (C17). Sin
|
||
`-std`, gcc 15 usa C23 (`202311L`).
|
||
|
||
**En Go** no existe. El compilador tiene que suponer lo peor cuando dos slices
|
||
pueden compartir memoria, y `copy` funciona aunque se solapen, como `memmove`.
|
||
|
||
**Por qué usarlo en vez de un bucle:** glibc implementa `memcpy` en ensamblador
|
||
optimizado, con instrucciones que copian 16 o 32 bytes de una vez (SIMD). A
|
||
veces gcc incluso convierte un bucle de copia en una llamada a `memcpy`. Y
|
||
además dice lo que haces en una sola línea.
|
||
|
||
La documentación de cualquier función de la librería de C se consulta con
|
||
`man 3 memcpy`. El 3 es la sección de funciones de librería; la 2 es la de
|
||
llamadas al sistema.
|
||
|
||
Para copiar al final del buffer hace falta la dirección de la casilla `len`:
|
||
|
||
```c
|
||
&buffer->data[buffer->len] /* "la dirección de la casilla len" */
|
||
buffer->data + buffer->len /* lo mismo: aritmética de punteros */
|
||
```
|
||
|
||
Sumar un número a un puntero avanza ese número de **elementos** (no de bytes;
|
||
aquí coincide porque cada elemento es un `uint8_t`).
|
||
|
||
### Desbordamiento al calcular tamaños
|
||
|
||
`len + count` o `cap * 2` pueden pasar del máximo de `size_t` y dar la vuelta
|
||
a un número pequeño. Entonces `realloc` reserva poco y `memcpy` escribe fuera.
|
||
Con datos que vienen de fuera (por ejemplo, desde Go mediante cgo) hay que
|
||
comprobarlo antes de sumar: `if (count > SIZE_MAX - len)`. `SIZE_MAX` está en
|
||
`<stdint.h>`.
|
||
|
||
## Fugas de memoria
|
||
|
||
Si pierdes el último puntero a un bloque sin hacer `free`, ese bloque queda
|
||
reservado hasta que acabe el proceso: es una **fuga** (*memory leak*). En un
|
||
programa que dura poco no se nota. En un servicio que imprime tiques todo el
|
||
día, la memoria crece hasta que el sistema lo mata.
|
||
|
||
**En Go** el GC lo evita. Las fugas que hay en Go son de otro tipo: goroutines
|
||
bloqueadas o referencias que guardas sin querer.
|
||
|
||
### Qué pasa cuando se acaba la memoria
|
||
|
||
**Linux: *overcommit* y OOM killer.**
|
||
|
||
- `malloc` casi nunca devuelve `NULL` en Linux. El núcleo solo reserva
|
||
**direcciones virtuales**, y promete más memoria de la que tiene
|
||
(*overcommit*). La RAM física se asigna página a página, **la primera vez
|
||
que escribes** en ella, mediante un fallo de página.
|
||
- Si llega un momento en que alguien escribe y no queda RAM ni *swap*, el núcleo
|
||
no puede cumplir la promesa. Entonces actúa el **OOM killer** (*Out Of
|
||
Memory*): elige el proceso con mayor `oom_score`, que es básicamente el que
|
||
más memoria usa, y le envía `SIGKILL`.
|
||
- `SIGKILL` no se puede capturar ni ignorar. El proceso muere en el acto, sin
|
||
`defer`, sin cerrar ficheros y sin dejar un error en su propio log.
|
||
- Queda rastro en el log del núcleo: `journalctl -k | grep -i "out of memory"`
|
||
(`Killed process 1234 (hello) ...`).
|
||
- Fedora tiene además `systemd-oomd`, que actúa antes, cuando detecta presión
|
||
de memoria sostenida, y mata grupos de procesos enteros.
|
||
- **Contenedores**: un límite de memoria de Docker o Kubernetes es un *cgroup*.
|
||
Al superarlo actúa el mismo OOM killer, pero solo dentro del contenedor. Es
|
||
el `OOMKilled` de Kubernetes (código de salida 137 = 128 + 9, la señal
|
||
`SIGKILL`).
|
||
|
||
**Windows** no tiene OOM killer. Lleva la cuenta de la memoria prometida
|
||
(*commit charge*) frente a RAM + fichero de paginación. Si se supera, `malloc`
|
||
**sí devuelve `NULL`**.
|
||
|
||
**Android** tiene `lmkd` (*low memory killer*), que mata primero las apps en
|
||
segundo plano.
|
||
|
||
Por eso hay que comprobar `NULL` aunque en Linux casi nunca salga: en Windows
|
||
sí ocurre, también con límites como `ulimit -v`, y con peticiones absurdas
|
||
(por ejemplo, un tamaño calculado mal que da varios exabytes).
|
||
|
||
**En Go** pasa lo mismo. Si el runtime no consigue memoria, el programa muere
|
||
con `fatal error: runtime: out of memory`, que no se puede recuperar con
|
||
`recover`. Y si lo mata el OOM killer, ni siquiera eso. `GOMEMLIMIT` le dice al
|
||
GC que trabaje más cuando se acerca a un límite, para no llegar ahí.
|
||
|
||
## Propiedad (*ownership*)
|
||
|
||
Como no hay GC, cada puntero tiene un **dueño**: el código responsable de
|
||
liberarlo. La API tiene que dejarlo claro. La convención del proyecto es
|
||
documentar en la cabecera, en cada función, quién reserva y quién libera.
|
||
|
||
El patrón típico es una pareja de funciones:
|
||
|
||
- `x_new()` reserva y devuelve el puntero. El que llama pasa a ser el dueño.
|
||
- `x_free(p)` libera. Después de llamarla, el que llama no debe usar `p`.
|
||
|
||
Es parecido a `defer f.Close()` en Go: un recurso que tienes que devolver tú.
|
||
La diferencia es que en C también la memoria es un recurso que se devuelve a
|
||
mano.
|
||
|
||
**El dueño es quien tiene la obligación de liberar.** No es quien reservó la
|
||
memoria, sino quien debe acabar llamando a `free`. Esa obligación puede pasar
|
||
de una función a otra, y por eso hay que escribirla en la cabecera: el tipo
|
||
`escpos_buffer *` no dice nada sobre quién libera.
|
||
|
||
Dos funciones de la API que devuelven punteros con dueños distintos:
|
||
|
||
| Función | Quién reserva | Dueño | Qué debe hacer el que llama |
|
||
|---|---|---|---|
|
||
| `escpos_buffer_new()` | la librería, con `malloc` | **el que llama** (se le traspasa) | llamar a `escpos_buffer_free` una vez, y no usar el puntero después |
|
||
| `escpos_version()` | nadie: es un literal de cadena en memoria de solo lectura | **la librería** (vive mientras vive el programa) | solo leerlo. Si hace `free`, el comportamiento es indefinido (normalmente el programa aborta) |
|
||
|
||
Mirando solo las firmas, ambas "devuelven un puntero". El comentario es lo que
|
||
dice qué hacer con cada una.
|
||
|
||
**Con cgo** (fase 12) importa todavía más: el GC de Go no ve la memoria de C.
|
||
El código Go que llame a `escpos_buffer_new` tendrá que hacer
|
||
`defer C.escpos_buffer_free(b)`, igual que con un fichero.
|
||
|
||
## Liberar en los caminos de error: C no tiene `defer`
|
||
|
||
En Go, `defer b.Close()` se ejecuta salga la función por donde salga. En C no
|
||
existe nada así: cada `return` tiene que dejar liberado todo lo que se creó
|
||
antes. Con varios recursos, repetir los `free` delante de cada `return` es
|
||
largo y es fácil olvidarse de alguno.
|
||
|
||
El patrón clásico de C es **un único punto de limpieza al final**, al que se
|
||
salta con `goto`:
|
||
|
||
```c
|
||
int run(void)
|
||
{
|
||
int result = 1; /* se supone fallo hasta llegar al final */
|
||
thing *a = NULL; /* todo a NULL desde el principio */
|
||
thing *b = NULL;
|
||
|
||
a = thing_new();
|
||
if (a == NULL) { goto cleanup; }
|
||
|
||
b = thing_new();
|
||
if (b == NULL) { goto cleanup; }
|
||
|
||
/* ... trabajo ... */
|
||
|
||
result = 0; /* todo ha ido bien */
|
||
|
||
cleanup: /* etiqueta: destino del goto */
|
||
thing_free(b); /* seguro aunque sea NULL */
|
||
thing_free(a);
|
||
return result;
|
||
}
|
||
```
|
||
|
||
**Variables locales sin inicializar = basura.** En Go toda variable nace con
|
||
su valor cero (`nil`, `0`, `""`). En C, una variable local que no inicializas
|
||
contiene lo que hubiera antes en esa posición de la pila. Si un `goto` salta
|
||
por encima de la línea `escpos_buffer *b = escpos_buffer_new();`, esa
|
||
asignación no se ejecuta y `b` vale cualquier cosa. Pasarla a `free` es
|
||
comportamiento indefinido. (Las variables **globales** y `static` sí empiezan a
|
||
cero.)
|
||
|
||
Por qué funciona:
|
||
|
||
- Todos los punteros empiezan en `NULL`, y la función de liberar acepta `NULL`
|
||
sin hacer nada. Por eso `escpos_buffer_free` se diseñó así. Da igual en qué
|
||
punto falle: la limpieza libera lo que exista e ignora lo demás.
|
||
- Hay un solo `return`. Al añadir un recurso nuevo, se añade su `free` en un
|
||
solo sitio.
|
||
- Se libera en orden inverso al de creación, como hace `defer` (LIFO).
|
||
|
||
`goto` tiene mala fama por el código "espagueti", pero este uso, saltar
|
||
**hacia delante** a la limpieza, es idiomático. El núcleo de Linux lo usa por
|
||
todas partes.
|
||
|
||
gcc y clang tienen una extensión, `__attribute__((cleanup(f)))`, que se
|
||
parece a `defer`. No es C estándar y no funciona en MSVC, así que en este
|
||
proyecto no se usa.
|
||
|
||
## Valgrind
|
||
|
||
`valgrind` ejecuta tu programa en una CPU simulada y vigila cada acceso a
|
||
memoria. Detecta fugas, lecturas de memoria sin inicializar, *use after free*
|
||
y accesos fuera de un bloque. El programa va unas 20-50 veces más lento.
|
||
|
||
```
|
||
valgrind --leak-check=full ./build/hello
|
||
```
|
||
|
||
Al final del informe:
|
||
|
||
- `All heap blocks were freed -- no leaks are possible`: todo bien.
|
||
- `definitely lost: N bytes in M blocks`: fuga. Con `-g`, Valgrind indica
|
||
la línea del `malloc` que nunca se liberó.
|
||
|
||
Solo funciona en Linux. Por eso la parte portable se prueba en Fedora.
|
||
|
||
### Cómo leer un informe de fuga
|
||
|
||
Informe real, quitando el `escpos_buffer_free` de `hello.c`:
|
||
|
||
```
|
||
HEAP SUMMARY:
|
||
in use at exit: 24 bytes in 1 blocks
|
||
total heap usage: 2 allocs, 1 frees, 4,120 bytes allocated
|
||
|
||
24 bytes in 1 blocks are definitely lost in loss record 1 of 1
|
||
at 0x483FB26: malloc (vg_replace_malloc.c:447)
|
||
by 0x4004DD: escpos_buffer_new (buffer.c:16)
|
||
by 0x4004A7: main (hello.c:10)
|
||
```
|
||
|
||
- **`2 allocs`**: una es nuestra (24 bytes: el struct). La otra (4.096 bytes)
|
||
es el búfer interno que crea `printf` para la salida estándar, que la
|
||
librería de C libera al terminar. Valgrind vigila todo el proceso, no solo
|
||
tu código.
|
||
- **La pila de llamadas se lee de abajo arriba**: `main` (línea 10 de
|
||
`hello.c`) llamó a `escpos_buffer_new`, que en la línea 16 de `buffer.c`
|
||
llamó a `malloc`. Los ficheros y las líneas salen gracias a `-g`.
|
||
- Valgrind dice **dónde se reservó** el bloque perdido, no dónde faltó el
|
||
`free`. Eso lo deduces tú: ¿quién era el dueño de ese puntero?
|
||
|
||
**Cómo sabe Valgrind que es una fuga.** Al terminar el programa, recorre toda
|
||
la memoria que todavía es accesible (pila, variables globales, registros y los
|
||
demás bloques del montón) buscando algún valor que sea la dirección del bloque.
|
||
Si no encuentra ninguno, ningún código podría ya hacer `free` de ese bloque: la
|
||
fuga es **segura**, no una sospecha. Es la fase de marcado del GC de Go: el GC
|
||
libera lo que no alcanza, y Valgrind lo denuncia.
|
||
|
||
En el ejemplo, el único puntero era la variable local `buffer` de `main`, que
|
||
estaba en la pila. Cuando `main` terminó, esa variable desapareció y con ella
|
||
la última referencia al bloque.
|
||
|
||
Tipos de pérdida en `LEAK SUMMARY`:
|
||
|
||
| Tipo | Significado |
|
||
|---|---|
|
||
| `definitely lost` | Ningún puntero apunta ya al bloque. Fuga segura |
|
||
| `indirectly lost` | Solo se llega al bloque desde otro bloque perdido. Por ejemplo, el `data` de un buffer cuyo struct se perdió |
|
||
| `possibly lost` | Solo hay punteros al interior del bloque, no a su inicio |
|
||
| `still reachable` | Al salir aún había un puntero (por ejemplo, una variable global). No es una fuga, pero nadie lo liberó |
|