Compare commits

...

8 Commits

Author SHA1 Message Date
pedro 4351a40bd2 progress 2026-10-07 03:45:29 +02:00
pedro 208a3e9738 windows system 2026-10-06 13:23:15 +02:00
pedro 03d2ece7d6 CLAUDE.md: adaptar sesiones a Windows/Fedora y preferencias de enseñanza
Las preferencias (pasos pequeños, pistas por niveles, chuleta) pasan a
CLAUDE.md para que estén en las dos máquinas. PROGRESO.md tiene ahora una
sección de pendientes por máquina.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:47:52 +02:00
pedro 6212fc971f PROGRESO: estado de 2.3b y pasos para la sesión en Windows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:46:55 +02:00
pedro 8383a725e2 hello.c 2026-10-05 21:45:53 +02:00
pedro b15620838b memcpy 2026-10-02 21:03:47 +02:00
pedro 9d517f3607 malloc, free, append 2026-10-02 11:36:59 +02:00
pedro b8141311aa update makefile 2026-09-30 11:57:16 +02:00
26 changed files with 2806 additions and 32 deletions
+7
View File
@@ -0,0 +1,7 @@
# Finales de línea LF en las dos máquinas (Make exige tabuladores y LF)
* text=auto eol=lf
# Bytes en crudo: git no debe tocarlos nunca
*.bin binary
*.exe binary
*.o binary
+6
View File
@@ -1,3 +1,9 @@
build/ build/
*.o *.o
*.exe *.exe
# Volcados de memoria de valgrind
vgcore.*
# Binarios de Go compilados en Linux
goescpos/hello
+21
View File
@@ -0,0 +1,21 @@
{
"version": 4,
"configurations": [
{
"name": "Linux",
"includePath": ["${workspaceFolder}/include"],
"compilerPath": "/usr/bin/gcc",
"compilerArgs": ["-std=c17", "-Wall", "-Wextra", "-Wpedantic"],
"cStandard": "c17",
"intelliSenseMode": "linux-gcc-x64"
},
{
"name": "Win32",
"includePath": ["${workspaceFolder}/include"],
"compilerPath": "C:/msys64/ucrt64/bin/gcc.exe",
"compilerArgs": ["-std=c17", "-Wall", "-Wextra", "-Wpedantic"],
"cStandard": "c17",
"intelliSenseMode": "windows-gcc-x64"
}
]
}
+37
View File
@@ -0,0 +1,37 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Depurar hello (gdb)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/hello",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"preLaunchTask": "make",
"setupCommands": [
{
"description": "Formato legible para structs y arrays",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "No descargar símbolos de depuración del sistema",
"text": "set debuginfod enabled off",
"ignoreFailures": true
}
],
"windows": {
"program": "${workspaceFolder}/build/hello.exe",
"miDebuggerPath": "C:/msys64/ucrt64/bin/gdb.exe",
"environment": [
{ "name": "PATH", "value": "C:/msys64/ucrt64/bin;${env:PATH}" }
]
}
}
]
}
+29
View File
@@ -0,0 +1,29 @@
{
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"files.eol": "\n",
"files.associations": {
"*.h": "c"
},
"[c]": {
"editor.tabSize": 4,
"editor.insertSpaces": true,
"editor.formatOnSave": false
},
"[makefile]": {
"editor.insertSpaces": false,
"editor.detectIndentation": false
},
"terminal.integrated.profiles.windows": {
"MSYS2 UCRT64": {
"path": "C:/msys64/usr/bin/bash.exe",
"args": ["--login", "-i"],
"env": {
"MSYSTEM": "UCRT64",
"CHERE_INVOKING": "1",
"MSYS2_PATH_TYPE": "inherit"
}
}
},
"terminal.integrated.defaultProfile.windows": "MSYS2 UCRT64"
}
+47
View File
@@ -0,0 +1,47 @@
{
"version": "2.0.0",
"windows": {
"options": {
"shell": {
"executable": "C:/msys64/usr/bin/bash.exe",
"args": ["--login", "-c"]
},
"env": {
"MSYSTEM": "UCRT64",
"CHERE_INVOKING": "1",
"MSYS2_PATH_TYPE": "inherit"
}
}
},
"tasks": [
{
"label": "make",
"type": "shell",
"command": "make",
"group": { "kind": "build", "isDefault": true },
"problemMatcher": ["$gcc"]
},
{
"label": "make clean",
"type": "shell",
"command": "make clean",
"problemMatcher": []
},
{
"label": "make (clang)",
"type": "shell",
"command": "make clean && make CC=clang",
"problemMatcher": ["$gcc"]
},
{
"label": "valgrind",
"type": "shell",
"command": "valgrind --leak-check=full ./build/hello",
"windows": {
"command": "echo 'valgrind no existe en Windows: pruébalo en Fedora'"
},
"dependsOn": "make",
"problemMatcher": []
}
]
}
+40 -4
View File
@@ -21,13 +21,38 @@ pregúntame marca y modelo de cada una.
# Entorno # Entorno
- Windows: MSYS2, entorno UCRT64, con GCC, gdb y make Trabajo en dos máquinas y voy cambiando de una a otra. Las sincronizo con git.
- Fedora en WSL: para compilar y probar la parte portable con
AddressSanitizer, UBSan y valgrind (en Windows con MinGW no funcionan) - **Sobremesa: Windows** con MSYS2, entorno UCRT64, con GCC, gdb y make.
- **Portátil: Fedora** nativo, con gcc, clang, gdb, make y valgrind. Aquí se
prueba la parte portable con AddressSanitizer, UBSan y valgrind, que no
funcionan en Windows con MinGW.
- Estándar: C17 - Estándar: C17
- Flags: -std=c17 -Wall -Wextra -Wpedantic -g - Flags: -std=c17 -Werror -Wall -Wextra -Wpedantic -g
- Makefile único que funcione en Windows (MSYS2) y en Linux - Makefile único que funcione en Windows (MSYS2) y en Linux
# Adaptar la sesión a la máquina
Al empezar cada sesión, antes de proponer nada:
1. Detecta en qué máquina estoy: la plataforma del entorno (`win32` es el
sobremesa con Windows y `linux` el portátil con Fedora) o `uname -s`.
2. Lee PROGRESO.md, incluida la sección "Pendiente por máquina".
3. Dime en una línea en qué máquina estoy y qué toca, sin preguntarme.
- Si el paso actual se puede hacer en esta máquina, sigue con él.
- Si necesita la otra (por ejemplo, valgrind en Windows o Winsock en
Fedora), dímelo, déjalo apuntado en "Pendiente por máquina" y proponme
algo que sí se pueda hacer aquí.
4. Usa los comandos de esta máquina en enunciados y comprobaciones (por
ejemplo, `build/hello.exe` y las rutas de MSYS2 en Windows).
Qué encaja mejor en cada una:
- Windows: lo específico de Windows (Makefile con `.exe`, Winsock, spooler,
WinUSB) y la parte portable sin comprobación de memoria.
- Fedora: validar la parte portable con valgrind y sanitizers (obligatorio
antes de cerrar una fase), sockets POSIX y USB en Linux.
# Arquitectura a construir # Arquitectura a construir
- Núcleo portable: buffer de bytes, constructor de comandos ESC/POS, - Núcleo portable: buffer de bytes, constructor de comandos ESC/POS,
@@ -82,6 +107,14 @@ pregúntame marca y modelo de cada una.
pase AddressSanitizer y valgrind en Fedora. pase AddressSanitizer y valgrind en Fedora.
- Antes de imprimir en papel, comprobamos los bytes en un .bin con un - Antes de imprimir en papel, comprobamos los bytes en un .bin con un
volcado hexadecimal, para no gastar papel en pruebas fallidas. volcado hexadecimal, para no gastar papel en pruebas fallidas.
- Pasos pequeños: me saturo con enunciados largos. Una tarea corta cada vez
y teoría larga en `docs/`, no en el chat.
- Nada de *vibecoding*. En cada paso de código dame solo el enunciado (qué
hace y cómo se comprueba) y pídeme una predicción antes de compilar. Sin
pseudocódigo, esqueletos ni líneas exactas. Si me atasco y pido ayuda,
sube poco a poco: la pista 1 dice dónde mirar, la 2 es más concreta y la
solución solo si la pido. Ante un error, pídeme primero mi diagnóstico.
No corrijas mi código salvo que te lo pida.
# Seguimiento # Seguimiento
@@ -95,6 +128,9 @@ con un índice en `docs/README.md`. Cada vez que explique un concepto nuevo,
añádelo al fichero del tema que corresponda o crea uno nuevo y enlázalo en el añádelo al fichero del tema que corresponda o crea uno nuevo y enlázalo en el
índice. Incluye siempre la comparación con Go. índice. Incluye siempre la comparación con Go.
Mantén `docs/cheatsheet.md` con cada comando nuevo que use, en el momento en
que aparece, agrupado y con una línea que explique qué hace.
# Comunicación # Comunicación
- En español - En español
+16 -7
View File
@@ -1,20 +1,29 @@
CC = gcc CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -Wpedantic -g CFLAGS = -std=c17 -Werror -Wall -Wextra -Wpedantic -g
CPPFLAGS = -Iinclude CPPFLAGS = -Iinclude
all: build/hello all: build/hello
build/escpos.o: src/escpos.c include/escpos.h build/%.o: src/%.c include/escpos.h
$(CC) $(CFLAGS) $(CPPFLAGS) -c src/escpos.c -o build/escpos.o mkdir -p build/
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
build/hello.o: examples/hello.c include/escpos.h build/hello.o: examples/hello.c include/escpos.h
$(CC) $(CFLAGS) $(CPPFLAGS) -c examples/hello.c -o build/hello.o mkdir -p build/
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
build/hello: build/hello.o build/escpos.o build/hello: build/hello.o build/escpos.o build/buffer.o
$(CC) build/hello.o build/escpos.o -o build/hello mkdir -p build/
$(CC) $^ -o $@
clean: clean:
rm -rf build/* rm -rf build
valgrind: build/hello
valgrind --leak-check=full build/hello
run: build/hello
build/hello
.PHONY: all clean .PHONY: all clean
+109 -8
View File
@@ -2,10 +2,43 @@
## Estado actual ## Estado actual
- **Fase 1** — Estructura del proyecto. - **Fase 2**: buffer dinámico. La Fase 1 está cerrada en Fedora y 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. (2026-10-05).
- Entorno activo: Fedora 42 nativo (gcc 15.2, clang 20, make 4.4, gdb 17, - 2026-10-06 (Fedora): antes del 2.3b, repaso de punteros. Le costaba qué
valgrind 3.26). viaja a una función, `*` frente a `->`, la pila frente al montón y restar en
hex. Se creó `docs/02-pointers-visual.html` (interactivo). Pendiente de que
conteste: A) por qué `&buffer_a - &buffer` da 4; B) cuántos bytes copia su
`append` actual con `data_b`.
- Paso actual: 2.3b. `hello.c` ya usa bien `goto cleanup` (punteros a `NULL`
uno por línea, `result`, un solo `return`), sin avisos con gcc ni clang, y
Valgrind limpio. Falta `escpos_buffer_append`: el parámetro con la cantidad,
`const`, reservar `len + cantidad`, `memcpy` y el comentario de la cabecera.
Ahora copia 1 byte (`sizeof *byte`).
- Máquinas: sobremesa con Windows (MSYS2 UCRT64) y portátil con Fedora 42
(gcc 15.2, clang 20, make 4.4, gdb 17, valgrind 3.26). Ver "Pendiente por
máquina".
## Pendiente por máquina
### Windows (sobremesa)
- Hay gcc, gdb y make en `C:\msys64`. Faltan clang y `xxd` (opcionales:
`pacman -S mingw-w64-ucrt-x86_64-clang vim`; mientras, `hexdump -C`).
- Fase 1 cerrada: `make`, `make run` y `Nothing to be done` funcionan sin
`EXE` gracias a la emulación de `.exe` de MSYS2 (ver docs/06-make.md). El
`ifeq ($(OS),Windows_NT)` se pospone a la Fase 4, cuando haga falta
`-lws2_32`.
- Probar en VS Code: terminal nueva (debe abrir UCRT64), Ctrl+Shift+B y F5.
- El 2.3b se puede avanzar aquí; la comprobación de memoria queda para Fedora.
### Fedora (portátil)
- Validar con valgrind el 2.3b cuando esté hecho (y con ASan/UBSan antes de
cerrar la Fase 2).
- Tras el `git pull` del 2026-10-05, comprobar que `make` y F5 siguen igual
(se ha tocado `.vscode/` y se han añadido `.gitattributes` y `.gitignore`).
Los binarios de Go ya no se versionan: recompilarlos con `go build` si hacen
falta.
## Hecho ## Hecho
@@ -13,20 +46,88 @@
la usa. Compila y enlaza a mano en Windows y en Fedora. la usa. Compila y enlaza a mano en Windows y en Fedora.
- `samples/hello_world.bin`: ESC @, texto, LF, ESC d 5, GS V 0. - `samples/hello_world.bin`: ESC @, texto, LF, ESC d 5, GS V 0.
- Emulador ESC/POS en Go (`emulator/`) para probar sin gastar papel. - Emulador ESC/POS en Go (`emulator/`) para probar sin gastar papel.
- Makefile con regla patrón, `clean` y `.PHONY`. Compila desde un clon limpio y
sin avisos con gcc y con clang.
- 2.1: `escpos_buffer_new` / `escpos_buffer_free` con tipo opaco. Valgrind
limpio, y fuga provocada a propósito para leer el informe.
- 2.2: `escpos_buffer_append_byte` con `realloc`: 64 bytes al principio, luego
el doble. Comprobado con 100 bytes (crece de 64 a 128). Valgrind limpio.
Sin el `free`, Valgrind da 24 `definitely` + 128 `indirectly`.
- 2.3a: crecimiento extraído a `static int buffer_reserve(buffer, min_cap)`:
arranca en 64 o en `cap` y dobla hasta que cabe; un único `realloc`.
## Conceptos de C aprendidos ## Conceptos de C aprendidos
- Etapas de compilación, unidades de traducción, cabeceras, *include guards*, - Etapas de compilación, unidades de traducción, cabeceras, *include guards*,
`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 *`, `'A'` frente a `"A"`.
- Memoria virtual de un proceso, modo usuario y modo núcleo. - Memoria virtual de un proceso, modo usuario y modo núcleo. `VmSize` frente a
`VmRSS`, *overcommit* y OOM killer.
- Make: reglas, dependencias por fechas, variables, regla patrón con
`$@ $< $^`, `.PHONY`.
- Pila y montón, `malloc`/`free`/`realloc`, `NULL`, propiedad de punteros,
tipo opaco, `typedef`, `->`, `uint8_t`/`size_t`, qué tipo usar.
- Liberar en caminos de error sin `defer`: patrón `goto cleanup`.
- Depuración: *segfault* (139), `free(): invalid pointer` (134), gdb básico,
y leer errores e informes de fugas en Valgrind.
## Forma de trabajar
- Desde el 2026-10-02: sin *vibecoding*. Enunciado y pruebas, predicción antes
de compilar, y pistas por niveles solo si las pide. Ante un error, primero su
diagnóstico.
## 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` y definido en
`src/buffer.c`. Campos `uint8_t *data`, `size_t len` y `size_t cap`.
- Crecimiento del buffer: 64 bytes al principio y luego el doble.
- Por ahora, las funciones que pueden fallar devuelven `int`: `0` si va bien y
`-1` si no hay memoria. Pendiente de cambiar a códigos de error propios.
- Estilo: nombres en `snake_case` y llave de apertura en línea aparte (estilo
Allman) en funciones, structs y bloques.
- Lo generado va en `build/`, que se crea en las recetas con `mkdir -p` y no se
versiona. Tampoco se versionan ejecutables ni volcados (`vgcore.*`).
- Dos máquinas transparentes: `.gitattributes` fuerza LF en todo y marca
`*.bin` como binario (git no toca los bytes ESC/POS). En Windows, VS Code usa
la shell UCRT64 de MSYS2 para la terminal y las tareas, así que `make`,
`mkdir -p` y `rm -rf` funcionan igual que en Fedora.
## Pendiente / puntos débiles ## Pendiente / puntos débiles
- Hay binarios (`.o`, `.exe`) versionados en git. Falta `.gitignore`. - Fase 1: probar el Makefile en Windows (MSYS2), con el sufijo `.exe`.
- Ficheros sin salto de línea final: corregido; editor configurado. - Make: le costó entender que va hacia atrás desde el objetivo y que una regla
patrón es una plantilla. Repasar con `docs/06-make.md`.
- Punteros: le cuesta saber cuándo hace falta `*` y cuándo no, y distinguir la
dirección del bloque (`data`) de una casilla (`data[i]`). Le funciona la
analogía de papel y casa. Reforzar con ejemplos de su propio código.
- Tipos: usó `uint8_t` para un código de retorno. Repasar la tabla "Qué tipo
usar" de `docs/08-structs.md`.
- Valorar añadir `-Wconversion` a `CFLAGS`: detecta conversiones con pérdida
que gcc no avisa por defecto.
- Bucles: en `buffer_reserve` calculaba cada vuelta a partir de un valor que no
cambiaba (`buffer->cap`) o que empezaba en 0, y eso daba bucles infinitos.
Lo resolvió siguiendo los valores vuelta a vuelta. Seguir pidiéndole trazas
a mano.
- Declaraciones múltiples: escribió `escpos_buffer *a, *b, *c = NULL;` creyendo
que las tres valían `NULL` (solo la última). Lo detectó clang con
`-Wsometimes-uninitialized`. Ahora declara una variable por línea.
- Variables locales sin inicializar: creía que valían `NULL` como en Go.
- `sizeof`: el 2026-10-06 dijo que `sizeof buffer` (un puntero) da 1 byte; da
8 en 64 bits. Y lo veía como algo que "va a la casa" mientras se ejecuta,
cuando se calcula al compilar a partir del tipo.
- Hexadecimal: no sabe restar direcciones (`0xd210 - 0xd1f0`). Se le enseñó
a hacerlo con `p` en gdb y con las potencias de 16. Pide ayuda con
"ni idea": darle herramientas para medir, no la cuenta hecha.
- Se salta la predicción: el 2026-10-06 miró gdb sin predecir tres veces
seguidas. Insistir en que la escriba antes de ejecutar.
- Impresoras físicas: sin registrar marca y modelo. - Impresoras físicas: sin registrar marca y modelo.
- Confunde puntero colgante ("se pierde") con fuga. Tampoco tenía claro por qué
el slice viejo es seguro en Go tras `append` (el GC no libera el array viejo,
no es por la copia). Repasar con la tabla de docs/07-dynamic-memory.md.
## Otros
- `goescpos/`: equivalente en Go idiomático (`bytes.Buffer`) de lo hecho hasta
ahora, para comparar.
+216
View File
@@ -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,208 @@ 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()`).
## Un puntero no sabe cuántos elementos hay
Un slice de Go lleva dentro tres cosas: puntero, `len` y `cap`. Un puntero de
C lleva **solo la dirección del primer elemento**. No hay forma de preguntarle
cuántos hay detrás.
Por eso las funciones de C que reciben un bloque de datos piden siempre **dos
parámetros**: el puntero y la cantidad.
```c
int escpos_buffer_append(escpos_buffer *buffer, const uint8_t *bytes, size_t count);
```
En Go sería un solo parámetro: `func (b *Buffer) Append(bytes []byte)`.
Si el que llama pasa un `count` mayor que lo que hay de verdad, la función lee
fuera del bloque. El compilador no puede detectarlo. Es la fuente de los
desbordamientos de memoria más famosos.
Las cadenas son la excepción: su longitud se deduce porque acaban en `'\0'`.
Pero los datos binarios (un comando ESC/POS, una imagen) pueden contener
bytes `0x00` en medio, así que necesitan su cantidad aparte.
## `const` en parámetros puntero: una promesa
```c
const uint8_t *bytes
```
Se lee "puntero a `uint8_t` que **no voy a modificar**". Es un contrato con el
que llama: le garantizas que sus datos salen intactos. Si dentro de la función
intentas escribir `bytes[0] = 0;`, el compilador da error.
- Pasar un `uint8_t *` normal donde se pide `const uint8_t *` está permitido:
solo añades una restricción.
- Al revés da aviso: estarías quitando una promesa.
**En Go** no existe. Un slice que recibes lo puedes modificar, y el que llama no
tiene ninguna garantía.
## Arrays
```c
uint8_t init[] = { 0x1B, 0x40 }; /* ESC @ */
```
- Un array de tamaño fijo. Si no pones el tamaño entre corchetes, el
compilador lo cuenta a partir de los valores.
- `sizeof init` da el tamaño **en bytes** del array completo (aquí 2). Solo
funciona con el array en su ámbito, no con un puntero.
- Al pasar un array a una función, se convierte en un **puntero a su primer
elemento** (*decay*). La función recibe la dirección, no una copia, y pierde
el tamaño. Por eso hay que pasar `sizeof init` aparte.
**En Go** `[2]byte{0x1B, 0x40}` es un array con su longitud dentro del tipo.
Se pasa por copia, y para pasarlo sin copiar se usa un slice: `init[:]`.
## Aritmética de punteros: `[]` también lleva un `*` escondido
Con dibujos interactivos y preguntas: [02-pointers-visual.html](02-pointers-visual.html).
Sumar un entero a un puntero da **otra dirección**: `p + i` avanza `i`
**elementos**, es decir, `i * sizeof *p` bytes. No va a la casa. Solo calcula
qué casa es.
`p[i]` es una abreviatura de `*(p + i)`: calcula la dirección y va a ella.
Igual que `->`, los corchetes llevan el `*` dentro.
La cadena completa con el buffer:
| Escribes | Equivale a | Tipo | Qué es |
|---|---|---|---|
| `buffer` | | `escpos_buffer *` | El papel (8 bytes) |
| `*buffer` | | `struct escpos_buffer` | La casa (24 bytes) |
| `buffer->data` | `(*buffer).data` | `uint8_t *` | Un papel guardado dentro de la casa, con la dirección del bloque de bytes |
| `buffer->data + i` | `&buffer->data[i]` | `uint8_t *` | La dirección de la casilla `i` |
| `buffer->data[i]` | `*(buffer->data + i)` | `uint8_t` | El byte de la casilla `i` |
Para saber si una expresión lleva `*`, cuenta: `*`, `->` y `[]` desreferencian
una vez cada uno, y `&` quita una desreferencia.
### Qué viaja cuando llamas a una función
En C, a una función le llega siempre **una copia** del valor que le pasas.
- `f(buffer)`: viaja una copia del papel, 8 bytes. La función llega a la misma
casa, así que lo que cambie en ella lo ves tú.
- `f(*buffer)`: viaja una copia de la casa, 24 bytes. Lo que cambie la función
se queda en su copia.
- `f(data)`, con `data` un array de 300 bytes: el array decae a la dirección
de su primera casilla. Viaja un papel de 8 bytes, no los 300 bytes. Dentro de
la función, `sizeof *data` es 1, el tamaño de una casilla.
### Restar direcciones
Las direcciones se escriben en hexadecimal, en base 16: cada posición vale 16
veces la de su derecha, y `a`–`f` valen 10–15.
| Hex | Decimal |
|---|---|
| `0x08` | 8 |
| `0x10` | 16 |
| `0x18` | 24 |
| `0x20` | 32 |
| `0x40` | 64 |
| `0x80` | 128 |
| `0x100` | 256 |
`0x20` es 2 × 16 + 0 = 32. Para no hacer la cuenta a mano:
- En gdb: `p 0xd210 - 0xd1f0` da `32`, y `p/x 32` da `0x20`.
- En bash: `echo $((0xd210 - 0xd1f0))`.
Si restas dos **punteros** del mismo tipo (`p &b - &a`), el resultado no son
bytes: son **elementos**, igual que en la suma. Dos `escpos_buffer *` separados
32 bytes dan `4`, porque cada uno ocupa 8.
**En Go** pasa lo mismo, todo se copia: un `*Buffer` copia el puntero y un
slice copia la cabecera (puntero, len y cap). La diferencia es que en el slice
viaja la longitud, y en C tienes que pasarla tú.
## `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
File diff suppressed because it is too large Load Diff
+115 -2
View File
@@ -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
@@ -92,6 +130,19 @@ Make. Por eso allí todo va en `.PHONY` y la comparación de fechas no se usa
nunca. En C es al revés: Make **es** el sistema de compilación, y solo las nunca. En C es al revés: Make **es** el sistema de compilación, y solo las
acciones (`all`, `clean`) son phony. acciones (`all`, `clean`) son phony.
### Los avisos solo salen al compilar
Un *warning* aparece **solo cuando ese `.c` se compila**. Como el `.o` sí se
genera, el siguiente `make` lo ve al día, no recompila, y el aviso no vuelve a
salir. Es fácil no verlo nunca. Hay tres formas de evitarlo:
- `make clean && make` para verlo todo otra vez.
- `touch src/buffer.c && make` para recompilar solo ese fichero.
- Añadir `-Werror` a `CFLAGS`: convierte los avisos en errores. Así no se
genera el `.o` y el aviso sale en cada `make` hasta que lo arreglas.
**En Go** no pasa: lo que en C son avisos importantes, en Go son errores.
### Variables desde la línea de comandos ### Variables desde la línea de comandos
`make CC=clang` sobrescribe la variable `CC` del Makefile solo para esa `make CC=clang` sobrescribe la variable `CC` del Makefile solo para esa
@@ -103,6 +154,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:
@@ -134,3 +224,26 @@ endif
Más adelante (Fase 4) aquí irán también las librerías que cambian según la Más adelante (Fase 4) aquí irán también las librerías que cambian según la
plataforma, como `-lws2_32` para Winsock en Windows. plataforma, como `-lws2_32` para Winsock en Windows.
### La «magia .exe» de MSYS2
Sin `EXE`, el Makefile con objetivo `build/hello` parece funcionar en Windows:
el segundo `make` dice `Nothing to be done`. No es mérito del Makefile:
- `make`, `ls` y la shell de `/usr/bin` son programas de MSYS2, que emula
POSIX encima de Windows.
- Si un programa de MSYS2 pregunta por `build/hello` y solo existe
`build/hello.exe`, la capa de emulación responde que existe y le da la fecha
del `.exe`.
| Comando | Qué ve |
|---|---|
| `ls -l build/hello` | Pasa por la emulación: «existe». |
| `cmd //c dir build` | Pregunta a Windows: solo hay `hello.exe`. |
Con un make nativo (`mingw32-make`) esa emulación no existe: no encuentra
`build/hello` y vuelve a enlazar cada vez. Por eso se pone el nombre real con
`$(EXE)`.
**Comparación con Go:** `go build` añade `.exe` por sí mismo cuando
`GOOS=windows`. En C, el nombre del ejecutable lo decides tú en el Makefile.
+533
View File
@@ -0,0 +1,533 @@
# 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ó |
+201
View File
@@ -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` |
+131
View File
@@ -0,0 +1,131 @@
# Depurar: *segfault* y gdb
## Qué es un *segmentation fault*
El programa accede a una dirección que no tiene mapeada en su espacio
virtual, o la usa sin permiso (por ejemplo, escribe en memoria de solo
lectura). La MMU no encuentra la traducción, el procesador avisa al núcleo, y
el núcleo envía al proceso la señal `SIGSEGV`, que por defecto lo mata (ver
[03-process-memory.md](03-process-memory.md)).
- La shell lo muestra como `Segmentation fault (core dumped)`.
- El código de salida es **139** = 128 + 11 (`SIGSEGV` es la señal 11). Es la
misma cuenta que el 137 de `SIGKILL` en el OOM killer.
- El caso más típico es escribir o leer a través de un puntero `NULL`. La
página de la dirección 0 nunca se mapea, precisamente para que este error se
detecte siempre.
Ojo: no todo acceso indebido da *segfault*. Si te sales de un bloque pero
caes en memoria que sí es del proceso, no pasa nada visible: corrompes datos
en silencio. Para eso están Valgrind y AddressSanitizer.
**En Go**: el mismo fallo da `panic: runtime error: invalid memory address or
nil pointer dereference`, con la traza de la pila. Go captura el `SIGSEGV` y lo
convierte en un `panic`. En C no hay traza: hay que pedírsela a gdb.
## `free(): invalid pointer` y *Aborted*
glibc comprueba algunas cosas al hacer `free`. Si el puntero no es el inicio de
un bloque que haya dado `malloc`, `calloc` o `realloc`, imprime
`free(): invalid pointer` y llama a `abort()`.
- La shell muestra `Aborted (core dumped)`, con código **134** = 128 + 6
(`SIGABRT`).
- Causas típicas: hacer `free` de la dirección de una variable local (`&x`),
de un literal (`"hola"`), de un puntero al **interior** de un bloque, o de un
bloque ya liberado (*double free*).
**Para depurarlo, mejor Valgrind que gdb.** gdb te dice dónde se cae (en el
`free`), pero el error está antes, donde el puntero recibió un valor malo.
Valgrind te dice **de dónde viene** la dirección:
```
Invalid free() / delete / delete[] / realloc()
at free
by escpos_buffer_free (buffer.c:36)
by main (hello.c:22)
Address 0x1ffefff544 is on thread 1's stack
```
"Is on thread 1's stack": la dirección es de **la pila**, así que nunca la dio
`malloc`. Después busca cada línea que asigna un valor a ese puntero
(`grep -n "data =" src/*.c`) y mira cuál le da una dirección de la pila.
### Método general
1. Lee el mensaje. Valgrind casi siempre dice qué pasó (`Invalid write`,
`Invalid free`), dónde y de qué tipo es la dirección (pila, N bytes después
de un bloque, bloque ya liberado...).
2. El síntoma (donde se cae) no suele ser la causa. Pregúntate de dónde salió
el valor malo.
3. Busca todas las asignaciones de esa variable y descarta una a una.
## gdb: lo mínimo
Hace falta compilar con `-g`. El Makefile ya lo hace.
```
gdb ./build/hello
```
| Comando de gdb | Qué hace |
|---|---|
| `run` (`r`) | Ejecuta el programa. Si se cuelga, gdb se para en la línea exacta |
| `bt` (*backtrace*) | Muestra la pila de llamadas: quién llamó a quién hasta llegar aquí |
| `print expr` (`p`) | Muestra el valor de una variable o expresión: `p buffer->data` |
| `break fichero.c:línea` (`b`) | Pone un punto de parada |
| `next` (`n`) | Ejecuta la línea actual sin entrar en las funciones |
| `step` (`s`) | Ejecuta la línea actual entrando en las funciones |
| `continue` (`c`) | Sigue hasta el siguiente punto de parada |
| `quit` (`q`) | Sale |
### Programa que no termina (bucle infinito)
1. `gdb ./build/hello` y `run`.
2. Cuando lleve un rato colgado, pulsa **Ctrl+C**: gdb detiene el programa
donde esté y te muestra la línea.
3. `bt` para ver en qué función está, y `print` de las variables del bucle
(`print new_cap`). Si al repetir `next` varias veces la variable no cambia,
has encontrado el problema.
Ejemplo real: un bucle `while (new_cap < min_cap) { new_cap *= 2; }` con
`new_cap` empezando en 0. Cero por dos es cero, así que el bucle no termina
nunca. `print new_cap` da `0` en cada vuelta.
Al arrancar, Fedora pregunta si quieres descargar símbolos de depuración del
sistema (*debuginfod*). Para nuestro código no hacen falta: responde `n`.
**En Go** el equivalente es Delve (`dlv debug`), con comandos casi iguales.
## Depurar en VS Code
La extensión C/C++ (`ms-vscode.cpptools`) lanza **el mismo gdb** y le envía
órdenes por su protocolo de texto (gdb/MI). Cada clic equivale a un comando:
| VS Code | gdb |
|---|---|
| Clic a la izquierda del número de línea (punto rojo) | `break fichero.c:línea` |
| F5 | `run` / `continue` |
| F10 | `next` |
| F11 | `step` |
| Shift+F11 | `finish` (salir de la función actual) |
| Panel *Variables*, pasar el ratón por encima o panel *Watch* | `print` |
| Panel *Call Stack* | `bt` |
| Botón de pausa | Ctrl+C |
Configuración del proyecto, en `.vscode/`:
- `tasks.json`: tareas `make`, `make clean`, `make (clang)` y `valgrind`.
Se lanzan con *Terminal → Run Task*, y Ctrl+Shift+B ejecuta `make`.
- `launch.json`: depura `build/hello` con gdb y ejecuta `make` antes. Tiene ya
los valores para Windows (MSYS2 UCRT64).
- `c_cpp_properties.json`: dice a IntelliSense dónde están las cabeceras
(`include/`) y que el estándar es C17, para que el editor avise de lo mismo
que gcc.
- `settings.json`: salto de línea final, sin espacios al final de línea, y
tabuladores reales en el Makefile.
Para ver un puntero como array en el panel *Watch*: `*buffer->data@10` (los 10
primeros bytes de `data`). Es sintaxis de gdb y funciona igual en su consola.
**En Go**, la extensión de Go hace lo mismo con Delve.
+18 -1
View File
@@ -8,7 +8,12 @@ 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, aritmética de
punteros (`p + i`, `p[i]`) y qué viaja al llamar a una función, un puntero no sabe
cuántos elementos hay, `const` en parámetros, arrays y el formato de
`printf`. Versión interactiva con dibujos:
[02-pointers-visual.html](02-pointers-visual.html) (ábrela en el navegador).
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 +26,18 @@ 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, `memcpy` y `restrict`, desbordamiento al calcular tamaños, fugas, liberar en caminos
de error sin `defer` (`goto cleanup`), 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, los comandos mínimos de gdb y la depuración visual en
VS Code.
[Chuleta de comandos](cheatsheet.md): todos los comandos usados, agrupados por [Chuleta de comandos](cheatsheet.md): todos los comandos usados, agrupados por
tarea. tarea.
+51
View File
@@ -30,8 +30,48 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes.
| `objdump --dwarf=info fichero.o` | Muestra la información de depuración que añade `-g`. | | `objdump --dwarf=info fichero.o` | Muestra la información de depuración que añade `-g`. |
| `xxd fichero.bin` | Volcado hexadecimal (para revisar los bytes ESC/POS). | | `xxd fichero.bin` | Volcado hexadecimal (para revisar los bytes ESC/POS). |
| `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`). |
| `man 3 memcpy` | Documentación de una función de la librería de C: cabecera, firma y comportamiento. `man 2` para llamadas al sistema. |
| `echo \| gcc -std=c17 -dM -E - \| grep __STDC_VERSION__` | Qué versión del estándar de C usa el compilador: `201710L` es C17, `202311L` es C23. |
| `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. |
| `Ctrl+C` (dentro de gdb, con el programa en marcha) | Detiene el programa donde esté. Para encontrar bucles infinitos. |
| `break f.c:42` / `next` / `step` / `continue` | Dentro de gdb: punto de parada, siguiente línea, entrar en la función, seguir. |
| `p &x` / `p *p` / `p/x *p@n` | Dentro de gdb: dirección de `x`, lo que hay en la dirección de `p`, y `n` elementos seguidos desde `p` en hexadecimal. |
| `info proc mappings` | Dentro de gdb: mapa de memoria del proceso (`[heap]`, `[stack]`, librerías). Dice en qué zona cae una dirección. |
### VS Code
| Atajo | Qué hace |
|---|---|
| F5 | Compila con `make` y depura `build/hello` |
| F9 o clic junto al número de línea | Punto de parada |
| F10 / F11 / Shift+F11 | Siguiente línea / entrar en la función / salir de la función |
| Ctrl+Shift+B | `make` |
| *Terminal → Run Task* | `make clean`, `make (clang)`, `valgrind` |
| `*buffer->data@10` en *Watch* | Ver los 10 primeros bytes de un puntero |
## Solo en Windows (MSYS2) ## Solo en Windows (MSYS2)
| Comando | Qué hace | | Comando | Qué hace |
@@ -39,6 +79,10 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes.
| `objdump -p hello.exe \| grep "DLL Name"` | Lista las DLL de las que depende el ejecutable. | | `objdump -p hello.exe \| grep "DLL Name"` | Lista las DLL de las que depende el ejecutable. |
| `strip hello.exe` | Quita símbolos e información de depuración. | | `strip hello.exe` | Quita símbolos e información de depuración. |
| `gcc -static ...` | Enlaza estáticamente todas las librerías posibles. | | `gcc -static ...` | Enlaza estáticamente todas las librerías posibles. |
| `uname -s` | Nombre del sistema: `Linux` en Fedora, `UCRT64_NT-...` en la terminal UCRT64 de Windows. |
| `echo $MSYSTEM` | Entorno de MSYS2 activo. Tiene que ser `UCRT64`. |
| `pacman -S mingw-w64-ucrt-x86_64-clang` | Instala clang para el entorno UCRT64. |
| `pacman -S vim` | Instala `xxd` (viene con vim). Sin él, usa `hexdump -C fichero.bin`. |
## Make ## Make
@@ -61,6 +105,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
Binary file not shown.
+41 -3
View File
@@ -1,11 +1,49 @@
#include <stdio.h> #include <stdio.h>
#include "escpos.h" #include "escpos.h"
int main(void) { int main(void)
{
int result = 1;
const char *version = escpos_version(); const char *version = escpos_version();
escpos_buffer *buffer = NULL;
escpos_buffer *buffer_a = NULL;
escpos_buffer *buffer_b = NULL;
printf("%s\n", version); printf("%s\n", version);
return 0; buffer = escpos_buffer_new();
if (buffer == NULL) { goto cleanup; }
for (int i = 0; i < 100; i++)
{
int err = escpos_buffer_append_byte(buffer, 'A');
if (err == -1) { goto cleanup; }
}
buffer_a = escpos_buffer_new();
if (buffer_a == NULL) { goto cleanup; }
uint8_t data[] = {0x1B, 0x40};
int err = 0;
err = escpos_buffer_append(buffer_a, data, sizeof data);
if (err == -1) { goto cleanup; }
buffer_b = escpos_buffer_new();
if (buffer_b == NULL) { goto cleanup; }
uint8_t data_b[300];
for (int i = 0; i < 300; i++)
{
data_b[i] = 'A';
}
err = escpos_buffer_append(buffer_b, data_b, sizeof data_b);
if (err == -1) { goto cleanup; }
result = 0;
cleanup:
escpos_buffer_free(buffer);
escpos_buffer_free(buffer_a);
escpos_buffer_free(buffer_b);
return result;
} }
+21
View File
@@ -0,0 +1,21 @@
// Equivalente a examples/hello.c.
package main
import (
"bytes"
"fmt"
"goescpos/escpos"
)
func main() {
fmt.Println(escpos.Version)
// bytes.Buffer sustituye a todo src/buffer.c: crece solo y lo libera el GC.
var buf bytes.Buffer
for range 100 {
buf.WriteByte('A')
}
fmt.Printf("len=%d cap=%d\n", buf.Len(), buf.Cap())
}
+5
View File
@@ -0,0 +1,5 @@
// Package escpos es el equivalente en Go de include/escpos.h + src/escpos.c.
package escpos
// Version equivale a escpos_version().
const Version = "1.0.0"
+3
View File
@@ -0,0 +1,3 @@
module goescpos
go 1.26
Binary file not shown.
+33 -1
View File
@@ -1,6 +1,38 @@
#ifndef ESCPOS_H #ifndef ESCPOS_H
#define ESCPOS_H #define ESCPOS_H
#include <stdint.h>
#include <stddef.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 del buffer.
* Devuelve 0 si va bien, o -1 si no hay memoria; en ese caso el buffer
* queda intacto.
*/
int escpos_buffer_append_byte(escpos_buffer *buffer, uint8_t byte);
/*
* Añade los bytes al final del buffer
*/
int escpos_buffer_append(escpos_buffer *buffer, uint8_t byte[], size_t size);
#endif #endif
+103
View File
@@ -0,0 +1,103 @@
#include "escpos.h"
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
struct escpos_buffer
{
uint8_t *data;
size_t len;
size_t cap;
};
static int buffer_reserve(escpos_buffer *buffer, size_t min_cap)
{
if (min_cap <= buffer->cap)
{
return 0;
}
size_t new_cap;
if (buffer->cap == 0)
{
new_cap = 64;
} else
{
new_cap = buffer->cap;
}
while (new_cap < min_cap)
{
new_cap *= 2;
}
uint8_t *tmp = realloc(buffer->data, new_cap);
if (tmp == NULL)
{
return -1;
}
buffer->data = tmp;
buffer->cap = new_cap;
return 0;
}
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)
{
int err = buffer_reserve(buffer, buffer->len + 1);
if (err == -1)
{
return -1;
}
buffer->data[buffer->len] = byte;
buffer->len++;
return 0;
}
int escpos_buffer_append(escpos_buffer *buffer, uint8_t byte[], size_t size)
{
int err = buffer_reserve(buffer, buffer->len + size);
if (err == -1)
{
return -1;
}
memcpy(buffer->data, byte, size);
return 0;
}
+2 -1
View File
@@ -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";
} }