This commit is contained in:
2026-09-30 11:49:39 +02:00
parent 3b458fcd7f
commit 6e33293ea6
10 changed files with 382 additions and 11 deletions
+111 -2
View File
@@ -20,6 +20,54 @@
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.
@@ -89,6 +137,15 @@ no sabe nada de `escpos.c`. Cada `.c`, junto con todo lo que incluye, forma una
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.
@@ -150,6 +207,16 @@ hacen dos programas distintos:
- 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".
@@ -218,6 +285,12 @@ En C17, `int f();` significa "parámetros **sin especificar**", así que
| `-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).
@@ -257,12 +330,29 @@ 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` o `collect2`, además de ficheros
`.o` y nombres de símbolo. A esa etapa ya no llega el 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
@@ -277,6 +367,25 @@ ejecutando un binario viejo creyendo que es el nuevo. Para evitarlo:
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