# 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//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]`. ### Memoria virtual frente a memoria real `/proc//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 ``. - `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 ``) 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. **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 ``. ## 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; } ``` 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ó |