initial commit

This commit is contained in:
2026-09-30 10:22:42 +02:00
commit 3b0f5e365f
32 changed files with 4498 additions and 0 deletions
+283
View File
@@ -0,0 +1,283 @@
# Compilación y enlazado
## Las etapas de `gcc`
`gcc` no es un único programa: es un *driver* que llama a otros por turnos.
```
-E para aquí -S para aquí -c para aquí
│ │ │
.c ── cpp ──▶ .i ── cc1 ──▶ .s ── as ──▶ .o ── ld ──▶ .exe
preprocesador compilador ensamblador enlazador
```
- **Preprocesador**: procesa `#include` y `#define`. Solo manipula texto.
- **Compilador**: traduce C a ensamblador y comprueba tipos.
- **Ensamblador**: traduce el ensamblador a código máquina y produce un `.o`,
llamado *fichero objeto*.
- **Enlazador** (`ld`): junta los `.o` y las librerías en un ejecutable.
Sin flags de parada, gcc hace la cadena completa. Si no le das `-o`, el
ejecutable se llama `a.exe` en Windows y `a.out` en Linux.
**En Go**: `go build` hace lo mismo por dentro (`go tool compile` genera un
`.o` por paquete y `go tool link` los enlaza), pero no lo ves.
### Cómo compila Go
Go **no pasa por C**. Su compilador traduce Go a una representación
intermedia (SSA) y de ahí directamente a código máquina, con su propio
ensamblador y su propio enlazador. No usa `gcc` ni `ld`.
- El compilador de Go estuvo escrito en C hasta Go 1.5 (2015). Después se
tradujo a Go y ahora se compila a sí mismo.
- **Excepción, cgo**: si un fichero tiene `import "C"`, `go build` llama a
`gcc` para compilar la parte de C, y el resultado se enlaza con el de Go.
Así funcionarán los bindings de la fase 12.
- `go build -x` muestra los comandos que ejecuta por dentro, y
`go build -gcflags=-S` muestra el ensamblador generado.
- Un "hola mundo" en Go ocupa unos 2 MB porque incluye el *runtime*
(recolector de basura, planificador de goroutines). El de C es mucho más
pequeño porque usa la librería de C del sistema (en Windows, la UCRT, una
DLL).
### Enlazado estático frente a dinámico
- **Estático**: el código de las librerías se copia dentro del ejecutable.
Funciona en cualquier máquina sin instalar nada, pero ocupa más. Go lo hace
por defecto, y por eso es tan fácil de desplegar.
- **Dinámico**: el ejecutable guarda solo una referencia a la DLL (o `.so`),
y el sistema operativo la carga al arrancar el programa. Ocupa menos, pero
la DLL tiene que estar instalada en la máquina y en una versión compatible.
Es lo que hace gcc por defecto con la librería de C.
Comandos para medirlo en MSYS2:
- `objdump -p hello.exe | grep "DLL Name"`: lista las DLL de las que depende.
- `strip hello.exe`: quita símbolos e información de depuración.
- `gcc -static ...`: enlaza estáticamente también la librería de C.
- En Go, `go build -ldflags="-s -w"` es el equivalente de `strip`.
En MSYS2 UCRT64, `-static` no mete la librería de C: la UCRT es un componente
de Windows y solo existe como DLL. `-static` afecta a las demás librerías
(libgcc, winpthread...). Por eso un "hola mundo" ocupa lo mismo con y sin
`-static`.
### Tres formas de distribuir software
| Modelo | Ejemplos | Ventajas | Inconvenientes |
|---|---|---|---|
| **Todo dentro del ejecutable** (estático) | Go, herramientas "portables" | Un único fichero, no depende de nada | Ocupa más. Si una librería tiene un fallo de seguridad, **cada** programa que la lleva dentro hay que recompilarlo y redistribuirlo |
| **Ejecutable con sus DLL al lado** | Programas típicos de Windows en `Program Files` | El fabricante controla las versiones; puede actualizar una DLL por separado; varios ejecutables del mismo producto la comparten | Cada programa lleva su copia. Riesgo de *DLL hijacking*: Windows busca primero en la carpeta del ejecutable |
| **Librerías compartidas del sistema** | Linux con `dnf`/`apt`; en Windows, el "Visual C++ Redistributable" o el runtime de .NET | Una sola copia para todo el sistema: un parche de seguridad arregla todos los programas a la vez | Hay que instalar las dependencias en la versión correcta (*dependency hell*) |
Cuando compilas desde el código fuente en Linux, además de la librería
(`libfoo`) hay que instalar su paquete `-dev` o `-devel`, que trae **la
cabecera y el fichero para el enlazador**: las dos mitades del "import" de C.
Las licencias también cuentan. Una librería LGPL enlazada estáticamente
obliga a permitir que el usuario la vuelva a enlazar con otra versión, y por
eso se suele distribuir como DLL.
## Unidades de traducción
Cada `.c` se compila **por separado y a ciegas**: cuando gcc compila `hello.c`
no sabe nada de `escpos.c`. Cada `.c`, junto con todo lo que incluye, forma una
*unidad de traducción*.
- **Declaración**: `const char *escpos_version(void);`. Anuncia que la
función existe y con qué firma. Puede repetirse las veces que quieras.
- **Definición**: la función con su cuerpo `{ ... }`. Debe aparecer **una sola
vez** en todo el programa. Es la *regla de una definición*.
- **`static`** delante de una función la hace privada a su `.c`. Es lo más
parecido a la minúscula inicial de Go.
## Por qué compilar a `.o` y enlazar después
1. **Compilación incremental**: si solo cambia un `.c`, recompilas ese y vuelves
a enlazar. Go lo hace solo con su caché (`go env GOCACHE`); en C se encarga
el Makefile.
2. **Distribuir librerías sin el código fuente**: se entrega la cabecera más los
`.o` empaquetados (`.a` o `.dll`). Así será la librería que use cgo.
### Qué ve quien recibe los binarios
No recibe el `.c`, pero **no todo queda oculto**:
- **Nombres de los símbolos exportados** (`T`): el enlazador los necesita para
emparejarlos.
- **Cadenas literales**: salen en claro con `strings fichero.o`.
- **Con `-g`**: nombres de variables y tipos, rutas de los ficheros fuente y
la línea de código de cada instrucción (se ven con `objdump --dwarf=info`).
Para distribuir, se compila sin `-g` o se pasa `strip` al resultado.
- **El código máquina**: se desensambla con `objdump -d` y se puede
descompilar a un C aproximado con herramientas como Ghidra.
Entregar solo binarios dificulta leer el código, pero **no lo protege**. La
cabecera, en cambio, es texto que ve todo el mundo: es el contrato público.
Un `.o` solo sirve para la plataforma en la que se compiló (arquitectura,
sistema operativo, ABI). Un `.o` de Windows x86-64 no sirve en Android ARM:
hay que generar uno por cada plataforma.
**En Go**: las librerías se distribuyen casi siempre como código fuente. Los
paquetes solo binarios se eliminaron en Go 1.13. Un binario de Go se
desensambla igual que uno de C.
## Cabeceras e `#include`
En Go, `import` hace dos cosas a la vez. En C son dos pasos separados, y los
hacen dos programas distintos:
| | Para qué | Quién la usa | Cómo se indica |
|---|---|---|---|
| **Cabecera** (`.h`) | Conocer las firmas y comprobar tipos | Compilador | `#include` + `-I carpeta` |
| **Código** (`.o`, `.a`, `.dll`) | El cuerpo de las funciones | Enlazador | Pasarle el fichero en la línea de comandos |
- `#include` **no es un import**: el preprocesador copia y pega el texto del
`.h`, sin más. No trae código compilado.
- `#include <x.h>` busca en las carpetas de `-I` y luego en las del sistema,
**nunca en la carpeta del fichero que incluye**. Se usa para cabeceras del
sistema y de librerías externas.
- `#include "x.h"` busca primero en la carpeta del fichero que incluye y
después igual que `< >`. Se usa para las cabeceras del proyecto.
- No pongas rutas en el `#include` (`"include/escpos.h"`). Escribe
`"escpos.h"` y di dónde buscarlo con `-I include`.
- `-I` recibe una **carpeta**, no un fichero.
- Cada `.c` incluye **su propia cabecera**. Así el compilador comprueba que la
declaración y la definición coinciden. Si no la incluye y difieren, nadie lo
detecta, y el resultado es comportamiento indefinido.
- En las cabeceras van declaraciones, tipos y macros. **Nunca cuerpos de
funciones**: cada `.c` que incluyera la cabecera tendría su propia definición
y el enlazador daría `multiple definition`.
- Las funciones `static` van en el `.c`. Metidas en un `.h`, cada `.o` recibe
una copia privada: el código se duplica y aparecen avisos de "no usada".
### Encapsulación con cabeceras
La cabecera oculta **funciones** (lo que no declara y es `static` no existe
para el cliente), pero tiene límites:
- **Un `struct` definido en la cabecera no tiene campos privados.** Todos son
visibles y modificables. Además, su tamaño y el orden de sus campos pasan a
formar parte del contrato: si cambian, los programas que usan la librería
tienen que recompilarse.
- **No existe el nivel "paquete".** En Go, lo que no se exporta se comparte
entre todos los ficheros del paquete. En C, `static` es privado **al
fichero**. Para compartir algo entre dos `.c` de la librería tiene que ser
no-`static`, y entonces es visible para todo el mundo. Se resuelve con
cabeceras internas que no se entregan y controlando qué exporta la `.dll` o
la `.so`.
- **No hay protección en tiempo de ejecución.** Todo es memoria compartida:
con un puntero se puede escribir cualquier byte. La encapsulación la impone
el compilador, no el programa en marcha. En Go (`unsafe`, `reflect`) y en
Java (reflexión) pasa lo mismo.
La encapsulación de verdad se consigue con el **tipo opaco** (el *handle
opaco* de la API): la cabecera declara que el `struct` existe pero no lo
define, y la definición va en el `.c`. El cliente solo puede tener punteros a
él y llamar a funciones de la librería; ni siquiera conoce su tamaño ni sus
campos. Precio: hay que crearlo en memoria dinámica y cada acceso es una
llamada a función.
### *Include guard*
```c
#ifndef ESCPOS_H
#define ESCPOS_H
/* todo el contenido de la cabecera */
#endif
```
Es un `if` del preprocesador: la segunda vez que la cabecera se incluye en la
misma unidad de traducción, `ESCPOS_H` ya está definido y se salta el bloque
entero. El `#endif` tiene que ser la **última línea**; si no, lo que queda
fuera se copia siempre.
Una declaración de función repetida no da error, pero la definición de un
`struct` repetida sí (`redefinition of 'struct ...'`), y la librería tendrá
structs en la cabecera.
## `f()` frente a `f(void)`
En C17, `int f();` significa "parámetros **sin especificar**", así que
`f(1, 2, 3)` compila sin avisar. Para decir "sin parámetros" hay que escribir
`int f(void);`. En C23 ya significan lo mismo, pero este proyecto usa C17.
## Flags de gcc
| Flag | Etapa | Qué hace |
|---|---|---|
| `-std=c17` | compilación | Versión del lenguaje. Sin ella, gcc usa su dialecto con extensiones. |
| `-Wall -Wextra` | compilación | Activan avisos. `-Wall` no activa todos, pese al nombre. |
| `-Wpedantic` | compilación | Avisa de extensiones de gcc que no son C estándar. |
| `-g` | compilación | Añade información de depuración (secciones `.debug_*`) para gdb. |
| `-I carpeta` | preprocesador | Añade una carpeta donde buscar los `#include`. |
| `-E` / `-S` / `-c` | control | Paran tras preprocesar, compilar o ensamblar. |
| `-o nombre` | control | Nombre del fichero de salida. |
Todo menos `-o` se usa **al compilar**. Al enlazar solo se pasan los `.o`,
`-o` y, más adelante, las librerías (por ejemplo, `-lws2_32` para Winsock).
**En Go**: los avisos importantes son errores obligatorios (un import o una
variable sin usar no compilan), la versión va en `go.mod` y la información de
depuración se incluye siempre.
## `nm`: los símbolos de un `.o`
`nm fichero.o` lista los símbolos de un fichero objeto, es decir, los nombres
de sus funciones y variables globales. Las líneas que empiezan por `.` son
secciones internas.
| Letra | Significado |
|---|---|
| `T` | Definido aquí y **visible** para otros `.o` |
| `t` | Definido aquí pero **privado** (`static`) |
| `U` | **Usado** aquí pero definido en otro sitio: un hueco que rellenará el enlazador |
**El trabajo del enlazador es emparejar cada `U` con exactamente una `T`:**
- Ninguna `T` para un `U`: `undefined reference to 'x'`.
- Dos `T` con el mismo nombre: `multiple definition of 'x'`.
Para qué sirve `nm` en la práctica:
- Diagnosticar errores de enlazado: ver qué `.o` define un símbolo y cuál lo
necesita.
- Comprobar que la librería solo exporta (`T`) lo que está en la API pública y
que las funciones internas son `t`.
- Comprobar que una `.a` o `.dll` contiene lo que esperas antes de enlazarla
desde cgo.
Una declaración que no usas no crea ningún `U`: no genera hueco.
Pueden aparecer símbolos que no escribiste tú. Por ejemplo, `puts`: gcc
cambia `printf("%s\n", s)` por `puts(s)`. Y `__main`, que es el código de
arranque de MinGW.
## Errores: compilador frente a enlazador
- **Compilador**: el mensaje empieza por `fichero.c:línea:columna:`, porque
está leyendo código fuente.
- **Enlazador**: el mensaje menciona `ld.exe` o `collect2`, además de ficheros
`.o` y nombres de símbolo. A esa etapa ya no llega el código fuente.
## Warning frente a error
- `warning:` es un aviso: el `.o` se genera igualmente.
- `error:` significa que **no se genera nada**.
Cuando gcc falla, **no borra el `.o` ni el `.exe` anteriores**. Puedes acabar
ejecutando un binario viejo creyendo que es el nuevo. Para evitarlo:
- Busca `error:` en la salida.
- Comprueba `echo $?` justo después del comando: `0` es éxito y cualquier otro
valor es fallo.
- `make` se detiene en cuanto un comando falla.
## Varios
- En bash, un programa de la carpeta actual se ejecuta con `./hello.exe`. Bash
no busca en la carpeta actual por seguridad.
+60
View File
@@ -0,0 +1,60 @@
# Punteros y cadenas
## En C no hay tipo `string`
En Go, `string` es un tipo propio: por dentro guarda un puntero y una
longitud, y lo manejas como un valor.
En C, una cadena es solo una secuencia de bytes en memoria terminada en `'\0'`.
La longitud **no se guarda en ningún sitio**: para saberla hay que recorrer la
cadena hasta encontrar el `'\0'`.
```
"1.0.0" → '1' '.' '0' '.' '0' '\0' (6 bytes, no 5)
```
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,
el programa se cierra.
## `const char *`
Una función que "devuelve una cadena" en realidad devuelve **la dirección de
su primer byte**:
```c
const char *version = escpos_version();
```
- `char`: en esa dirección hay caracteres.
- `*`: `version` es un puntero; guarda una dirección, no un carácter.
- `const`: a través de este puntero no se pueden modificar los caracteres.
`char` es un **entero de 1 byte**. Si guardas un puntero en un `char`, pierdes
la dirección. Por eso el error dice `makes pointer from integer`.
## Los dos significados de `*`
| Dónde aparece | Qué significa |
|---|---|
| En una **declaración**: `const char *p` | "`p` es un puntero" |
| En una **expresión**: `*p` | "el valor que hay en la dirección de `p`" (desreferenciar) |
Si `p` apunta a `"1.0.0"`, `*p` es solo el `'1'`.
**En Go**: es lo mismo que `var p *T` frente a `*p`.
## `printf` y el formato
El primer argumento de `printf` es el **formato**. Nunca pases ahí una cadena
variable:
```c
printf(s); /* mal */
printf("%s\n", s); /* bien */
```
Si `s` contuviera un `%`, `printf` iría a leer argumentos que no existen, lo
que es comportamiento indefinido y un agujero de seguridad clásico
(*format string*). `%s` significa "imprime los bytes desde esta dirección hasta
el `'\0'`".
+200
View File
@@ -0,0 +1,200 @@
# La memoria de un proceso y el sistema operativo
## Memoria virtual
Cada proceso tiene su propio **espacio de direcciones virtual**. Las
direcciones que guarda un puntero no son direcciones de la RAM física: son
virtuales.
- La **MMU**, un componente del procesador, traduce cada dirección virtual a
una física en cada acceso, usando las **tablas de páginas** del proceso.
- Esas tablas las crea y mantiene el **núcleo** (*kernel*), y un proceso no
puede modificarlas.
- La memoria se gestiona en **páginas** de 4 KB. Cada página tiene permisos:
lectura, escritura, ejecución, y si es accesible desde modo usuario o solo
desde el núcleo.
Consecuencia: la misma dirección, por ejemplo `0x7ff6a000`, en dos procesos
distintos apunta a sitios físicos diferentes. **Un proceso no puede ni
siquiera nombrar la memoria de otro.**
## Qué pasa al acceder a una dirección no válida
1. La MMU no encuentra la traducción, o los permisos no lo permiten (por
ejemplo, escribir en una página de solo lectura).
2. El procesador genera una excepción de fallo de página y cede el control al
núcleo.
3. El núcleo decide: o era legítimo (una página que tenía en disco, por
ejemplo), o es un error y mata el proceso.
- Windows: `Access violation` (código `0xC0000005`).
- Linux: señal `SIGSEGV` (`Segmentation fault`).
Así fallaría escribir en `"1.0.0"`: está en una página de solo lectura.
## Modo usuario y modo núcleo
- Tu programa se ejecuta en **modo usuario** (*ring 3*). Hay instrucciones
que no puede ejecutar, y no puede acceder a las páginas del núcleo ni al
hardware directamente.
- El núcleo se ejecuta en **modo núcleo** (*ring 0*), con acceso a todo.
- Para cualquier cosa fuera de su memoria (ficheros, red, USB, otros
procesos), el programa hace una **llamada al sistema** (*syscall*): pide al
núcleo que lo haga por él, y el núcleo comprueba permisos.
Por eso los transportes del proyecto (Winsock, spooler, WinUSB, sockets
POSIX) son todos APIs del sistema: ningún programa en modo usuario habla
directamente con la tarjeta de red ni con el USB.
## Leer la memoria de otro proceso
Solo a través del núcleo y con permisos:
- **Windows**: `OpenProcess` con `PROCESS_VM_READ` y después
`ReadProcessMemory`. Se permite sobre procesos del mismo usuario; para
procesos del sistema hacen falta permisos de administrador
(`SeDebugPrivilege`).
- **Linux**: `ptrace`, `process_vm_readv` o `/proc/<pid>/mem`. Hace falta el
mismo usuario, y además lo limita `ptrace_scope`; con `root` o
`CAP_SYS_PTRACE`, de cualquiera.
Así funcionan los depuradores como gdb.
## Leer la memoria física o la del núcleo
Solo desde código que se ejecute en modo núcleo, es decir, un **driver**. En
Windows, los drivers tienen que estar firmados. En Linux, `/dev/mem` está muy
restringido. Los fallos de CPU como Meltdown (2018) fueron graves justo porque
permitían saltarse esa barrera.
## El peligro real en C: tu propia memoria
C no impide que un puntero lea **cualquier parte de tu propio proceso**.
Salirse de un array no provoca un fallo si la página de al lado está mapeada:
lee o machaca en silencio otros datos tuyos. Es comportamiento indefinido.
- Heartbleed (OpenSSL, 2014) era exactamente esto: una lectura fuera de los
límites que devolvía al atacante memoria del propio servidor, con claves y
contraseñas incluidas.
## Cómo actúa el software malicioso
Casi nunca rompe el aislamiento de memoria. Las vías habituales son:
1. **Usar permisos legítimos.** Un programa que ejecutas se ejecuta con
**tus** permisos: puede leer, cifrar o enviar tus ficheros con las APIs
normales. El ransomware no necesita ningún truco de memoria. El sistema
operativo aísla unos procesos de otros y al núcleo de los usuarios, pero no
protege tus ficheros de un programa que tú mismo has lanzado.
2. **Explotar fallos de memoria en programas legítimos.** Un servidor escrito
en C recibe datos preparados por el atacante que desbordan un buffer, y con
eso toma el control de ese proceso. Aquí es donde la falta de comprobaciones
de C importa. Según Microsoft y Google, en torno al 70 % de sus
vulnerabilidades graves son fallos de seguridad de memoria.
3. **Escalar privilegios.** Aprovechar un fallo en el núcleo o en un driver
para pasar de usuario a administrador o a modo núcleo.
Defensas que usan el procesador y el sistema operativo:
- **DEP/NX**: las páginas de datos no son ejecutables.
- **ASLR**: el sistema coloca el código y los datos en direcciones aleatorias
en cada ejecución.
- **Canarios de pila** (`-fstack-protector`): el compilador detecta si alguien
ha sobrescrito la pila.
Estas defensas dificultan el ataque, pero no corrigen el fallo de fondo.
**Para la librería**: tratará datos que no controla, como las respuestas de la
impresora (`DLE EOT`, `GS I`) o el texto que llegue desde Go. Toda longitud
que venga de fuera se comprueba antes de copiar nada.
### Drivers del núcleo frente a la librería del proyecto
"Driver" es una palabra con varios significados.
- **Driver del núcleo**: un módulo que se carga *dentro* del núcleo, en modo
núcleo. Habla con el hardware directamente (registros, interrupciones, DMA)
y ofrece el dispositivo a los programas de usuario de forma controlada.
Ejemplos: el de la tarjeta de red, `usbprint.sys`, `winusb.sys`, `usblp` en
Linux.
- **La librería del proyecto**: código en **modo usuario** que conoce el
*protocolo* de la impresora (ESC/POS) y genera y envía los bytes a través de
lo que ofrecen los drivers del núcleo.
La pila completa al imprimir:
```
programa en Go
└─ librería escpos (modo usuario) ← lo que construimos: sabe ESC/POS
├─ LAN: Winsock / sockets ──▶ syscall ──▶ pila TCP/IP ──▶ driver de red
└─ USB: spooler / WinUSB / libusb ──▶ syscall ──▶ usbprint/winusb ──▶ controlador USB
└──── modo núcleo ────┘
```
Los drivers del núcleo solo saben mover bytes y no entienden ESC/POS. La
librería entiende ESC/POS, pero no puede tocar el hardware.
No es un "driver de mentira": es otra capa. Los drivers de impresora de
Windows que instalan los fabricantes también son, en su mayor parte, código
en modo usuario que convierte el documento al lenguaje de la impresora;
debajo usan los mismos drivers del núcleo. Microsoft tiene incluso un marco
para escribir drivers en modo usuario (UMDF).
**En Go**: es como `net/http`. No implementa TCP, que es cosa del núcleo:
habla HTTP sobre los sockets que le da el sistema. La librería hace lo mismo
con ESC/POS.
### Caso real: CrowdStrike (19 de julio de 2024)
- **Qué es**: Falcon, el antivirus/EDR de CrowdStrike, tiene en Windows un
**driver en modo núcleo** que se carga al arrancar el sistema. Está en el
núcleo para poder vigilar todo el sistema y para que un malware no pueda
desactivarlo.
- **Qué pasó**: una actualización de contenido (un fichero de *datos*, el
"Channel File 291", no código nuevo) definía 21 campos de entrada. El
código del driver que lo procesaba solo proporcionaba 20. Al acceder al
campo 21 hizo una **lectura fuera de límites**, en modo núcleo.
- **Por qué fue tan grave**: en modo usuario, el núcleo mata el proceso y el
resto del sistema sigue funcionando. En modo núcleo no hay nadie por encima
que pueda recoger el error, así que Windows se detiene entero (pantalla
azul). Como el driver se carga al arrancar, el equipo caía **en cada
reinicio**. Microsoft estimó unos 8,5 millones de equipos afectados:
aerolíneas, hospitales, bancos.
- **Arreglo**: manual, equipo por equipo: arrancar en modo seguro y borrar el
fichero. Con BitLocker hacía falta además la clave de recuperación de cada
disco.
- **Fallos encadenados**:
1. El driver confió en que los datos de fuera tenían el tamaño esperado.
2. El validador de contenido tenía su propio fallo y dio el fichero por
bueno.
3. La actualización se envió a todos los clientes a la vez, sin
despliegue progresivo.
- **Consecuencia**: Microsoft anunció después planes para que los antivirus
puedan funcionar fuera del núcleo.
**En Go**, ese acceso habría sido un `panic: index out of range` en un
proceso. En C dentro del núcleo, millones de pantallas azules.
**Para el proyecto**: todo lo que haremos (Winsock, spooler, WinUSB, libusb)
se ejecuta en modo usuario. Un fallo en la librería tumba el programa que la
usa, no la máquina.
### cgo: Go y C comparten proceso
Con cgo, la librería en C se ejecuta **dentro del mismo proceso** que el
programa en Go, en el mismo espacio de direcciones. Las comprobaciones de Go
solo protegen el código Go: el código C puede escribir en cualquier parte del
proceso, incluidas la memoria de Go y las estructuras de su runtime.
- Un desbordamiento en la librería puede corromper datos de Go y provocar un
fallo mucho más tarde, en código Go correcto, lejos de la causa.
- Por LAN, cualquier equipo de la red puede hacerse pasar por la impresora en
`IP:9100` y enviar respuestas preparadas. Si la librería las copia sin
comprobar la longitud, ese equipo puede tomar el control del programa en Go.
**La garantía de seguridad de memoria de Go termina en la frontera de cgo.**
**En Go**: el modelo de procesos es el mismo, porque lo impone el sistema
operativo y no el lenguaje. La diferencia es que Go comprueba los límites de
cada slice y cada puntero `nil`, y provoca un `panic` **antes** del acceso
indebido. C no comprueba nada; para detectar estos errores están
AddressSanitizer y valgrind.
+95
View File
@@ -0,0 +1,95 @@
# Go escrito con mentalidad de C
Experimento: el mismo generador de tickets ESC/POS escrito en Go de dos
formas, midiendo el resultado (i9-9900K, Go 1.26, 30 líneas por ticket). Las
dos versiones producen exactamente los mismos bytes.
| | ns/ticket | bytes reservados/ticket | reservas/ticket |
|---|---|---|---|
| Hábitos de JS (`+=` en strings, `fmt.Sprintf`, `[]*Item`) | ~9.600 | 14.378 | 126 |
| Hábitos de C (un buffer reutilizado, `strconv.Append*`) | ~550 | 0 | 0 |
## Por qué
- **Los strings son inmutables.** `s += x` crea un string nuevo y copia todo
lo anterior en él. En un bucle, cada iteración copia más bytes que la
anterior: el coste crece con el cuadrado del tamaño.
- **`fmt.Sprintf`** reserva memoria para el resultado y usa reflexión.
`strconv.AppendInt` escribe directamente en tu buffer.
- **Reutilizar el buffer**: `buf = buf[:0]` pone la longitud a 0 pero
conserva la capacidad, es decir, la memoria ya reservada. Tras el primer
ticket no se vuelve a reservar nada. Es lo que hará el buffer de la fase 2.
## Padding: el orden de los campos cambia el tamaño
El procesador exige que un `int64` empiece en una dirección múltiplo de 8. El
compilador rellena con huecos (*padding*) para cumplirlo:
- `{bool, int64, bool, int64, bool}` ocupa **40 bytes**.
- `{int64, int64, bool, bool, bool}` ocupa **24 bytes**.
Con un millón de elementos son 38 MB frente a 22 MB. En C pasa exactamente lo
mismo.
## Un slice es `{puntero, longitud, capacidad}`
```go
header := make([]byte, 0, 16)
header = append(header, 0x1b, '@')
a := append(header, 'A')
b := append(header, 'B') // a también termina en 'B'
```
Hay capacidad libre, así que `append` no copia: escribe en el mismo array.
`a` y `b` comparten memoria. Quien piensa en punteros lo ve venir; quien
piensa en arrays de JS, no.
## Dinero en `float64`
`10 × 0.10` da `0.9999999999999999`. El `number` de JS es un `float64`. Para
dinero se usan enteros en céntimos.
## El precio de la versión rápida
`BuildTicket` devuelve una **vista del buffer interno**, que deja de ser
válida en la siguiente llamada. Hay que documentar quién es el dueño de esa
memoria. Es exactamente la regla de la API de la librería: documentar en cada
función quién es dueño de cada puntero.
## Tercera versión: una IA a partir de una spec escueta
Spec: "cabecera en negrita, una línea por producto, total, corte, precios con
dos decimales, tests". El código resultante es idiomático (`bytes.Buffer`,
`fmt.Fprintf`, dinero en céntimos) y **pasa todos los criterios de
aceptación**.
| | ns/ticket | bytes/ticket | reservas/ticket |
|---|---|---|---|
| IA con spec escueta | ~8.300 | 3.186 | 99 |
Casos que la spec no menciona:
| Caso | IA | Estilo C | Estilo JS |
|---|---|---|---|
| Descuento de −1,50 | `-1.-50` ❌ | `-1.+0` ❌ | `-1.50` ✅ (de casualidad) |
| `é`, `ñ`, `€` | UTF-8 ❌ | UTF-8 ❌ | UTF-8 ❌ |
| Nombre más largo que el papel (48/32 columnas) | no lo trata | no lo trata | no lo trata |
- **Negativos**: en Go (y en C) el resto de un número negativo es negativo:
`-150 % 100 == -50`. Las versiones con enteros lo formatean mal.
- **Codificación**: la impresora no entiende UTF-8. `é` se envía como `C3 A9`
y en papel sale como dos símbolos extraños. Hay que convertir a la página de
códigos de la impresora (fase 6).
Conclusión: el resultado nunca es mejor que la spec más la capacidad de quien
lo revisa. Las decisiones que no se piden las toma el modelo "por defecto", y
solo las detecta quien sabe que existen. Una spec de experto incluiría:
presupuesto de reservas de memoria, quién es dueño del buffer, importes
negativos, página de códigos y ancho de línea según el modelo de impresora.
## Cuándo no merece la pena
Si el ticket se genera una vez por venta, 9 µs frente a 0,5 µs da igual. La
versión rápida compensa en rutas calientes: miles de peticiones por segundo o
bucles sobre millones de elementos. Primero se mide (`go test -bench
-benchmem`, `pprof`) y después se optimiza.
+103
View File
@@ -0,0 +1,103 @@
# Frameworks y librerías del ecosistema C/C++
## Qt (C++, no C)
- Framework multiplataforma en **C++**: interfaces gráficas, red, hilos,
bases de datos, impresión, multimedia. Funciona en Windows, Linux, macOS,
Android, iOS y sistemas embebidos.
- Lo usan KDE, OBS Studio, Telegram Desktop, VirtualBox y muchos sistemas de
coche y de punto de venta.
- **Señales y slots**: un objeto emite un evento y otros lo reciben. Para
implementarlo, Qt tiene un precompilador propio (`moc`) que genera código
C++ adicional antes de compilar.
- **QML**: lenguaje declarativo, parecido a JS, para las interfaces modernas.
- **Licencia**: LGPLv3 o comercial (algunos módulos solo GPL o comercial). Con
LGPL hay que permitir que el usuario sustituya las librerías de Qt, y por
eso las aplicaciones Qt se distribuyen con sus DLL al lado.
- **Relación con este proyecto**: una aplicación Qt imprime con `QPrinter`, que
pasa por el driver del fabricante y el spooler. Para ESC/POS en crudo
(cajón, corte, estado) necesitaría una librería como esta.
## Por qué en C casi no hay "frameworks"
- La librería estándar de C es mínima: no trae red, ni contenedores, ni JSON,
ni HTTP. Los hilos (`threads.h`) llegaron en C11 y son opcionales.
- No hay gestor de paquetes oficial ni genéricos.
- Cultura resultante: **librerías pequeñas que se combinan**. Muchas son de
"una sola cabecera" o de un solo `.c`, para poder copiarlas en el proyecto
sin más.
**En Go**: la librería estándar ya trae `net/http`, `encoding/json`, `sync`,
etc., más `go get`.
## Librerías C relevantes
| Librería | Qué es | Relación |
|---|---|---|
| **GTK** + **GLib/GObject** | Interfaz gráfica de GNOME y GIMP. GObject implementa orientación a objetos a mano sobre C | Ejemplo de cómo simular clases e interfaces en C |
| **SDL** | Ventanas, gráficos, audio y entrada; multiplataforma, incluido Android | Juegos y multimedia |
| **LVGL** | Interfaces gráficas para microcontroladores con pantalla | Terminales de punto de venta embebidos |
| **libuv** | Bucle de eventos multiplataforma | Es el motor de Node.js: lo que hay debajo de JS |
| **libcurl** | Cliente HTTP y muchos protocolos más | |
| **SQLite** | Base de datos completa en un solo `.c` | Código C de referencia, muy bien probado |
| **libusb** | Acceso a USB desde modo usuario | Candidata para las fases 5 y 9 |
| **zlib**, **OpenSSL** | Compresión y criptografía | |
| **Unity**, **cmocka** | Frameworks de tests para C | Para probar la librería |
| **stb** | Colección de librerías de una sola cabecera (por ejemplo, `stb_image` para cargar PNG o JPEG) | Posible ayuda en la fase 7 (imágenes) |
## Interfaces en C
Una `interface` de Go se implementa en C como un `struct` de **punteros a
función**. Así funcionan GObject, los drivers de Linux y, en la fase 4, la
abstracción de transportes de la librería (LAN, USB) detrás de una misma API.
### Piezas
- **Puntero a función**: las funciones también están en memoria (en la
sección `.text`; la dirección que muestra `nm` junto a cada `T`). Un
puntero a función guarda esa dirección y permite llamarla.
Sintaxis: `double (*area)(const void *self);`.
- **`void *`**: un puntero "a cualquier cosa", sin tipo. Hace el papel del
receptor del método. C no tiene métodos: el objeto se pasa explícitamente.
Go hace lo mismo por dentro: `c.Area()` se compila como `Area(c)`.
- **vtable**: un `struct` con los punteros a función de un tipo concreto. Se
define una vez por tipo y la comparten todas sus instancias.
- **Valor de la "interfaz"**: dos punteros, `{vtable, datos}`.
### Cómo es una interfaz de Go por dentro
En el runtime de Go, una interfaz con métodos es exactamente eso:
```go
type iface struct {
tab *itab // tipo concreto + punteros a sus métodos
data unsafe.Pointer // el valor concreto
}
```
Por eso una interfaz ocupa 16 bytes en 64 bits. Y por eso existe la trampa
del `nil`:
```go
var p *MyError = nil
var err error = p
err != nil // true: tab apunta al tipo *MyError, aunque data sea nil
```
Una interfaz solo es `nil` si **los dos** campos son `nil`.
### Qué hace Go por ti y qué haces tú en C
| Go | C |
|---|---|
| Comprueba en compilación que el tipo implementa la interfaz | Nadie lo comprueba: rellenas la vtable a mano |
| Construye el `itab` automáticamente | Declaras la vtable como `static const` |
| El receptor tiene su tipo | `void *`: si pasas el objeto equivocado, comportamiento indefinido |
| Un método que falta no compila | Un puntero a `NULL` en la vtable provoca un fallo al llamarlo |
### Ejemplo real: los drivers de Linux
Cada driver rellena un `struct file_operations` con punteros a sus funciones
`open`, `read`, `write`... Cuando un programa hace `write()` sobre
`/dev/usb/lp0`, el núcleo llama a `usblp_write` a través de ese puntero. Es
el `io.Writer` del núcleo de Linux.
+19
View File
@@ -0,0 +1,19 @@
# Apuntes
Ordenados por tema, en el orden en que se ven en el proyecto.
1. [Compilación y enlazado](01-compilation-and-linking.md): etapas de gcc,
cómo compila Go, unidades de traducción, cabeceras e `#include`,
encapsulación y tipos opacos, *include guards*, 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 `*` 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
memoria de otro proceso, y por qué el peligro real está en la tuya.
4. [Go con mentalidad de C](04-go-with-c-mindset.md): benchmark de dos
estilos, reservas de memoria, padding, slices que comparten memoria,
dinero en float y el precio de la versión rápida.
5. [Frameworks y librerías](05-libraries-and-frameworks.md): Qt, por qué en C
casi no hay frameworks, librerías C relevantes para el proyecto e
interfaces con punteros a función.