284 lines
13 KiB
Markdown
284 lines
13 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.
|
|
|
|
**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.
|