From 4351a40bd2296e9582e9eee9e7dfaa89252e3f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Wed, 7 Oct 2026 03:45:29 +0200 Subject: [PATCH] progress --- PROGRESO.md | 13 + docs/02-pointers-and-strings.md | 64 ++ docs/02-pointers-visual.html | 1016 +++++++++++++++++++++++++++++++ docs/07-dynamic-memory.md | 57 ++ docs/README.md | 8 +- docs/cheatsheet.md | 3 + examples/hello.c | 4 +- include/escpos.h | 13 +- src/buffer.c | 11 +- 9 files changed, 1171 insertions(+), 18 deletions(-) create mode 100644 docs/02-pointers-visual.html diff --git a/PROGRESO.md b/PROGRESO.md index 3c692a8..d7ffc6e 100644 --- a/PROGRESO.md +++ b/PROGRESO.md @@ -4,6 +4,11 @@ - **Fase 2**: buffer dinámico. La Fase 1 está cerrada en Fedora y en Windows (2026-10-05). +- 2026-10-06 (Fedora): antes del 2.3b, repaso de punteros. Le costaba qué + 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, @@ -109,6 +114,14 @@ 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. - 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, diff --git a/docs/02-pointers-and-strings.md b/docs/02-pointers-and-strings.md index 4ceb2bc..5481fec 100644 --- a/docs/02-pointers-and-strings.md +++ b/docs/02-pointers-and-strings.md @@ -196,6 +196,70 @@ uint8_t init[] = { 0x1B, 0x40 }; /* ESC @ */ **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 El primer argumento de `printf` es el **formato**. Nunca pases ahí una cadena diff --git a/docs/02-pointers-visual.html b/docs/02-pointers-visual.html new file mode 100644 index 0000000..68231c8 --- /dev/null +++ b/docs/02-pointers-visual.html @@ -0,0 +1,1016 @@ + + + + + +Papel y casa + + + + + + +
+ +
+

Papel y casa

+

Punteros en C dibujados con las direcciones reales que sacaste con gdb de build/hello. En cada pantalla, primero predices y luego miras.

+
+ papel: un puntero, 8 bytes con una dirección + pila: variables locales + montón: lo que sale de malloc/realloc +
+
+ + + + +
+
+

Dónde vive cada cosa

+

El momento: el punto de parada en la línea 42 de hello.c. buffer tiene 100 bytes 'A' y buffer_a tiene un 0x1B. Contesta las preguntas y el mapa se irá rellenando.

+
+
+
+ +
+
Pila · [stack]0x7fff…
+
+
+
buffer
0x4052b0
en 0x7fffffffd520 · 8 B
+
+
0x20 = 32 bytes
+
+
buffer_a
0x405360
en 0x7fffffffd540 · 8 B
+
+
+
+
entre las dos zonas hay unos 128 TB de direcciones sin usar
+
+
Montón · [heap]0x40…
+
+
+
*buffer0x4052b0 · 24 B
+
+
data · +00x4052d0
+
len · +8100
+
cap · +16128
+
+
+
+
bloque de buffer0x4052d0 · 128 B
+
+
100 usados · 28 libres
+
+
+
+
+
*buffer_a0x405360 · 24 B
+
+
data · +00x405380
+
len · +81
+
cap · +1664
+
+
+
+
bloque de buffer_a0x405380 · 64 B
+
+
1 usado · 63 libres
+
+
+
+

Las cajas con ? aparecen cuando contestes.

+
+ +
+
En Go

b := &Buffer{} también puede dejar el papel en la pila y la casa en el montón. La diferencia es que lo decide el compilador (escape analysis) y tú no lo ves. En C lo decides tú: si lo pides con malloc, va al montón.

+
+ + + + + + + + + + + + + +
Direcciones de gdb en Fedora (2026-10-06). En tu ejecución pueden cambiar, pero las distancias entre ellas se mantienen. Teoría en 02-pointers-and-strings.md y 07-dynamic-memory.md.
+
+ + + + diff --git a/docs/07-dynamic-memory.md b/docs/07-dynamic-memory.md index 577c55d..4519976 100644 --- a/docs/07-dynamic-memory.md +++ b/docs/07-dynamic-memory.md @@ -52,6 +52,30 @@ 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//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//status` muestra dos cifras: @@ -243,6 +267,39 @@ Comparado con el `copy(dst, src)` de Go: - 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 diff --git a/docs/README.md b/docs/README.md index 14dd11a..6ee6e97 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,9 +9,11 @@ Ordenados por tema, en el orden en que se ven en el proyecto. flags, qué ve quien recibe los binarios, `nm` y errores del enlazador. 2. [Punteros y cadenas](02-pointers-and-strings.md): cadenas terminadas en `'\0'`, `const char *`, los dos significados de `*`, dónde va el `*`, - nombres de parámetros, paso por valor frente a puntero, un puntero no sabe + 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`. + `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 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. @@ -26,7 +28,7 @@ Ordenados por tema, en el orden en que se ven en el proyecto. 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 desbordamiento al calcular tamaños, fugas, liberar en caminos + 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 diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md index ec7fe56..906378f 100644 --- a/docs/cheatsheet.md +++ b/docs/cheatsheet.md @@ -31,6 +31,7 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes. | `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`). | | `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. | ## Memoria @@ -57,6 +58,8 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes. | `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 diff --git a/examples/hello.c b/examples/hello.c index 4f3099f..676db87 100644 --- a/examples/hello.c +++ b/examples/hello.c @@ -25,7 +25,7 @@ int main(void) uint8_t data[] = {0x1B, 0x40}; int err = 0; - err = escpos_buffer_append(buffer_a, data); + err = escpos_buffer_append(buffer_a, data, sizeof data); if (err == -1) { goto cleanup; } buffer_b = escpos_buffer_new(); @@ -36,7 +36,7 @@ int main(void) data_b[i] = 'A'; } - err = escpos_buffer_append(buffer_b, data_b); + err = escpos_buffer_append(buffer_b, data_b, sizeof data_b); if (err == -1) { goto cleanup; } result = 0; diff --git a/include/escpos.h b/include/escpos.h index 07b797b..d63dca1 100644 --- a/include/escpos.h +++ b/include/escpos.h @@ -1,12 +1,13 @@ #ifndef ESCPOS_H #define ESCPOS_H #include - + #include + 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. @@ -15,7 +16,7 @@ escpos_buffer *escpos_buffer_new(void); /* - * Libera el buffer y toda su memoria. Si recibe NULL no hace nada. + * 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); @@ -31,7 +32,7 @@ /* * Añade los bytes al final del buffer */ - int escpos_buffer_append(escpos_buffer *buffer, uint8_t byte[]); - - + int escpos_buffer_append(escpos_buffer *buffer, uint8_t byte[], size_t size); + + #endif diff --git a/src/buffer.c b/src/buffer.c index ff3cc07..3a23dbf 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -2,6 +2,7 @@ #include #include #include +#include struct escpos_buffer @@ -88,19 +89,15 @@ int escpos_buffer_append_byte(escpos_buffer *buffer, uint8_t byte) return 0; } -int escpos_buffer_append(escpos_buffer *buffer, uint8_t byte[]) +int escpos_buffer_append(escpos_buffer *buffer, uint8_t byte[], size_t size) { - int err = buffer_reserve(buffer, buffer->len + sizeof *byte); + int err = buffer_reserve(buffer, buffer->len + size); if (err == -1) { return -1; } - for (size_t i = 0; i < sizeof *byte; i++) - { - buffer->data[buffer->len] = byte[i]; - buffer->len++; - } + memcpy(buffer->data, byte, size); return 0; }