malloc, free, append
This commit is contained in:
@@ -4,17 +4,17 @@ CPPFLAGS = -Iinclude
|
|||||||
|
|
||||||
all: build/hello
|
all: build/hello
|
||||||
|
|
||||||
build/escpos.o: src/escpos.c include/escpos.h
|
build/%.o: src/%.c include/escpos.h
|
||||||
mkdir -p build/
|
mkdir -p build/
|
||||||
$(CC) $(CFLAGS) $(CPPFLAGS) -c src/escpos.c -o build/escpos.o
|
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
|
||||||
|
|
||||||
build/hello.o: examples/hello.c include/escpos.h
|
build/hello.o: examples/hello.c include/escpos.h
|
||||||
mkdir -p build/
|
mkdir -p build/
|
||||||
$(CC) $(CFLAGS) $(CPPFLAGS) -c examples/hello.c -o build/hello.o
|
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
|
||||||
|
|
||||||
build/hello: build/hello.o build/escpos.o
|
build/hello: build/hello.o build/escpos.o build/buffer.o
|
||||||
mkdir -p build/
|
mkdir -p build/
|
||||||
$(CC) build/hello.o build/escpos.o -o build/hello
|
$(CC) $^ -o $@
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf build
|
rm -rf build
|
||||||
|
|||||||
+10
-3
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
## Estado actual
|
## Estado actual
|
||||||
|
|
||||||
- **Fase 1** — Estructura del proyecto.
|
- **Fase 2** — Buffer dinámico. (Fase 1 cerrada en Fedora; falta probar en Windows.)
|
||||||
- Paso actual: Makefile. Hecho: reglas con dependencias correctas, variables CC/CFLAGS/CPPFLAGS, clean, .PHONY, cero warnings con gcc y clang. Siguiente: .gitignore y carpeta build/ en un clon limpio; después reglas patrón.
|
- Paso actual: 2.2, escpos_buffer_append_byte con realloc y duplicado de capacidad.
|
||||||
- Entorno activo: Fedora 42 nativo (gcc 15.2, clang 20, make 4.4, gdb 17,
|
- Entorno activo: Fedora 42 nativo (gcc 15.2, clang 20, make 4.4, gdb 17,
|
||||||
valgrind 3.26).
|
valgrind 3.26).
|
||||||
|
|
||||||
@@ -20,13 +20,20 @@
|
|||||||
`static` para encapsular, `nm` y errores del enlazador.
|
`static` para encapsular, `nm` y errores del enlazador.
|
||||||
- Cadenas terminadas en `'\0'`, `const char *`.
|
- Cadenas terminadas en `'\0'`, `const char *`.
|
||||||
- Memoria virtual de un proceso, modo usuario y modo núcleo.
|
- Memoria virtual de un proceso, modo usuario y modo núcleo.
|
||||||
|
- Make: reglas, dependencias por fechas, variables, regla patrón con $@ $< $^, .PHONY.
|
||||||
|
- Pila y montón, malloc/free, NULL, propiedad de punteros, tipo opaco, typedef, `->`, uint8_t/size_t, Valgrind.
|
||||||
|
|
||||||
## Decisiones de diseño
|
## Decisiones de diseño
|
||||||
|
|
||||||
- Prefijo `escpos_` para toda la API pública.
|
- Prefijo `escpos_` para toda la API pública.
|
||||||
|
- `escpos_buffer` es un tipo opaco: declarado en `escpos.h`, definido en `src/buffer.c`. Campos `uint8_t *data`, `size_t len`, `size_t cap`.
|
||||||
|
- Estilo: nombres en `snake_case` y llave de apertura en línea aparte (estilo Allman) en funciones, structs y bloques. Aplicarlo a todo el proyecto, también a `escpos.c`.
|
||||||
|
- Lo generado va en `build/`, que se crea en las recetas con `mkdir -p` y no se versiona.
|
||||||
|
|
||||||
## Pendiente / puntos débiles
|
## Pendiente / puntos débiles
|
||||||
|
|
||||||
- Hay binarios (`.o`, `.exe`) versionados en git. Falta `.gitignore`.
|
- `.gitignore` creado. Siguen versionados `emulator/escpos-emu.exe` y `goruntime/goruntime.exe`.
|
||||||
- Ficheros sin salto de línea final: corregido; editor configurado.
|
- Ficheros sin salto de línea final: corregido; editor configurado.
|
||||||
|
- Make: costó entender que va hacia atrás desde el objetivo, y que una regla patrón es una plantilla. Repasar con la sección de docs/06-make.md.
|
||||||
|
- Punteros: le cuesta saber cuándo hace falta `*` y cuándo no (también le pasaba en Go). Reforzar con ejemplos de su propio código.
|
||||||
- Impresoras físicas: sin registrar marca y modelo.
|
- Impresoras físicas: sin registrar marca y modelo.
|
||||||
|
|||||||
@@ -17,6 +17,20 @@ Los literales como `"1.0.0"` viven en **memoria de solo lectura**. Si
|
|||||||
intentas escribir en ellos, el comportamiento es indefinido; en la práctica,
|
intentas escribir en ellos, el comportamiento es indefinido; en la práctica,
|
||||||
el programa se cierra.
|
el programa se cierra.
|
||||||
|
|
||||||
|
## `'A'` frente a `"A"`
|
||||||
|
|
||||||
|
| Literal | Qué es | Tipo | Ocupa |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `'A'` (comillas simples) | Un **carácter**: el número 65 | `int` (cabe en un `uint8_t`) | 1 byte guardado en un `uint8_t` |
|
||||||
|
| `"A"` (comillas dobles) | Una **cadena**: los bytes `'A'` y `'\0'` en memoria de solo lectura | `char *`: un puntero a esos bytes | 2 bytes, más el puntero |
|
||||||
|
|
||||||
|
**En Go** es la misma diferencia que entre `'A'` (un `rune`) y `"A"` (un
|
||||||
|
`string`).
|
||||||
|
|
||||||
|
Pasar `"A"` donde se espera un byte es pasar **una dirección** donde se
|
||||||
|
espera **un número**. gcc lo detecta: `makes integer from pointer without a
|
||||||
|
cast` o `differ in signedness`.
|
||||||
|
|
||||||
## `const char *`
|
## `const char *`
|
||||||
|
|
||||||
Una función que "devuelve una cadena" en realidad devuelve **la dirección de
|
Una función que "devuelve una cadena" en realidad devuelve **la dirección de
|
||||||
@@ -44,6 +58,87 @@ Si `p` apunta a `"1.0.0"`, `*p` es solo el `'1'`.
|
|||||||
|
|
||||||
**En Go**: es lo mismo que `var p *T` frente a `*p`.
|
**En Go**: es lo mismo que `var p *T` frente a `*p`.
|
||||||
|
|
||||||
|
### Regla práctica: ¿quiero la dirección o la cosa?
|
||||||
|
|
||||||
|
Un puntero es un papel con la dirección de una casa:
|
||||||
|
|
||||||
|
- `buffer` es **el papel**, la dirección.
|
||||||
|
- `*buffer` es **la casa**, el struct que está en esa dirección.
|
||||||
|
- `buffer->len` es "ve a la casa y coge `len`". La flecha ya va a la casa,
|
||||||
|
así que no lleva `*`. Equivale a `(*buffer).len`.
|
||||||
|
- `&x` es lo contrario de `*`: "dame la dirección de `x`".
|
||||||
|
|
||||||
|
Antes de escribir, pregúntate: **¿esta función o esta operación necesita la
|
||||||
|
dirección o la cosa?**
|
||||||
|
|
||||||
|
Aplicado a `buffer.c`:
|
||||||
|
|
||||||
|
| Código | ¿`*`? | Por qué |
|
||||||
|
|---|---|---|
|
||||||
|
| `escpos_buffer *buffer = ...` | Sí | Declaración: dice que `buffer` **es** un puntero |
|
||||||
|
| `malloc(sizeof *buffer)` | Sí | Quiero el tamaño **de la casa** (el struct, 24 bytes). `sizeof buffer` sería el tamaño **del papel** (el puntero, 8 bytes) |
|
||||||
|
| `if (buffer == NULL)` | No | Compruebo si el papel tiene una dirección válida |
|
||||||
|
| `buffer->data = NULL` | No | `->` ya va a la casa |
|
||||||
|
| `return buffer;` | No | Devuelvo el papel; la función devuelve `escpos_buffer *` |
|
||||||
|
| `free(buffer)` | No | `free` necesita la dirección del bloque |
|
||||||
|
|
||||||
|
**En Go** las reglas son las mismas, pero Go oculta casi todos los `*`:
|
||||||
|
`p.X` desreferencia solo, y los métodos con receptor puntero también. Por eso en
|
||||||
|
Go casi solo ves `*` en los tipos (`*Buffer`) y rara vez en expresiones
|
||||||
|
(`*p = v`). En C la diferencia entre papel y casa siempre se escribe.
|
||||||
|
|
||||||
|
## Dónde va el `*`: siempre con el tipo
|
||||||
|
|
||||||
|
Los espacios alrededor del `*` dan igual. Estas tres líneas son idénticas:
|
||||||
|
|
||||||
|
```c
|
||||||
|
escpos_buffer* escpos_buffer_new(void);
|
||||||
|
escpos_buffer * escpos_buffer_new(void);
|
||||||
|
escpos_buffer *escpos_buffer_new(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
En todas, el tipo de retorno es `escpos_buffer *` ("puntero a
|
||||||
|
`escpos_buffer`"). El `*` **no** es de la función, es del tipo que devuelve.
|
||||||
|
En Go se lee igual: `func New() *Buffer`.
|
||||||
|
|
||||||
|
La costumbre en C es pegarlo al nombre por esta trampa:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int *a, b; /* a es int *, pero b es int, NO un puntero */
|
||||||
|
```
|
||||||
|
|
||||||
|
En una declaración múltiple, el `*` solo afecta al nombre que tiene al lado.
|
||||||
|
Pegarlo al nombre lo deja a la vista. Mejor aún: una variable por línea.
|
||||||
|
|
||||||
|
## Nombres de parámetros en las declaraciones
|
||||||
|
|
||||||
|
En una **declaración** (el prototipo de la cabecera), el nombre del parámetro
|
||||||
|
es opcional: `void escpos_buffer_free(escpos_buffer *);` es válido. El
|
||||||
|
compilador solo necesita los tipos.
|
||||||
|
|
||||||
|
En la **definición** (en el `.c`) sí hace falta, porque el cuerpo lo usa.
|
||||||
|
|
||||||
|
Aun así, conviene ponerlo también en la cabecera: es documentación. Con
|
||||||
|
`void copy(char *, const char *)` no sabes cuál es el destino;
|
||||||
|
con `void copy(char *dst, const char *src)` sí.
|
||||||
|
|
||||||
|
**En Go** pasa lo mismo en los tipos función y en las interfaces:
|
||||||
|
`func(int) error` es válido sin nombres.
|
||||||
|
|
||||||
|
## Paso por valor: un struct entero frente a un puntero
|
||||||
|
|
||||||
|
C copia siempre los argumentos, igual que Go. Si un parámetro es
|
||||||
|
`escpos_buffer` (sin `*`), la función recibiría **una copia del struct
|
||||||
|
entero**. Eso tiene dos problemas:
|
||||||
|
|
||||||
|
1. Con un tipo opaco ni siquiera compila: el struct está incompleto fuera de
|
||||||
|
su `.c` y no se sabe cuánto copiar.
|
||||||
|
2. Liberar una copia no sirve de nada: el bloque que reservó `malloc` es el
|
||||||
|
original.
|
||||||
|
|
||||||
|
Por eso las funciones de un tipo opaco reciben siempre un **puntero**. Es lo
|
||||||
|
mismo que los métodos con receptor puntero de Go (`func (b *Buffer) Free()`).
|
||||||
|
|
||||||
## `printf` y el formato
|
## `printf` y el formato
|
||||||
|
|
||||||
El primer argumento de `printf` es el **formato**. Nunca pases ahí una cadena
|
El primer argumento de `printf` es el **formato**. Nunca pases ahí una cadena
|
||||||
|
|||||||
+79
-2
@@ -26,6 +26,19 @@ objetivo: prerrequisitos
|
|||||||
- **Prerrequisitos**: los ficheros de los que depende.
|
- **Prerrequisitos**: los ficheros de los que depende.
|
||||||
- **Receta**: los comandos de shell que producen el objetivo.
|
- **Receta**: los comandos de shell que producen el objetivo.
|
||||||
|
|
||||||
|
Es como una receta de cocina: *plato: ingredientes* y, debajo, los pasos.
|
||||||
|
|
||||||
|
```make
|
||||||
|
build/hello: build/hello.o build/escpos.o
|
||||||
|
$(CC) build/hello.o build/escpos.o -o build/hello
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parte | Qué es | Pregunta que responde |
|
||||||
|
|---|---|---|
|
||||||
|
| `build/hello` (antes de `:`) | **Objetivo**: el fichero que sale. No es el nombre de un comando. | ¿Qué quiero tener? |
|
||||||
|
| `build/hello.o build/escpos.o` (después de `:`) | **Prerrequisitos**: los ficheros que necesita. | ¿Qué hace falta antes? |
|
||||||
|
| línea con tabulador | **Receta**: el comando. | ¿Cómo se fabrica? |
|
||||||
|
|
||||||
Make rehace un objetivo si no existe o si **algún prerrequisito tiene una fecha
|
Make rehace un objetivo si no existe o si **algún prerrequisito tiene una fecha
|
||||||
de modificación más reciente** que él. No usa hashes como Go: solo compara
|
de modificación más reciente** que él. No usa hashes como Go: solo compara
|
||||||
fechas (`mtime`). Por eso un `touch` basta para forzar una recompilación.
|
fechas (`mtime`). Por eso un `touch` basta para forzar una recompilación.
|
||||||
@@ -54,8 +67,33 @@ build/escpos.o: src/escpos.c include/escpos.h
|
|||||||
**En Go** es la diferencia entre llamar a `go build` paquete por paquete desde
|
**En Go** es la diferencia entre llamar a `go build` paquete por paquete desde
|
||||||
un script y dejar que `go build` resuelva los imports.
|
un script y dejar que `go build` resuelva los imports.
|
||||||
|
|
||||||
Make recorre el grafo desde el objetivo que le pides (o el primero del fichero)
|
### Make va hacia atrás
|
||||||
hacia sus prerrequisitos, en profundidad.
|
|
||||||
|
Make no ejecuta el Makefile de arriba abajo como un script. Empieza por el
|
||||||
|
objetivo que le pides (o el primero del fichero si no pides ninguno) y va
|
||||||
|
tirando de lo que necesita:
|
||||||
|
|
||||||
|
```
|
||||||
|
all
|
||||||
|
└─ build/hello (enlazar)
|
||||||
|
├─ build/hello.o ← examples/hello.c + escpos.h
|
||||||
|
└─ build/escpos.o ← regla patrón: src/escpos.c + escpos.h
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Le pides `all`, que necesita `build/hello`.
|
||||||
|
2. `build/hello` necesita `build/hello.o` y `build/escpos.o`.
|
||||||
|
3. Para cada prerrequisito busca una regla que lo fabrique y repite lo mismo,
|
||||||
|
hasta llegar a ficheros que ya existen (`examples/hello.c`, `src/escpos.c`).
|
||||||
|
4. Después construye de las hojas hacia arriba. Antes de cada receta compara
|
||||||
|
fechas: si el objetivo existe y es más nuevo que todos sus prerrequisitos,
|
||||||
|
no lo rehace.
|
||||||
|
|
||||||
|
El orden de las reglas en el fichero da igual. Solo importa que `all` sea la
|
||||||
|
primera, porque es la que se construye por defecto.
|
||||||
|
|
||||||
|
Nadie le dice a Make "compila `escpos.o`": llega ahí porque `build/hello` lo
|
||||||
|
pide. Si un objetivo no aparece como prerrequisito de nada, Make no lo
|
||||||
|
construye.
|
||||||
|
|
||||||
## Piezas que vas a necesitar
|
## Piezas que vas a necesitar
|
||||||
|
|
||||||
@@ -103,6 +141,45 @@ cambies compilador o flags: `make clean` y construir de cero.
|
|||||||
**En Go** los flags y la versión del compilador forman parte del hash de la
|
**En Go** los flags y la versión del compilador forman parte del hash de la
|
||||||
caché, así que `go build` sí recompila lo necesario.
|
caché, así que `go build` sí recompila lo necesario.
|
||||||
|
|
||||||
|
### Regla patrón = función con parámetros
|
||||||
|
|
||||||
|
Una regla patrón es como una función de Go, y `$@` y `$<` son sus parámetros:
|
||||||
|
|
||||||
|
```make
|
||||||
|
build/%.o: src/%.c
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Equivalente mental en Go
|
||||||
|
func buildObject(target, source string) { // target = $@, source = $<
|
||||||
|
run("gcc -std=c17 ... -c " + source + " -o " + target)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Cuando Make necesita `build/escpos.o`:
|
||||||
|
|
||||||
|
1. Busca una regla para `build/escpos.o`. No hay ninguna escrita a mano.
|
||||||
|
2. Prueba la regla patrón `build/%.o`. Encaja, con `%` = `escpos`.
|
||||||
|
3. Calcula el prerrequisito: `src/%.c` → `src/escpos.c`.
|
||||||
|
4. "Llama a la función": `$@` = `build/escpos.o`, `$<` = `src/escpos.c`.
|
||||||
|
5. Sustituye las variables, **imprime el comando ya sustituido** y lo ejecuta.
|
||||||
|
|
||||||
|
Por eso en la salida de `make` nunca se ve `$<`: Make imprime el comando
|
||||||
|
después de sustituir, igual que `fmt.Printf` imprime los valores y no `%s`.
|
||||||
|
|
||||||
|
Otro ejemplo, para `build/buffer.o`:
|
||||||
|
|
||||||
|
| | Valor | Ojo |
|
||||||
|
|---|---|---|
|
||||||
|
| `%` | `buffer` | Solo la parte que varía: sin carpeta ni extensión. |
|
||||||
|
| `$@` | `build/buffer.o` | El objetivo **completo**, con su carpeta. |
|
||||||
|
| `$<` | `src/buffer.c` | El prerrequisito **completo**, con su carpeta. |
|
||||||
|
|
||||||
|
**La regla patrón es una plantilla, no compila "todo lo que hay en `src`".**
|
||||||
|
Make solo la usa cuando algún objetivo pide un `build/ALGO.o`. Si creas
|
||||||
|
`src/buffer.c` y nadie pide `build/buffer.o`, no se compila.
|
||||||
|
|
||||||
## El problema de las cabeceras
|
## El problema de las cabeceras
|
||||||
|
|
||||||
Si `escpos.c` incluye `escpos.h` y cambias solo el `.h`, make no lo sabe:
|
Si `escpos.c` incluye `escpos.h` y cambias solo el `.h`, make no lo sabe:
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
# 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]`.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### 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×).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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ó |
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# Structs
|
||||||
|
|
||||||
|
## Definir un struct
|
||||||
|
|
||||||
|
Definir es dar los campos. Es el equivalente a `type Point struct { ... }` de
|
||||||
|
Go:
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct point {
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
}; /* ← punto y coma obligatorio */
|
||||||
|
```
|
||||||
|
|
||||||
|
Diferencias con Go:
|
||||||
|
|
||||||
|
| Go | C |
|
||||||
|
|---|---|
|
||||||
|
| `X int` (nombre, tipo) | `int x;` (tipo, nombre) |
|
||||||
|
| Sin `;` | Cada campo acaba en `;`, y la llave final también: `};` |
|
||||||
|
| El nombre del tipo es `Point` | El nombre del tipo es `struct point`: la palabra `struct` forma parte del nombre |
|
||||||
|
| Mayúscula = exportado | No hay campos privados: o ves la definición entera o no ves nada |
|
||||||
|
|
||||||
|
## Declarar un struct (tipo incompleto)
|
||||||
|
|
||||||
|
Declarar es decir "existe un struct con este nombre" sin dar sus campos:
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct point;
|
||||||
|
```
|
||||||
|
|
||||||
|
Hasta que aparezca la definición, `struct point` es un **tipo incompleto**. El
|
||||||
|
compilador no conoce su tamaño ni sus campos, así que:
|
||||||
|
|
||||||
|
| Con un tipo incompleto | ¿Se puede? |
|
||||||
|
|---|---|
|
||||||
|
| Declarar un puntero: `struct point *p;` | Sí. Todos los punteros a struct miden lo mismo |
|
||||||
|
| Pasar o devolver punteros en funciones | Sí |
|
||||||
|
| Crear una variable: `struct point p;` | No: no sabe cuántos bytes reservar |
|
||||||
|
| `sizeof(struct point)` | No |
|
||||||
|
| Acceder a un campo: `p->x` | No: no sabe que existe `x` |
|
||||||
|
|
||||||
|
Esto es justo lo que hace funcionar el **tipo opaco**: la cabecera pública
|
||||||
|
declara, el `.c` define. El código cliente solo maneja punteros y llama a
|
||||||
|
funciones de la librería. Dentro del `.c`, donde está la definición, el tipo
|
||||||
|
está completo y se usan los campos con normalidad.
|
||||||
|
|
||||||
|
Es parecido a devolver una interfaz en Go para ocultar el tipo concreto, pero
|
||||||
|
sin coste de llamada indirecta: es solo un puntero.
|
||||||
|
|
||||||
|
### Struct público frente a struct opaco
|
||||||
|
|
||||||
|
Definir el struct completo en la cabecera es perfectamente válido. Es una
|
||||||
|
decisión de diseño:
|
||||||
|
|
||||||
|
| | Struct definido en el `.h` (público) | Struct opaco |
|
||||||
|
|---|---|---|
|
||||||
|
| Crear uno | En la pila, sin `malloc`: `struct point p;` | Solo con la función `_new` de la librería (montón) |
|
||||||
|
| Campos | Cualquiera puede leerlos **y cambiarlos** | Solo a través de funciones |
|
||||||
|
| Invariantes (`len <= cap`) | El cliente puede romperlas: `b.len = 9999` y luego un desbordamiento | La librería las garantiza |
|
||||||
|
| Cambiar los campos | Rompe a quien ya compiló contra la versión anterior (el tamaño y los desplazamientos van dentro de su `.o`) | Libre: el cliente nunca supo cómo era |
|
||||||
|
| Coste | Acceso directo, sin llamadas | Una llamada a función por cada acceso |
|
||||||
|
|
||||||
|
Se usa **público** para datos simples sin reglas internas, que el usuario
|
||||||
|
rellena: `struct tm` (fechas) o `struct sockaddr_in` (direcciones de red, fase
|
||||||
|
4). Se usa **opaco** para objetos con estado e invariantes: `FILE` en la
|
||||||
|
práctica, o nuestro `escpos_buffer`.
|
||||||
|
|
||||||
|
**En Go** es la diferencia entre `type Point struct { X, Y int }` (campos
|
||||||
|
exportados) y `type Buffer struct { data []byte }` (campos sin exportar, con
|
||||||
|
constructor y métodos). La diferencia es que en Go el tamaño del struct no
|
||||||
|
importa entre paquetes, porque todo se recompila junto.
|
||||||
|
|
||||||
|
Para este proyecto el handle opaco es obligatorio: los bindings de cgo
|
||||||
|
(fase 12) necesitan una API que no cambie aunque cambien las tripas.
|
||||||
|
|
||||||
|
## `typedef`: un alias para no escribir `struct`
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct point point;
|
||||||
|
```
|
||||||
|
|
||||||
|
Se lee así: "`point` es otro nombre para `struct point`". A partir de ahí,
|
||||||
|
`point *p` equivale a `struct point *p`. Puede ir antes de la definición: sirve
|
||||||
|
igual para un tipo incompleto. Es lo más parecido a cómo nombra Go los tipos.
|
||||||
|
|
||||||
|
Es habitual usar el mismo nombre para el struct y para el alias. No chocan,
|
||||||
|
porque C guarda los nombres de struct en un espacio de nombres aparte.
|
||||||
|
|
||||||
|
La sintaxis es `typedef <tipo existente> <nombre nuevo>;`. El nombre nuevo va
|
||||||
|
**al final**, como en la declaración de una variable. Error típico: escribir
|
||||||
|
`typedef struct escpos_buffer;` sin el nombre nuevo. gcc solo avisa
|
||||||
|
(`useless storage class specifier in empty declaration`): declara el struct
|
||||||
|
pero no crea ningún alias.
|
||||||
|
|
||||||
|
**En Go**, el alias equivalente es `type Nuevo = Existente`, con el orden al
|
||||||
|
revés.
|
||||||
|
|
||||||
|
## Tipos que necesitan un `#include`
|
||||||
|
|
||||||
|
En Go, `byte`, `int` o `uint8` existen siempre. En C solo vienen de serie los
|
||||||
|
tipos básicos (`char`, `int`, `long`...). Los de tamaño fijo y los de tamaños
|
||||||
|
son `typedef` declarados en cabeceras estándar:
|
||||||
|
|
||||||
|
| Tipo | Cabecera | Equivalente Go |
|
||||||
|
|---|---|---|
|
||||||
|
| `uint8_t`, `int32_t`, `uint64_t`... | `<stdint.h>` | `uint8`, `int32`, `uint64` |
|
||||||
|
| `size_t` (tamaños y longitudes) | `<stddef.h>` (también la traen `<stdlib.h>`, `<stdio.h>`, `<string.h>`) | el `int` de `len()`, pero sin signo |
|
||||||
|
| `bool`, `true`, `false` | `<stdbool.h>` | `bool` |
|
||||||
|
| `NULL` | `<stddef.h>` (y las mismas que `size_t`) | `nil` |
|
||||||
|
|
||||||
|
Sin el `#include`: `error: unknown type name 'uint8_t'`. El editor (IntelliSense
|
||||||
|
de VS Code) lo muestra como `identifier "uint8_t" is undefined`.
|
||||||
|
|
||||||
|
### `uint8_t`
|
||||||
|
|
||||||
|
Entero **sin signo** (*u*nsigned) de **exactamente 8 bits**: valores de 0 a
|
||||||
|
255. Es el `byte`/`uint8` de Go. El sufijo `_t` es la convención para los
|
||||||
|
nombres de tipos creados con `typedef`.
|
||||||
|
|
||||||
|
¿Por qué no `char`, que también mide un byte? Porque el estándar no fija si
|
||||||
|
`char` tiene signo. En x86 lo tiene (de -128 a 127), y en ARM, el procesador
|
||||||
|
de casi todos los Android, no. Un byte `0xE9` (la `é` en algunas páginas de
|
||||||
|
códigos) valdría -23 en tu PC y 233 en el móvil. `uint8_t` es 0-255 en
|
||||||
|
todas partes.
|
||||||
|
|
||||||
|
### `size_t`
|
||||||
|
|
||||||
|
Entero **sin signo** pensado para **tamaños en bytes y número de elementos**:
|
||||||
|
|
||||||
|
- Es lo que devuelve `sizeof` y lo que recibe `malloc`.
|
||||||
|
- Mide lo mismo que un puntero: 64 bits en x86-64 y 32 bits en un ARM de 32
|
||||||
|
bits. Así siempre cabe el tamaño de cualquier bloque que puedas direccionar.
|
||||||
|
- En `printf` se imprime con `%zu`.
|
||||||
|
|
||||||
|
**En Go** su papel lo hace `int`, que es lo que devuelven `len()` y `cap()`.
|
||||||
|
Pero `int` tiene signo y `size_t` no.
|
||||||
|
|
||||||
|
Trampa de los tipos sin signo: no hay negativos. `size_t n = 0; n - 1` **no
|
||||||
|
da -1**: da la vuelta al valor máximo (18446744073709551615 en 64 bits). Un
|
||||||
|
bucle `for (size_t i = n - 1; i >= 0; i--)` no termina nunca, porque
|
||||||
|
`i >= 0` siempre es verdad. Go, con `int`, no tiene este problema.
|
||||||
|
|
||||||
|
### Qué tipo usar
|
||||||
|
|
||||||
|
Se elige por **lo que representa el valor**, no por ahorrar memoria:
|
||||||
|
|
||||||
|
| Representa | Tipo | Go |
|
||||||
|
|---|---|---|
|
||||||
|
| Un número cualquiera, un contador o un código de resultado (puede ser negativo: `-1`) | `int` | `int` |
|
||||||
|
| Un tamaño, una longitud o un índice en memoria | `size_t` | `int` (`len`, `cap`) |
|
||||||
|
| Un byte de datos crudos (lo que se envía a la impresora) | `uint8_t` | `byte` |
|
||||||
|
| Un valor con anchura fija por protocolo o formato (2 bytes de un comando, 4 de una cabecera) | `uint16_t`, `uint32_t`... | `uint16`, `uint32` |
|
||||||
|
| Sí o no | `bool` (`<stdbool.h>`) | `bool` |
|
||||||
|
| Texto | `char` / `char *` | `string` |
|
||||||
|
|
||||||
|
Usar un tipo pequeño para una variable local o un valor de retorno **no ahorra
|
||||||
|
nada**. El valor viaja en un registro de 64 bits igualmente, y en las
|
||||||
|
operaciones C lo convierte a `int` (*promoción entera*). Solo complica las
|
||||||
|
cosas.
|
||||||
|
|
||||||
|
**Ejemplo real de fallo**: una función que devuelve `uint8_t` y hace
|
||||||
|
`return -1;`. Un `uint8_t` no tiene negativos, así que `-1` se convierte en
|
||||||
|
`255`. Después, `if (err == -1)` compara `255` con `-1`: **siempre es falso**, y
|
||||||
|
el error no se detecta nunca. gcc con los flags del proyecto no avisa. Clang
|
||||||
|
sí (`comparison of constant -1 with expression of type 'uint8_t' is always
|
||||||
|
false`), y gcc también lo hace con `-Wconversion`.
|
||||||
|
|
||||||
|
Cada fichero incluye lo que usa él mismo. No hay que fiarse de que otra
|
||||||
|
cabecera lo traiga de rebote.
|
||||||
|
|
||||||
|
## Acceder a los campos: `.` y `->`
|
||||||
|
|
||||||
|
| Tienes | C | Go |
|
||||||
|
|---|---|---|
|
||||||
|
| Un valor: `struct point p` | `p.x` | `p.X` |
|
||||||
|
| Un puntero: `struct point *p` | `p->x` | `p.X` (Go desreferencia solo) |
|
||||||
|
|
||||||
|
`p->x` es una abreviatura de `(*p).x`: "sigue el puntero y coge el campo `x`".
|
||||||
|
Si usas `.` con un puntero o `->` con un valor, el compilador da error.
|
||||||
|
|
||||||
|
## Inicializar
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct point a = { .x = 1, .y = 2 }; /* inicializadores designados */
|
||||||
|
struct point b = { 0 }; /* todo a cero */
|
||||||
|
```
|
||||||
|
|
||||||
|
Los campos que no nombras quedan a cero. Como en un literal de Go
|
||||||
|
(`Point{X: 1}`).
|
||||||
|
|
||||||
|
Pero esto solo vale al **crear una variable**. Un struct reservado con
|
||||||
|
`malloc` es basura. Hay que rellenar cada campo a mano con `->`, o usar
|
||||||
|
`calloc`, que lo pone todo a cero.
|
||||||
|
|
||||||
|
## Dónde va cada cosa en el proyecto
|
||||||
|
|
||||||
|
| Fichero | Contenido | Quién lo ve |
|
||||||
|
|---|---|---|
|
||||||
|
| `include/escpos.h` | `typedef struct escpos_buffer escpos_buffer;` (declaración + alias) | Todo el mundo |
|
||||||
|
| `src/buffer.c` | `struct escpos_buffer { ... };` (definición) | Solo `buffer.c` |
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# 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 |
|
||||||
|
|
||||||
|
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.
|
||||||
+13
-1
@@ -8,7 +8,9 @@ Ordenados por tema, en el orden en que se ven en el proyecto.
|
|||||||
para compilar, cabeceras e `#include`, encapsulación y tipos opacos, *include guards*,
|
para compilar, cabeceras e `#include`, encapsulación y tipos opacos, *include guards*,
|
||||||
flags, qué ve quien recibe los binarios, `nm` y errores del enlazador.
|
flags, qué ve quien recibe los binarios, `nm` y errores del enlazador.
|
||||||
2. [Punteros y cadenas](02-pointers-and-strings.md): cadenas terminadas en
|
2. [Punteros y cadenas](02-pointers-and-strings.md): cadenas terminadas en
|
||||||
`'\0'`, `const char *`, los dos significados de `*` y el formato de `printf`.
|
`'\0'`, `const char *`, los dos significados de `*`, dónde va el `*`,
|
||||||
|
nombres de parámetros, paso por valor frente a puntero y el formato de
|
||||||
|
`printf`.
|
||||||
3. [La memoria de un proceso](03-process-memory.md): memoria virtual, fallos
|
3. [La memoria de un proceso](03-process-memory.md): memoria virtual, fallos
|
||||||
de página, modo usuario y modo núcleo, llamadas al sistema, cómo se lee la
|
de página, modo usuario y modo núcleo, llamadas al sistema, cómo se lee la
|
||||||
memoria de otro proceso, y por qué el peligro real está en la tuya.
|
memoria de otro proceso, y por qué el peligro real está en la tuya.
|
||||||
@@ -21,6 +23,16 @@ Ordenados por tema, en el orden en que se ven en el proyecto.
|
|||||||
6. [Make](06-make.md): reglas, grafo de dependencias por fechas, variables
|
6. [Make](06-make.md): reglas, grafo de dependencias por fechas, variables
|
||||||
automáticas, reglas patrón, dependencias de cabeceras con `-MMD -MP` y
|
automáticas, reglas patrón, dependencias de cabeceras con `-MMD -MP` y
|
||||||
detección de Windows/Linux.
|
detección de Windows/Linux.
|
||||||
|
7. [Memoria dinámica](07-dynamic-memory.md): pila y montón, *escape
|
||||||
|
analysis* de Go, `malloc`/`free`/`calloc`, `sizeof *p`, `realloc` y
|
||||||
|
estrategia de crecimiento, fugas, propiedad de
|
||||||
|
los punteros y Valgrind.
|
||||||
|
8. [Structs](08-structs.md): definir frente a declarar, tipos incompletos y
|
||||||
|
tipo opaco, struct público frente a opaco, `typedef`, `uint8_t` y
|
||||||
|
`size_t`, qué tipo usar en cada caso, `.` frente a `->` e inicializadores designados.
|
||||||
|
9. [Depurar](09-debugging.md): qué es un *segfault*, código de salida 139,
|
||||||
|
`NULL` y la página 0, `free(): invalid pointer` (código 134), método para
|
||||||
|
depurar con Valgrind y los comandos mínimos de gdb.
|
||||||
|
|
||||||
[Chuleta de comandos](cheatsheet.md): todos los comandos usados, agrupados por
|
[Chuleta de comandos](cheatsheet.md): todos los comandos usados, agrupados por
|
||||||
tarea.
|
tarea.
|
||||||
|
|||||||
@@ -32,6 +32,30 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes.
|
|||||||
| `tail -c 5 fichero \| xxd` | Muestra los últimos bytes: sirve para ver si el fichero acaba en `0a` (`\n`). |
|
| `tail -c 5 fichero \| xxd` | Muestra los últimos bytes: sirve para ver si el fichero acaba en `0a` (`\n`). |
|
||||||
| `echo $?` | Código de salida del último comando: `0` es éxito. |
|
| `echo $?` | Código de salida del último comando: `0` es éxito. |
|
||||||
|
|
||||||
|
## Memoria
|
||||||
|
|
||||||
|
| Comando | Qué hace |
|
||||||
|
|---|---|
|
||||||
|
| `valgrind ./build/hello` | Sin `--leak-check`: errores de acceso (`Invalid write`, `Invalid free`) y de dónde viene cada dirección. |
|
||||||
|
| `valgrind --leak-check=full ./build/hello` | Ejecuta vigilando la memoria: fugas, *use after free*, lecturas sin inicializar. Solo Linux. |
|
||||||
|
| `cat /proc/self/maps` | Mapa de memoria virtual del proceso: código, `[heap]`, `[stack]`, librerías. Con `<pid>` en vez de `self`, el de otro proceso. |
|
||||||
|
| `journalctl -k \| grep -i "out of memory"` | Ver si el OOM killer del núcleo ha matado algún proceso. |
|
||||||
|
| `pgrep -a nombre` | Lista los procesos cuyo nombre contiene `nombre`, con su PID y su línea de comandos. |
|
||||||
|
| `pgrep -o nombre` | PID del proceso más antiguo con ese nombre (normalmente el principal). |
|
||||||
|
| `grep -E 'VmSize\|VmRSS' /proc/<pid>/status` | Memoria virtual reservada (`VmSize`) frente a RAM física en uso (`VmRSS`). |
|
||||||
|
| `grep -E 'heap\|stack' /proc/<pid>/maps` | Rangos de direcciones del montón y de la pila de un proceso. |
|
||||||
|
| `pmap -x <pid>` | Mapa de memoria con tamaño virtual y residente de cada zona. |
|
||||||
|
| `ulimit -s` | Tamaño máximo de la pila en KB (8192 = 8 MB). |
|
||||||
|
| `go build -gcflags=-m` | (Go) Muestra qué variables escapan al montón. |
|
||||||
|
|
||||||
|
## Depurar
|
||||||
|
|
||||||
|
| Comando | Qué hace |
|
||||||
|
|---|---|
|
||||||
|
| `gdb ./build/hello` | Abre el programa en el depurador (hace falta `-g`). |
|
||||||
|
| `run` / `bt` / `print x` | Dentro de gdb: ejecutar, ver la pila de llamadas, ver una variable. |
|
||||||
|
| `break f.c:42` / `next` / `step` / `continue` | Dentro de gdb: punto de parada, siguiente línea, entrar en la función, seguir. |
|
||||||
|
|
||||||
## Solo en Windows (MSYS2)
|
## Solo en Windows (MSYS2)
|
||||||
|
|
||||||
| Comando | Qué hace |
|
| Comando | Qué hace |
|
||||||
@@ -61,6 +85,13 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes.
|
|||||||
| `missing separator` | Make | La receta empieza por espacios en vez de tabulador. |
|
| `missing separator` | Make | La receta empieza por espacios en vez de tabulador. |
|
||||||
| `'x' is up to date` | Make | No es un error: el objetivo es más nuevo que todos sus prerrequisitos. Si `x` es una acción (`clean`), falta en `.PHONY`. |
|
| `'x' is up to date` | Make | No es un error: el objetivo es más nuevo que todos sus prerrequisitos. Si `x` es una acción (`clean`), falta en `.PHONY`. |
|
||||||
| `no newline at end of file` | Clang | El fichero no termina en `\n`. |
|
| `no newline at end of file` | Clang | El fichero no termina en `\n`. |
|
||||||
|
| `unknown type name 'uint8_t'` / `identifier ... is undefined` | Compilador / VS Code | Falta el `#include` que declara ese tipo (`<stdint.h>`, `<stddef.h>`...). |
|
||||||
|
| `makes integer from pointer without a cast` | Compilador | Se pasa una dirección (puntero) donde se espera un número. Por ejemplo, `"A"` en vez de `'A'`. |
|
||||||
|
| `pointer targets ... differ in signedness` | Compilador | Se mezclan `char *` y `uint8_t *` (`unsigned char *`). |
|
||||||
|
| `Segmentation fault (core dumped)` (código 139) | Núcleo | Acceso a una dirección no mapeada; casi siempre un puntero `NULL`. Usa gdb. |
|
||||||
|
| `free(): invalid pointer` / `Aborted` (código 134) | glibc | `free` de algo que no dio `malloc`: dirección de la pila (`&x`), literal, interior de un bloque o *double free*. Usa Valgrind. |
|
||||||
|
| `comparison of constant -1 with expression of type 'uint8_t' is always false` | Clang / VS Code | Comparas un tipo sin signo con un negativo: nunca puede ser igual. Revisa el tipo. |
|
||||||
|
| `useless storage class specifier in empty declaration` | Compilador | Un `typedef` sin nombre nuevo al final. |
|
||||||
|
|
||||||
## Go equivalente
|
## Go equivalente
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,23 @@ int main(void) {
|
|||||||
const char *version = escpos_version();
|
const char *version = escpos_version();
|
||||||
|
|
||||||
printf("%s\n", version);
|
printf("%s\n", version);
|
||||||
|
|
||||||
|
escpos_buffer *buffer = escpos_buffer_new();
|
||||||
|
if (buffer == NULL)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
int err = escpos_buffer_append_byte(buffer, 'A');
|
||||||
|
if (err == -1)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
escpos_buffer_free(buffer);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-1
@@ -1,6 +1,30 @@
|
|||||||
#ifndef ESCPOS_H
|
#ifndef ESCPOS_H
|
||||||
#define ESCPOS_H
|
#define ESCPOS_H
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
const char *escpos_version(void);
|
const char *escpos_version(void);
|
||||||
|
|
||||||
|
typedef struct escpos_buffer escpos_buffer;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Crea un buffer vacío. Devuelve un puntero al buffer, o NULL si no hay
|
||||||
|
* memoria.
|
||||||
|
* El que llama es el dueño y debe liberarlo con escpos_buffer_free.
|
||||||
|
* Por ejemplo: si main.c llama a escpos_buffer_new, main.c es EL DUEÑO.
|
||||||
|
*/
|
||||||
|
escpos_buffer *escpos_buffer_new(void);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Libera el buffer y toda su memoria. Si recibe NULL no hace nada.
|
||||||
|
* Propiedad: después de llamarla, el puntero ya no es válido.
|
||||||
|
*/
|
||||||
|
void escpos_buffer_free(escpos_buffer *buffer);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Añade un byte al final. Devuelve 0 o -1 si no hay memoria; en ese caso
|
||||||
|
* el buffer queda intacto.
|
||||||
|
*/
|
||||||
|
int escpos_buffer_append_byte(escpos_buffer *buffer, uint8_t byte);
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#include "escpos.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
|
||||||
|
struct escpos_buffer
|
||||||
|
{
|
||||||
|
uint8_t *data;
|
||||||
|
size_t len;
|
||||||
|
size_t cap;
|
||||||
|
};
|
||||||
|
|
||||||
|
escpos_buffer *escpos_buffer_new(void)
|
||||||
|
{
|
||||||
|
escpos_buffer *buffer = malloc(sizeof *buffer);
|
||||||
|
if (buffer == NULL)
|
||||||
|
{
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer->data = NULL;
|
||||||
|
buffer->len = 0;
|
||||||
|
buffer->cap = 0;
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void escpos_buffer_free(escpos_buffer *buffer)
|
||||||
|
{
|
||||||
|
if (buffer == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(buffer->data);
|
||||||
|
free(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
int escpos_buffer_append_byte(escpos_buffer *buffer, uint8_t byte)
|
||||||
|
{
|
||||||
|
if (buffer->len == buffer->cap)
|
||||||
|
{
|
||||||
|
size_t new_cap = 0;
|
||||||
|
if (buffer->cap == 0)
|
||||||
|
{
|
||||||
|
new_cap = 64;
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
new_cap = buffer->cap * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
uint8_t *tmp = realloc(buffer->data, new_cap);
|
||||||
|
if (tmp == NULL)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer->data = tmp;
|
||||||
|
buffer->cap = new_cap;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer->data[buffer->len] = byte;
|
||||||
|
buffer->len++;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
#include "escpos.h"
|
#include "escpos.h"
|
||||||
|
|
||||||
const char *escpos_version(void) {
|
const char *escpos_version(void)
|
||||||
|
{
|
||||||
return "1.0.0";
|
return "1.0.0";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user