memcpy
This commit is contained in:
@@ -139,6 +139,63 @@ entero**. Eso tiene dos problemas:
|
||||
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[:]`.
|
||||
|
||||
## `printf` y el formato
|
||||
|
||||
El primer argumento de `printf` es el **formato**. Nunca pases ahí una cadena
|
||||
|
||||
@@ -130,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
|
||||
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
|
||||
|
||||
`make CC=clang` sobrescribe la variable `CC` del Makefile solo para esa
|
||||
|
||||
@@ -171,6 +171,37 @@ solo entonces asignarlo.
|
||||
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
|
||||
@@ -182,6 +213,63 @@ 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.
|
||||
|
||||
**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
|
||||
@@ -265,6 +353,57 @@ dice qué hacer con cada una.
|
||||
El código Go que llame a `escpos_buffer_new` tendrá que hacer
|
||||
`defer C.escpos_buffer_free(b)`, igual que con un fichero.
|
||||
|
||||
## Liberar en los caminos de error: C no tiene `defer`
|
||||
|
||||
En Go, `defer b.Close()` se ejecuta salga la función por donde salga. En C no
|
||||
existe nada así: cada `return` tiene que dejar liberado todo lo que se creó
|
||||
antes. Con varios recursos, repetir los `free` delante de cada `return` es
|
||||
largo y es fácil olvidarse de alguno.
|
||||
|
||||
El patrón clásico de C es **un único punto de limpieza al final**, al que se
|
||||
salta con `goto`:
|
||||
|
||||
```c
|
||||
int run(void)
|
||||
{
|
||||
int result = 1; /* se supone fallo hasta llegar al final */
|
||||
thing *a = NULL; /* todo a NULL desde el principio */
|
||||
thing *b = NULL;
|
||||
|
||||
a = thing_new();
|
||||
if (a == NULL) { goto cleanup; }
|
||||
|
||||
b = thing_new();
|
||||
if (b == NULL) { goto cleanup; }
|
||||
|
||||
/* ... trabajo ... */
|
||||
|
||||
result = 0; /* todo ha ido bien */
|
||||
|
||||
cleanup: /* etiqueta: destino del goto */
|
||||
thing_free(b); /* seguro aunque sea NULL */
|
||||
thing_free(a);
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
Por qué funciona:
|
||||
|
||||
- Todos los punteros empiezan en `NULL`, y la función de liberar acepta `NULL`
|
||||
sin hacer nada. Por eso `escpos_buffer_free` se diseñó así. Da igual en qué
|
||||
punto falle: la limpieza libera lo que exista e ignora lo demás.
|
||||
- Hay un solo `return`. Al añadir un recurso nuevo, se añade su `free` en un
|
||||
solo sitio.
|
||||
- Se libera en orden inverso al de creación, como hace `defer` (LIFO).
|
||||
|
||||
`goto` tiene mala fama por el código "espagueti", pero este uso, saltar
|
||||
**hacia delante** a la limpieza, es idiomático. El núcleo de Linux lo usa por
|
||||
todas partes.
|
||||
|
||||
gcc y clang tienen una extensión, `__attribute__((cleanup(f)))`, que se
|
||||
parece a `defer`. No es C estándar y no funciona en MSVC, así que en este
|
||||
proyecto no se usa.
|
||||
|
||||
## Valgrind
|
||||
|
||||
`valgrind` ejecuta tu programa en una CPU simulada y vigila cada acceso a
|
||||
|
||||
@@ -79,7 +79,53 @@ gdb ./build/hello
|
||||
| `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.
|
||||
|
||||
+6
-3
@@ -9,7 +9,8 @@ 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 y el formato de
|
||||
nombres de parámetros, paso por valor frente a puntero, un puntero no sabe
|
||||
cuántos elementos hay, `const` en parámetros, arrays y el formato de
|
||||
`printf`.
|
||||
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
|
||||
@@ -25,14 +26,16 @@ 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, fugas, propiedad de
|
||||
estrategia de crecimiento, `memcpy` y 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 y los comandos mínimos de gdb.
|
||||
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
|
||||
tarea.
|
||||
|
||||
@@ -30,6 +30,7 @@ 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`. |
|
||||
| `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 $?` | Código de salida del último comando: `0` es éxito. |
|
||||
|
||||
## Memoria
|
||||
@@ -54,8 +55,20 @@ Si juntas dos flags de parada (`-E -S`), gana la que para antes.
|
||||
|---|---|
|
||||
| `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. |
|
||||
|
||||
### 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)
|
||||
|
||||
| Comando | Qué hace |
|
||||
|
||||
Reference in New Issue
Block a user