initial commit
This commit is contained in:
@@ -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.
|
||||
@@ -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'`".
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user