393 lines
19 KiB
Markdown
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.
|