Files
printer-driver/docs/01-compilation-and-linking.md
2026-09-30 11:49:39 +02:00

393 lines
19 KiB
Markdown

# 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.
Si juntas dos flags de parada, gana la que para antes: `gcc -E -S` se comporta
como `-E`. Además, `-o` no convierte nada: `gcc -E x.c -o x.o` guarda texto C
preprocesado en un fichero que se llama `.o`, aunque no sea un fichero objeto.
### Empezar por cualquier etapa
gcc decide por la **extensión** del fichero de entrada en qué etapa empieza:
| Entrada | Empieza en |
|---|---|
| `.c` | preprocesador |
| `.i` | compilador (ya está preprocesado; sobra `-I`) |
| `.s` | ensamblador |
| `.o` | enlazador |
Por ejemplo, `gcc -c build/escpos.i -o build/escpos.o` compila un fichero ya
preprocesado.
Lo normal es partir del `.c`: gcc hace la cadena completa y crea el `.i` y el
`.s` como ficheros temporales que borra al acabar. Parar en cada etapa solo
sirve para aprender o para depurar. `-save-temps` hace la cadena completa y
**conserva** los intermedios en la carpeta actual.
**En Go**: `go build` también genera intermedios, pero en una carpeta temporal
que no ves (`go build -work` imprime cuál es y no la borra).
### Qué hay dentro de un `.i`
Es el `.c` después del preprocesador: C puro, sin `#include`, `#define` ni
`#ifndef`. Es exactamente lo que lee el compilador.
- Cada `#include` ha desaparecido y en su lugar está **el texto del `.h`
pegado**. Esto va en cadena: `stdio.h` incluye otras cabeceras, que a su vez
incluyen otras. Un `hello.c` de 10 líneas se convierte en un `.i` de unas 800.
- Los *include guards* (`#ifndef`/`#define`/`#endif`) ya se han ejecutado y no
aparecen. Solo queda lo que había dentro.
- Las líneas como `# 1 "include/escpos.h" 1` no son código. Son **marcas de
línea**: le indican al compilador de qué fichero y de qué línea viene cada
trozo, para que un error diga `escpos.h:4` en vez de "línea 11 del `.i`".
- **En el `.i` no hay código de otros `.c`.** De `escpos.h` solo llega la
declaración `const char *escpos_version(void);`, sin cuerpo. Con `printf`
pasa igual: el `.i` trae `extern int printf(...);` y su código está en la
librería de C del sistema (`libc.so` en Linux), ya compilado.
**En Go**: no hay preprocesador. `import` no pega texto, y además de darte las
firmas mete el código del paquete en el binario. En C son dos cosas separadas:
las firmas llegan con `#include` al preprocesar, y el código llega al enlazar.
**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*.
**Para compilar una llamada basta con la declaración.** Al compilar
`hello.c`, gcc no tiene el código de `escpos_version`, y no le hace falta.
Con saber que existe, qué recibe y qué devuelve, comprueba que la llamada es
correcta y genera la instrucción de llamada **con la dirección en blanco**.
Ese hueco lo rellena después el enlazador. Por eso
`gcc -c examples/hello.c` funciona sin `escpos.c`, y `nm build/hello.o`
muestra `U escpos_version`. Es como programar en Go contra una interfaz: el
compilador comprueba las firmas sin conocer la implementació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`.
Comprobado: con el cuerpo en `escpos.h`, `nm` muestra `T escpos_version`
en `hello.o` **y** en `escpos.o`, y el enlazado falla.
- **Si cambias un `.h`, hay que recompilar todos los `.c` que lo incluyen.**
Si no lo haces, enlazas `.o` compilados con la versión vieja de la cabecera,
y ni el compilador ni el enlazador avisan. Nos pasó: después de mover el
cuerpo a `escpos.h` solo se recompiló `escpos.o`. El `hello.o` viejo seguía
teniendo `U escpos_version`, así que todo enlazaba y parecía que no pasaba
nada. Hay que comparar las fechas (`ls -l`) o, mejor, dejar que un Makefile
lleve la cuenta. **En Go** esto no puede pasar: `go build` sabe qué depende
de qué y recompila lo necesario.
- 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. |
Los flags que llevan un valor admiten el valor pegado o separado: `-Iinclude`
y `-I include` son lo mismo, igual que `-obuild/x.o` y `-o build/x.o`. Por
costumbre, `-I`, `-L`, `-l` y `-D` se escriben pegados (`-lws2_32`,
`-DDEBUG`), y `-o` separado. Los flags largos como `-std=c17` llevan siempre
`=`. **En Go** pasa algo parecido con el paquete `flag`: vale `-o x` y `-o=x`.
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.
Ese `U puts` no da `undefined reference` aunque no le pases nada al enlazador
para resolverlo, porque gcc añade siempre la librería de C (`libc`) al enlazar.
Las demás librerías hay que pedirlas explícitamente con `-l`.
## 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` (Windows), `/usr/bin/ld` (Linux)
o `collect2`, además de ficheros `.o` y nombres de símbolo. A esa etapa ya no
llega el código fuente. `collect2` es el programa de gcc que lanza `ld`.
Ejemplo real, enlazando `hello.o` sin `escpos.o`:
```
/usr/bin/ld: build/hello.o: in function `main':
.../examples/hello.c:6:(.text+0x9): undefined reference to `escpos_version'
collect2: error: ld returned 1 exit status
```
Sale `hello.c:6` aunque el enlazador no lee código fuente. Ese dato viene de la
información de depuración que `-g` guardó en el `.o`. Sin `-g` solo verías
`(.text+0x9)`, que es la posición dentro del código máquina.
## 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.
## Salto de línea al final del fichero
C17 exige que todo fichero fuente no vacío termine en `\n`. Si no, el
comportamiento es indefinido. El riesgo está en `#include`, que pega el
texto: la última línea del `.h` (`#endif`) podría quedar unida a la siguiente
línea del fichero que lo incluye. Los preprocesadores actuales lo arreglan por
dentro, pero sigue sin ser C correcto.
- Clang avisa con `-Wpedantic`: `warning: no newline at end of file
[-Wnewline-eof]`. gcc ya no avisa.
- Para verlo: `tail -c 5 fichero.c | xxd`. El último byte tiene que ser `0a`.
- Solución: configurar el editor (`"files.insertFinalNewline": true` en
VS Code).
Compilar con gcc **y** con clang da dos juegos de avisos distintos, sin coste:
`make clean && make CC=clang`.
**En Go**: `gofmt` añade el salto final automáticamente.
## Varios
- En bash, un programa de la carpeta actual se ejecuta con `./hello.exe`. Bash
no busca en la carpeta actual por seguridad.