This commit is contained in:
2026-10-07 03:45:29 +02:00
parent 208a3e9738
commit 4351a40bd2
9 changed files with 1171 additions and 18 deletions
+13
View File
@@ -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,
+64
View File
@@ -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
File diff suppressed because it is too large Load Diff
+57
View File
@@ -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/<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:
@@ -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
+5 -3
View File
@@ -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
+3
View File
@@ -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
+2 -2
View File
@@ -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;
+2 -1
View File
@@ -1,6 +1,7 @@
#ifndef ESCPOS_H
#define ESCPOS_H
#include <stdint.h>
#include <stddef.h>
const char *escpos_version(void);
@@ -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
+4 -7
View File
@@ -2,6 +2,7 @@
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
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;
}