# 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 ` 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.