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.
|
||||
Reference in New Issue
Block a user