1. Un archivo objeto por dentro
Para mirar el enlace de cerca hace falta un proyecto chico, con tres archivos que se necesitan mutuamente. Uno define datos, otro una función que los usa, y el tercero llama a la función y a printf:
/* datos.c */
int limite = 100; /* .data: global con valor */
int visitas; /* .bss: global sin valor */
static int secreto = 7; /* .data, pero local al archivo */
const char saludo[] = "hola"; /* .rodata */
int leer_secreto(void)
{
return secreto;
}
/* suma.c */
extern int limite;
extern int visitas;
int sumar_hasta(int n)
{
visitas++;
if (n > limite)
n = limite;
int total = 0;
for (int i = 1; i <= n; i++)
total += i;
return total;
}
/* principal.c */
#include <stdio.h>
int sumar_hasta(int n);
extern const char saludo[];
int main(void)
{
printf("%s: %d\n", saludo, sumar_hasta(500));
return 0;
}
Cada archivo se compila por separado hasta su objeto, y los objetos se enlazan. Primero, qué es un archivo objeto:
$ gcc -std=c17 -Wall -Wextra -c datos.c suma.c principal.c
$ file datos.o suma.o principal.o
datos.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
suma.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
principal.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
$ readelf -SW suma.o
There are 13 section headers, starting at offset 0x2b8:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000000000000 000040 000055 00 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 0001d0 000060 18 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 000095 000000 00 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 000095 000000 00 WA 0 0 1
[ 5] .comment PROGBITS 0000000000000000 000095 000027 01 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 0000bc 000000 00 0 0 1
[ 7] .note.gnu.property NOTE 0000000000000000 0000c0 000020 00 A 0 0 8
[ 8] .eh_frame PROGBITS 0000000000000000 0000e0 000038 00 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 000230 000018 18 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 000118 000090 18 11 3 8
[11] .strtab STRTAB 0000000000000000 0001a8 000023 00 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000248 00006c 00 0 0 1
La respuesta de file contiene la palabra clave: relocatable. Un objeto está en formato ELF, el mismo de los ejecutables, pero es una pieza todavía no ubicada: todas las secciones empiezan en la dirección 0, porque nadie decidió aún dónde van a quedar. Es una colección de secciones con un propósito cada una, y las que importan para el enlace son estas:
| Sección | Qué contiene | En suma.o |
|---|---|---|
.text | El código de máquina de las funciones del archivo. | 85 bytes (0x55): sumar_hasta. |
.data, .bss, .rodata | Las variables con valor, sin valor y las constantes del archivo (tema 6). | Vacías: suma.c no define ninguna variable. |
.symtab, .strtab | La tabla de símbolos y los nombres que esta usa. | 6 entradas de 24 bytes. |
.rela.text | Las reubicaciones del código: los huecos que el enlazador tiene que llenar. | 4 entradas de 24 bytes, que veremos en la sección 3. |
.eh_frame, .rela.eh_frame | Información para recorrer la pila al depurar o al manejar excepciones. | El enlazador la une igual que el resto. |
.comment, .note.* | Versión del compilador y propiedades como la pila no ejecutable. | Informativas. |
Todo objeto tiene así tres tipos de contenido: datos de sus secciones, símbolos que dicen qué nombre vive dónde, y reubicaciones que dicen qué lugares del contenido dependen de direcciones todavía desconocidas. El enlace es el proceso de resolver las dos últimas.
2. La tabla de símbolos
Un símbolo es un nombre asociado a una posición: una función o una variable. En un objeto hay dos clases de símbolos que le importan al enlazador: los que el archivo define y ofrece a los demás, y los que necesita y no define. readelf -s los muestra con todos sus datos:
$ readelf -sW datos.o
Symbol table '.symtab' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS datos.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 .data
4: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 secreto
5: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 limite
6: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 visitas
7: 0000000000000000 5 OBJECT GLOBAL DEFAULT 5 saludo
8: 0000000000000000 16 FUNC GLOBAL DEFAULT 1 leer_secreto
$ readelf -sW suma.o
Symbol table '.symtab' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS suma.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 85 FUNC GLOBAL DEFAULT 1 sumar_hasta
4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND visitas
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND limite
$ readelf -sW principal.o
Symbol table '.symtab' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS principal.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata
4: 0000000000000000 57 FUNC GLOBAL DEFAULT 1 main
5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sumar_hasta
6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND saludo
7: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
Cada columna tiene un significado, y entre todas cuentan la historia del módulo:
Ndxes el dato decisivo. Es el número de la sección donde está el símbolo. Un símbolo con un número, comolimiteen la sección 3,.data, está definido en este objeto. Un símbolo conUND(undefined) está indefinido: el archivo lo usa, pero lo define otro. Ensuma.o,sumar_hastaestá definido yvisitasylimiteno; enprincipal.o, solomainestá definido.Bindes la visibilidad hacia otros archivos.GLOBALse ofrece a los demás objetos;LOCALes de este archivo y el enlazador no lo une con nada.secreto, declaradastaticen el tema 6, esLOCAL: otro archivo que declareextern int secretono la encontraría. También hayWEAK, de la sección 5.Typedistingue funciones y datos (FUNC,OBJECT), más dos tipos auxiliares:FILEguarda el nombre del archivo fuente, para los depuradores, ySECTIONpermite que las reubicaciones se refieran a una sección entera. Los símbolos indefinidos sonNOTYPE: el objeto no sabe silimitees una variable o una función; solo sabe que lo necesita.ValueySizeson el desplazamiento dentro de su sección y el tamaño en bytes. Todos valen 0 salvosecreto, que está 4 bytes después delimiteen.data.
El lenguaje no tiene nada que ver con esto: una variable int limite y una función int limite(void) son, para el enlazador, un nombre con una dirección. C no «mangla» los nombres como C++ (que codifica los tipos de los parámetros en el símbolo): el nombre de la función en C es exactamente el símbolo, y por eso nada impide declarar una función con un tipo y definirla con otro en archivos distintos, y que el enlace salga bien y el programa falle. El enlazador no ve tipos.
nm presenta la misma información de manera compacta, con una letra por símbolo, la que usamos desde el tema 4:
$ nm datos.o
0000000000000000 T leer_secreto
0000000000000000 D limite
0000000000000000 R saludo
0000000000000004 d secreto
0000000000000000 B visitas
$ nm -S datos.o
0000000000000000 0000000000000010 T leer_secreto
0000000000000000 0000000000000004 D limite
0000000000000000 0000000000000005 R saludo
0000000000000004 0000000000000004 d secreto
0000000000000000 0000000000000004 B visitas
$ nm -u principal.o suma.o
principal.o:
U printf
U saludo
U sumar_hasta
suma.o:
U limite
U visitas
| Letra | Significa | Ejemplo |
|---|---|---|
T / t | Código (.text), global / local. | leer_secreto, sumar_hasta, main. |
D / d | Datos con valor (.data). | limite / secreto. |
B / b | Datos sin valor (.bss). | visitas. |
R / r | Datos de solo lectura (.rodata). | saludo. |
U | Indefinido: se usa y se define en otro lugar. | printf, limite. |
W / w | Débil, definido / indefinido (sección 5). | Más adelante. |
C | Común (sección 5). | Más adelante. |
Mayúscula es global, minúscula es local. Con -u se listan solo los indefinidos, que son exactamente lo que cada objeto le pide al resto del programa; con -S se agrega el tamaño; con --defined-only, lo opuesto de -u. Para buscar quién define un nombre entre muchos objetos, nm -A *.o | grep ' T nombre' imprime el archivo en cada línea.
3. Los huecos: las reubicaciones
Cuando el compilador genera suma.o, no sabe en qué dirección va a quedar visitas, ni limite, ni dónde va a estar sumar_hasta para quien la llame. Sin embargo, tiene que emitir las instrucciones que los usan. Lo que hace es dejar un hueco en el código, de ceros, y anotar en una tabla aparte qué hueco es de qué símbolo y cómo llenarlo. Se ve con objdump -dr, que desensambla y muestra las anotaciones debajo de cada instrucción afectada:
$ objdump -dr suma.o
0000000000000000 <sumar_hasta>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 89 7d ec mov %edi,-0x14(%rbp)
b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 11 <sumar_hasta+0x11>
d: R_X86_64_PC32 visitas-0x4
11: 83 c0 01 add $0x1,%eax
14: 89 05 00 00 00 00 mov %eax,0x0(%rip) # 1a <sumar_hasta+0x1a>
16: R_X86_64_PC32 visitas-0x4
1a: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 20 <sumar_hasta+0x20>
1c: R_X86_64_PC32 limite-0x4
20: 39 45 ec cmp %eax,-0x14(%rbp)
23: 7e 09 jle 2e <sumar_hasta+0x2e>
25: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 2b <sumar_hasta+0x2b>
27: R_X86_64_PC32 limite-0x4
2b: 89 45 ec mov %eax,-0x14(%rbp)
…
$ objdump -dr principal.o
0000000000000000 <main>:
…
8: bf f4 01 00 00 mov $0x1f4,%edi
d: e8 00 00 00 00 call 12 <main+0x12>
e: R_X86_64_PLT32 sumar_hasta-0x4
12: 89 c2 mov %eax,%edx
14: 48 8d 0d 00 00 00 00 lea 0x0(%rip),%rcx # 1b <main+0x1b>
17: R_X86_64_PC32 saludo-0x4
1b: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 22 <main+0x22>
1e: R_X86_64_PC32 .rodata-0x4
…
2d: e8 00 00 00 00 call 32 <main+0x32>
2e: R_X86_64_PLT32 printf-0x4
$ readelf -rW suma.o
Relocation section '.rela.text' at offset 0x1d0 contains 4 entries:
Offset Info Type Symbol's Value Symbol's Name + Addend
000000000000000d 0000000400000002 R_X86_64_PC32 0000000000000000 visitas - 4
0000000000000016 0000000400000002 R_X86_64_PC32 0000000000000000 visitas - 4
000000000000001c 0000000500000002 R_X86_64_PC32 0000000000000000 limite - 4
0000000000000027 0000000500000002 R_X86_64_PC32 0000000000000000 limite - 4
Cada instrucción afectada tiene un campo de cuatro bytes en cero, 00 00 00 00: el mov 0x0(%rip),%eax que lee visitas debería llevar la distancia hasta ella, y no se conoce. Debajo de cada una, una línea R_X86_64_… dice cuatro cosas, las mismas que las columnas de readelf -r:
- Offset: dónde está el hueco,
0xd, dentro de la sección.text. Es la dirección del primer byte a llenar. - Tipo: cómo calcular el valor.
R_X86_64_PC32es una distancia de 32 bits relativa a la instrucción que la usa, el direccionamiento0x0(%rip)de los accesos a variables.R_X86_64_PLT32es lo mismo, paracall, con la salvedad de que el destino puede pasar por una tabla de saltos (sección 7). Hay decenas de tipos más, para direcciones absolutas de 64 bits o accesos a la tabla GOT. - Símbolo: a qué nombre se refiere. Para los símbolos indefinidos es el nombre que el enlazador va a buscar. Para las constantes locales, como la cadena de formato de
printf, es la sección entera (.rodata). - Addend: un número que se suma al resultado, aquí −4. Este número no es arbitrario: sigue de cómo funciona el procesador.
Ese −4 es la clave de la cuenta. Una instrucción con direccionamiento relativo a %rip calcula la dirección efectiva sumando el desplazamiento al valor de %rip en el momento de ejecutarla, que ya apunta a la instrucción siguiente, 4 bytes después del final del campo del desplazamiento si el campo es lo último de la instrucción. Si P es la dirección del hueco y S la del símbolo, el valor que hay que escribir es S − (P + 4), que es S + A − P con A = −4. Esa es la fórmula de las reubicaciones PC32 y PLT32, y todo lo que hace el enlazador en este punto es calcularla para cada entrada y escribir el resultado en el hueco.
4. Lo que hace el enlazador
El enlazador (ld) recibe los objetos y las bibliotecas y hace, en esencia, tres cosas, en este orden:
- Resolver los símbolos. Recorre las tablas de símbolos de todos los objetos y construye una tabla única: para cada nombre global, quién lo define. Cada símbolo indefinido tiene que encontrar exactamente una definición; si no hay ninguna, es un
undefined reference; si hay varias, unmultiple definition(sección 5 para las excepciones). resolver - Asignar direcciones. Junta, sección por sección, el contenido de todos los objetos: primero las
.textuna detrás de otra, después las.rodata,.datay.bss, en el orden en que llegaron los archivos. Con eso, cada símbolo recibe su dirección definitiva. ubicar - Aplicar las reubicaciones. Para cada entrada de las tablas
.rela.*, calcula el valor con la fórmula de la sección anterior y lo escribe en el hueco. Después de este paso el código está completo. llenar
El resultado se puede ver tal como lo decidió el enlazador, pidiéndole un mapa de enlace (-Wl,-Map=archivo; -Wl,opción pasa la opción al enlazador a través de gcc):
$ gcc -o calculo principal.o suma.o datos.o && ./calculo
hola: 5050
$ grep -E '^\.text|^ \.text' calculo.map
.text 0x0000000000001060 0x187
.text 0x0000000000001060 0x26 …/Scrt1.o
.text 0x0000000000001086 0x0 …/crti.o
.text 0x0000000000001090 0xb9 …/crtbeginS.o
.text 0x0000000000001149 0x39 principal.o
.text 0x0000000000001182 0x55 suma.o
.text 0x00000000000011d7 0x10 datos.o
.text 0x00000000000011e7 0x0 …/crtendS.o
.text 0x00000000000011e7 0x0 …/crtn.o
$ nm -n calculo | grep -E ' (_start|main|sumar_hasta|leer_secreto|saludo|limite|secreto|visitas)$'
0000000000001060 T _start
0000000000001149 T main
0000000000001182 T sumar_hasta
00000000000011d7 T leer_secreto
000000000000200c R saludo
0000000000004010 D limite
0000000000004014 d secreto
000000000000401c B visitas
El mapa confirma el reparto. Delante de nuestro código está el de arranque de Scrt1.o, crti.o y crtbeginS.o, que gcc agrega solo (tema 4): son los 0x26 + 0xb9 bytes que hay desde _start, en 0x1060, hasta main, en 0x1149. Después, los tres objetos en el orden en que se escribieron en la línea de órdenes, con sus tamaños: principal.o ocupa 0x39 bytes (el 57 que decía Size de main), suma.o 0x55 y datos.o 0x10. Cambiar el orden de los archivos en la línea cambia las direcciones, no el resultado.
Falta comprobar la cuenta. Estos son los huecos de main y de sumar_hasta después del enlace, y las direcciones que dio nm:
0000000000001149 <main>:
1151: bf f4 01 00 00 mov $0x1f4,%edi
1156: e8 27 00 00 00 call 1182 <sumar_hasta>
115b: 89 c2 mov %eax,%edx
115d: 48 8d 0d a8 0e 00 00 lea 0xea8(%rip),%rcx # 200c <saludo>
1164: 48 8d 05 99 0e 00 00 lea 0xe99(%rip),%rax # 2004 <_IO_stdin_used+0x4>
…
1176: e8 d5 fe ff ff call 1050 <printf@plt>
0000000000001182 <sumar_hasta>:
118d: 8b 05 89 2e 00 00 mov 0x2e89(%rip),%eax # 401c <visitas>
119c: 8b 05 6e 2e 00 00 mov 0x2e6e(%rip),%eax # 4010 <limite>
| Reubicación | P (el hueco) | S (el símbolo) | S + A − P | Bytes escritos |
|---|---|---|---|---|
call sumar_hasta | 0x1157 | 0x1182 | 0x1182 − 4 − 0x1157 = 0x27 | 27 00 00 00 |
call printf | 0x1177 | 0x1050 (printf@plt) | 0x1050 − 4 − 0x1177 = −0x12b = 0xfffffed5 | d5 fe ff ff |
lea saludo | 0x1160 | 0x200c | 0x200c − 4 − 0x1160 = 0xea8 | a8 0e 00 00 |
mov visitas | 0x118f | 0x401c | 0x401c − 4 − 0x118f = 0x2e89 | 89 2e 00 00 |
Las cuatro cuentas dan exactamente los bytes que el enlazador escribió. Un detalle más: la llamada a printf da un valor negativo, en complemento a dos, porque la tabla de saltos está antes que main. Y un límite: el campo de la reubicación tiene 32 bits, de modo que el código y los datos a los que llega no pueden estar a más de ±2 GiB; en programas gigantescos, el modelo de código del compilador tiene que cambiar.
Se puede pedir además al enlazador que explique lo que hace con ciertos símbolos. -Wl,-y,nombre (--trace-symbol) informa cada objeto que lo referencia o lo define, y -Wl,--cref imprime una tabla de referencias cruzadas:
$ gcc -Wl,-y,sumar_hasta -Wl,-y,limite -o calculo principal.o suma.o datos.o
/usr/bin/x86_64-linux-gnu-ld.bfd: principal.o: reference to sumar_hasta
/usr/bin/x86_64-linux-gnu-ld.bfd: suma.o: definition of sumar_hasta
/usr/bin/x86_64-linux-gnu-ld.bfd: suma.o: reference to limite
/usr/bin/x86_64-linux-gnu-ld.bfd: datos.o: definition of limite
$ gcc -Wl,--cref -o calculo principal.o suma.o datos.o
Symbol File
leer_secreto datos.o
limite datos.o
suma.o
saludo datos.o
principal.o
sumar_hasta suma.o
principal.o
visitas datos.o
suma.o
En la tabla cruzada, el primer archivo de cada símbolo es el que lo define y los siguientes, los que lo usan. Es el equivalente, para todo el programa, del nm de cada objeto. Una última observación sobre el papel de gcc: no es el enlazador, sino un conductor que lo invoca con todos los archivos de arranque y las bibliotecas. Si se llama a ld directamente con solo nuestro objeto, protesta por lo que gcc habría puesto:
$ ld -o vacio principal.o
ld: warning: cannot find entry symbol _start; defaulting to 0000000000401000
ld: principal.o: in function `main':
principal.c:(.text+0xe): undefined reference to `sumar_hasta'
Sin Scrt1.o no hay _start, y sin -lc no habría printf: el enlazador solo une lo que le dan.
5. Símbolos fuertes, débiles y comunes
La regla general es que cada símbolo global tiene una sola definición. Pero el formato ELF distingue tres clases de definición, con reglas para cuando hay más de una:
| Clase | Cómo se crea | nm | Regla del enlazador |
|---|---|---|---|
| Fuerte | Una función, o una variable global inicializada. | T, D | Solo puede haber una. Dos fuertes con el mismo nombre: multiple definition. |
| Débil | __attribute__((weak)). | W (w si está indefinido) | Cede ante una fuerte; si solo hay débiles, GNU ld toma la primera que encuentra. Un débil indefinido que nadie define vale 0, sin error. |
| Común | Una variable global sin valor y sin extern, con -fcommon. | C | Varias comunes con el mismo nombre se unifican en una sola variable. |
Los símbolos débiles son la herramienta para ofrecer una definición «por omisión» que otro módulo puede reemplazar sin tocar el original. La biblioteca de C usa el mecanismo para permitir que un programa sustituya funciones de la biblioteca, y los núcleos de los sistemas, para manejadores de interrupciones por omisión. Este ejemplo reúne tres casos: una función con definición débil y otra fuerte, y una función débil que ni siquiera se define:
/* principal2.c */
#include <stdio.h>
void aviso(void); /* ¿cuál definición habrá? */
void opcional(void) __attribute__((weak)); /* débil sin definición: vale 0 */
int main(void)
{
aviso();
if (opcional != NULL)
opcional();
else
puts("opcional no está definida: su dirección es NULL");
return 0;
}
/* aviso_debil.c */
#include <stdio.h>
__attribute__((weak)) void aviso(void)
{
puts("aviso por omisión (definición débil)");
}
/* aviso_fuerte.c */
#include <stdio.h>
void aviso(void)
{
puts("aviso personalizado (definición fuerte)");
}
$ gcc -std=c17 -Wall -Wextra -c principal2.c aviso_debil.c aviso_fuerte.c
$ nm aviso_debil.o aviso_fuerte.o principal2.o
aviso_debil.o:
0000000000000000 W aviso
U puts
aviso_fuerte.o:
0000000000000000 T aviso
U puts
principal2.o:
U _GLOBAL_OFFSET_TABLE_
U aviso
0000000000000000 T main
w opcional
U puts
$ gcc -o w1 principal2.o aviso_debil.o && ./w1
aviso por omisión (definición débil)
opcional no está definida: su dirección es NULL
$ gcc -o w2 principal2.o aviso_debil.o aviso_fuerte.o && ./w2
aviso personalizado (definición fuerte)
opcional no está definida: su dirección es NULL
$ gcc -o w3 principal2.o aviso_fuerte.o aviso_debil.o && ./w3
aviso personalizado (definición fuerte)
opcional no está definida: su dirección es NULL
$ gcc -o w4 principal2.o
principal2.c:(.text+0x9): undefined reference to `aviso'
La W de aviso_debil.o y la w minúscula de opcional en principal2.o marcan los dos casos. Con solo la definición débil, se usa; con las dos, gana la fuerte, en cualquier orden de la línea de órdenes (w2 y w3); y sin ninguna, el símbolo aviso (fuerte indefinido) es un error. En cambio, opcional es débil e indefinida, y el enlace sale bien: su dirección es 0, el programa lo comprueba con if (opcional != NULL) y evita llamarla. Es el mecanismo con que un programa detecta, al cargarse, si una función opcional de una biblioteca está presente.
Los símbolos comunes son un caso histórico que causa problemas hasta hoy. En C, int cuenta; fuera de una función, sin extern y sin valor, es una «definición tentativa»: el estándar permite repetirla en un mismo archivo. Durante décadas, gcc extendió esa libertad a todo el programa, con -fcommon: el objeto marcaba cuenta como común, y el enlazador unificaba todas las que encontraba. Desde gcc 10, el valor por omisión es -fno-common: la definición va a .bss como una definición fuerte, y repetirla en dos archivos es un error:
$ gcc -std=c17 -Wall -Wextra -c cuenta_a.c cuenta_b.c && nm cuenta_a.o
0000000000000000 B cuenta
0000000000000000 T incrementar
$ gcc -o cuenta cuenta_a.o cuenta_b.o
/usr/bin/x86_64-linux-gnu-ld.bfd: cuenta_b.o:(.bss+0x0): multiple definition of `cuenta'; cuenta_a.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
$ gcc -std=c17 -fcommon -c cuenta_a.c -o ca.o; gcc -std=c17 -fcommon -c cuenta_b.c -o cb.o; nm ca.o cb.o
ca.o:
0000000000000004 C cuenta
0000000000000000 T incrementar
cb.o:
0000000000000004 C cuenta
U incrementar
0000000000000000 T main
$ gcc -o cuenta_c ca.o cb.o && ./cuenta_c; echo $?
1
Con -fcommon, los dos archivos comparten la misma variable y el programa «funciona»: main devuelve 1, el valor que incrementó el otro archivo. El enlace sale bien con una C (el 4 en la primera columna es el tamaño, no la dirección). Pero un error de ese tipo es una trampa: los dos archivos podrían declarar cuenta con tipos distintos (int y long) y el enlazador no se queja, sino que reserva el tamaño mayor y deja que cada uno acceda a su manera. Por eso el valor por omisión cambió, y por eso la regla del tema 13 es la correcta: la variable compartida se define en un solo .c, y se declara con extern en el encabezado. Los proyectos antiguos que usaban el comportamiento común fallan hoy con multiple definition, y se arreglan corrigiendo el diseño y no volviendo a -fcommon.
6. Los errores de enlace
Con lo anterior, los mensajes del enlazador dejan de ser crípticos. Tienen todos la misma forma: archivo.o: in function `f': a veces, seguido de la posición archivo.c:(.sección+0xN) donde está el hueco que falló, y la causa. Nunca hay un número de línea de C, porque el enlazador no ve C: ve secciones y reubicaciones; el .text+0x9 es el offset del hueco dentro de la sección, el Offset de readelf -r. Los casos reales:
$ gcc -o c5 principal.o datos.o
/usr/bin/x86_64-linux-gnu-ld.bfd: principal.o: in function `main':
principal.c:(.text+0xe): undefined reference to `sumar_hasta'
collect2: error: ld returned 1 exit status
$ gcc -o c6 principal.o suma.o
/usr/bin/x86_64-linux-gnu-ld.bfd: suma.o: warning: relocation against `visitas' in read-only section `.text'
/usr/bin/x86_64-linux-gnu-ld.bfd: principal.o: in function `main':
principal.c:(.text+0x17): undefined reference to `saludo'
/usr/bin/x86_64-linux-gnu-ld.bfd: suma.o: in function `sumar_hasta':
suma.c:(.text+0xd): undefined reference to `visitas'
/usr/bin/x86_64-linux-gnu-ld.bfd: suma.c:(.text+0x16): undefined reference to `visitas'
/usr/bin/x86_64-linux-gnu-ld.bfd: suma.c:(.text+0x1c): undefined reference to `limite'
/usr/bin/x86_64-linux-gnu-ld.bfd: suma.c:(.text+0x27): undefined reference to `limite'
/usr/bin/x86_64-linux-gnu-ld.bfd: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status
El primero, sin suma.o, es un error por falta de definición, en principal.o: el mismo del tema 4. El segundo, sin datos.o, es instructivo por otra razón: cada reubicación fallida genera una línea. Aparecen saludo (1 vez), visitas (2) y limite (2), y los offsets .text+0xd, +0x16, +0x1c y +0x27 son exactamente los de las reubicaciones de suma.o de la sección 3. Se ve también que los dos avisos al principio y al final (relocation against … in read-only section, creating DT_TEXTREL in a PIE) son consecuencias del error principal, no problemas adicionales: el enlazador, al no encontrar el símbolo, no pudo hacer el cálculo normal. Se lee de arriba abajo, pero se corrige la causa y no los efectos: falta un solo archivo.
$ gcc -std=c17 -Wall -Wextra -c estatica_a.c estatica_b.c && nm estatica_a.o
0000000000000000 t ayudante
000000000000000f T usar_ayudante
$ gcc -o est estatica_a.o estatica_b.o
/usr/bin/x86_64-linux-gnu-ld.bfd: estatica_b.o: in function `main':
estatica_b.c:(.text+0x9): undefined reference to `ayudante'
collect2: error: ld returned 1 exit status
$ gcc -std=c17 -c raiz.c && gcc -o raiz raiz.o
/usr/bin/x86_64-linux-gnu-ld.bfd: raiz.o: in function `main':
raiz.c:(.text+0x46): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
$ gcc -o raiz raiz.o -lm && ./raiz 2
sqrt(2) = 1.41421
$ gcc -o dosm m1.c m2.c
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccTXBltp.o: in function `main':
m2.c:(.text+0x0): multiple definition of `main'; /tmp/ccs9CdR2.o:m1.c:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status
ayudanteexiste, pero esstatic. El compilador aceptaestatica_b.cporque la declaración basta, yestatica_a.ocontiene la función, pero con unatminúscula: es un símboloLOCALque el enlazador no ofrece a nadie. El error dice «undefined reference», aunque el código existe, y se resuelve quitando elstatico poniendo la función en el módulo correcto. Es el caso inverso de olvidar un archivo, y nunca se diagnostica mirando el código C: hay que mirarnm.sqrtno está enlibc. La biblioteca matemática es una biblioteca aparte, y se pide con-lm(el tema 32 trata el significado de-ly el orden en la línea). El código deraiz.ccompila sin protestar, porquemath.hdeclarasqrt; el error es del enlace, con el offset de la llamada. Con-O2pasa lo mismo, porque la llamada asqrtcon un valor desconocido sigue ahí, para conservar el tratamiento de errores. Es elundefined referencemás común que se ve en la práctica.- Dos
main. Un caso demultiple definitionentre dos fuertes. Observa que aquí, como los fuentes se compilaron en la misma orden, los «archivos» son temporales del compilador (/tmp/cc….o); congcc -cpor separado tendrían nombre propio. El mismo error aparece cuando una función o una variable se define en un encabezado incluido desde dos archivos.c, el error del tema 13.
Un procedimiento sirve para casi todos los casos de undefined reference to X:
- Copiar el nombre exacto del mensaje, y comprobar que está bien escrito en el lugar que lo usa. nombre
- Buscar quién lo define:
nm -A *.o | grep ' T X', ogrep -n 'X(' *.csi se duda del módulo. Si nadie lo define, falta escribirlo; si lo define un archivo que no está en la línea, falta agregarlo. buscar - Si lo define alguien, mirar la letra: una
to unadminúscula significa que esstatic. visibilidad - Si es una función de una biblioteca del sistema, buscar su página de manual:
man 3 sqrtdice en la sinopsis «Link with -lm». biblioteca
7. Los símbolos del ejecutable dinámico
El ejecutable que produjo el enlace no está completo. La llamada a printf apunta a printf@plt, y el código de printf está en libc.so.6, un archivo aparte que el cargador buscará y mapeará al ejecutar el programa (tema 27). Entonces el ejecutable guarda, además de su tabla de símbolos habitual, la información para que el cargador termine el trabajo del enlace en tiempo de ejecución:
$ readelf --dyn-syms -W calculo
Symbol table '.dynsym' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.34 (2)
2: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (3)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
6: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (3)
$ readelf -rW calculo | sed -n '/rela.plt/,$p'
Relocation section '.rela.plt' at offset 0x5f0 contains 1 entry:
Offset Info Type Symbol's Value Symbol's Name + Addend
0000000000003fd0 0000000300000007 R_X86_64_JUMP_SLOT 0000000000000000 printf@GLIBC_2.2.5 + 0
$ readelf -d calculo | grep -E 'NEEDED|FLAGS'
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001e (FLAGS) BIND_NOW
0x000000006ffffffb (FLAGS_1) Flags: NOW PIE
$ nm -D calculo
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
w __cxa_finalize@GLIBC_2.2.5
w __gmon_start__
U __libc_start_main@GLIBC_2.34
U printf@GLIBC_2.2.5
El ejecutable tiene dos tablas de símbolos para dos propósitos:
.symtabes la tabla de símbolos completa, la que usannmy los depuradores: todo el contenido de los objetos que se unieron. No la necesita el sistema para ejecutar el programa..dynsymes la tabla dinámica, mucho más corta: solo lo que cruza la frontera entre el ejecutable y las bibliotecas compartidas. Aquí son siete símbolos, todos indefinidos:printfy__libc_start_main, con su versión deglibcpegada al nombre, y algunos débiles opcionales de las herramientas de perfilado y de transacciones (__gmon_start__,_ITM_*), que el cargador deja en 0 si nadie los define, exactamente el mecanismo deopcionalde la sección 5. Se consulta connm -Doreadelf --dyn-syms.
El enlazador dejó además reubicaciones dinámicas, que completa el cargador (ld.so) al cargar el programa: R_X86_64_JUMP_SLOT para printf, que llena una entrada de la tabla de saltos (.got, en 0x3fd0) con la dirección real de la función en libc, y R_X86_64_GLOB_DAT para los demás. El call de main va a printf@plt, un trampolín en .plt.sec que salta a través de esa entrada. La bandera BIND_NOW indica que Ubuntu pide resolverlas todas al cargar, no al primer uso, y dejar la tabla de solo lectura después (el RELRO del tema 27). La línea NEEDED es la lista de bibliotecas que el cargador tiene que buscar, que se estudia en los temas 33 y 34. Lo importante para este tema es la idea: el enlace tiene dos tiempos. El enlazador resuelve lo que está en los objetos y las bibliotecas estáticas, y deja pendiente, con símbolos y reubicaciones, lo que viene de bibliotecas dinámicas; el cargador lo termina cuando el programa arranca.
8. Quitar lo que sobra
Dos herramientas reducen el ejecutable. La primera elimina la información que el sistema no necesita para ejecutar:
$ cp calculo calculo_strip && strip calculo_strip
$ ls -l calculo calculo_strip | awk '{print $5, $9}'
16224 calculo
14472 calculo_strip
$ nm calculo_strip
nm: calculo_strip: no symbols
$ readelf -SW calculo_strip | grep -E 'symtab|strtab|dynsym'
[ 4] .dynsym DYNSYM 00000000000003b8 0003b8 0000a8 18 A 5 1 8
$ ./calculo_strip
hola: 5050
$ gdb -q -batch -ex 'info functions sumar' ./calculo_strip
All functions matching regular expression "sumar":
strip borra .symtab y .strtab, que suman unos 1,7 KB de los 16 KB de este ejemplo, y mucho más en programas reales, donde también quita la información de depuración de -g. El programa funciona igual, y se conserva .dynsym, que es la que el cargador necesita. El precio es que ya no hay nombres: nm no encuentra nada, y gdb no puede encontrar la función sumar_hasta por nombre, que sí aparece en el ejecutable sin recortar. Por eso en los programas que se entregan se separa la información de depuración en un archivo aparte, y se despoja el ejecutable. También hay una consecuencia de seguridad: los nombres de las funciones facilitan el análisis inverso.
La segunda herramienta quita código que nadie usa. El enlazador trabaja con secciones enteras: si un archivo objeto tiene cinco funciones en la misma sección .text y se usa una, entra el archivo entero. Con -ffunction-sections el compilador pone cada función en su propia sección, y con -Wl,--gc-sections el enlazador descarta las secciones a las que nadie llega desde el punto de entrada:
$ gcc -std=c17 -O1 -o muchas1 muchas.c
$ gcc -std=c17 -O1 -ffunction-sections -Wl,--gc-sections -o muchas2 muchas.c
$ size muchas1 muchas2 | awk '{print $1, $6}'
text filename
1894 muchas1
1574 muchas2
$ nm muchas1 | grep -c informe; nm muchas2 | grep -c informe
5
1
Con la opción, el ejecutable no contiene las cuatro funciones que nadie llama. Es una técnica de los programas para sistemas pequeños y de las bibliotecas estáticas (tema 32), donde pocas funciones de una biblioteca grande se usan. Hay que usar las dos opciones juntas: la del compilador crea las secciones, y la del enlazador descarta las inalcanzables.
9. Las herramientas, en una tabla
| Herramienta | Para qué | Ejemplo |
|---|---|---|
file | Qué tipo de archivo ELF es: objeto (relocatable), ejecutable, biblioteca compartida. | file suma.o |
nm | Los símbolos, una letra por símbolo. -u indefinidos, -S tamaño, -n por dirección, -D dinámicos, -A con el archivo. | nm -u principal.o |
readelf -s, --dyn-syms | La tabla de símbolos con todos sus campos (Bind, Ndx, Type). | readelf -sW datos.o |
readelf -S, -l, -d | Secciones, segmentos y dependencias dinámicas (NEEDED). | readelf -d calculo |
readelf -r, objdump -dr | Las reubicaciones, solas o debajo de cada instrucción. | objdump -dr suma.o |
objdump -d, -h, -t | Desensamblado, secciones y tabla de símbolos. | objdump -d calculo |
size | Tamaño de text, data y bss. | size calculo |
-Wl,-Map=archivo | El mapa de enlace: qué secciones de qué objeto quedaron en qué dirección. | gcc -Wl,-Map=m.map … |
-Wl,-y,nombre, -Wl,--cref | Quién define y quién referencia un símbolo; tabla cruzada. | gcc -Wl,-y,limite … |
strip | Quita .symtab y la información de depuración. | strip calculo |
-ffunction-sections y -Wl,--gc-sections | Descartar las funciones que nadie usa. | sección 8 |
10. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
| Buscar un error de enlace en el código C | El enlazador no ve C ni líneas: ve símbolos y secciones. | Mirar nm de los objetos y la línea de gcc. |
Olvidar un archivo .o o .c en la línea de gcc | undefined reference a todo lo que define. | Agregarlo; nm -A dice cuál es. |
Función definida pero declarada static | undefined reference aunque el código exista. | Quitar static si debe verse desde afuera. |
Olvidar -lm (u otra biblioteca) | undefined reference to `sqrt'. | Agregar -lm al final de la línea. |
Corregir las líneas de warning del enlazador antes del error | Son efectos de la misma causa. | Resolver el primer undefined reference. |
| Definir una variable o función en un encabezado | multiple definition con cada .c que lo incluya. | Definir en un .c; declarar con extern en el .h. |
int x; en dos archivos | Error desde gcc 10; antes funcionaba por los símbolos comunes. | Una definición y extern en las demás. |
Arreglar un viejo proyecto con -fcommon | Oculta dos variables distintas con el mismo nombre. | Corregir el diseño. |
| Declarar con un tipo y definir con otro | El enlace sale bien y el programa falla: el enlazador no ve tipos. | Un solo encabezado, incluido en la definición. |
| Suponer que el orden de los archivos no importa | Cambia las direcciones; con bibliotecas, puede cambiar el resultado (tema 32). | Objetos primero, bibliotecas al final. |
Hacer strip antes de depurar | Sin nombres ni líneas, gdb y nm no ayudan. | Conservar una copia con símbolos. |
Usar --gc-sections sin -ffunction-sections | Casi no descarta nada: cada objeto es una sola sección. | Las dos opciones juntas. |
11. Resumen
- Un archivo objeto es un ELF relocatable: secciones de datos y código, todavía sin dirección, una tabla de símbolos (
.symtab) y reubicaciones (.rela.text). - Un símbolo es un nombre con una posición. Está definido si tiene sección (
Ndx) e indefinido si diceUND. EsGLOBALsi se ofrece a otros archivos,LOCALsi esstatic, oWEAK. El enlazador no conoce tipos ni mangla nombres en C. - La letra de
nmresume sección y visibilidad:Tcódigo,Ddatos,Bsin valor,Rsolo lectura,Uindefinido,Wdébil,Ccomún; mayúscula global, minúscula local. - El compilador deja huecos de cuatro bytes en el código para lo que no sabe, y una reubicación por hueco: desplazamiento, tipo, símbolo y addend. El valor que se escribe es S + A − P; el addend −4 compensa que
%ripya apunta a la instrucción siguiente. - El enlazador resuelve los símbolos (cada indefinido necesita una definición), junta las secciones en el orden de la línea de órdenes y aplica las reubicaciones.
-Wl,-Map,-Wl,-yy-Wl,--crefmuestran lo que decidió. - Dos definiciones fuertes del mismo símbolo son un error; una fuerte vence a una débil; una débil indefinida vale 0. Los símbolos comunes (
-fcommon) ya no son el valor por omisión desdegcc10. - Los errores de enlace hablan en secciones:
undefined reference(falta un archivo, esstatic, o falta-l) ymultiple definition(definido en dos lugares). Un procedimiento fijo los resuelve: nombre exacto,nmpara buscar la definición, mirar la letra, mirar el manual. - El ejecutable dinámico lleva una segunda tabla,
.dynsym, y reubicaciones dinámicas (JUMP_SLOT,GLOB_DAT) que el cargador completa al ejecutar, conNEEDEDcomo lista de bibliotecas. stripquita.symtaby la depuración;-ffunction-sectionscon-Wl,--gc-sectionsdescartan el código que nadie usa.
12. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué significa que suma.o sea un archivo «relocatable»?
Que es una pieza todavía no ubicada: sus secciones empiezan en la dirección 0, y el código contiene huecos para las direcciones que se conocerán recién al enlazar. Lleva consigo las reubicaciones que dicen cómo llenarlos. El enlazador decide dónde queda cada sección y escribe los valores definitivos.
2. En readelf -s, ¿cómo se distingue un símbolo definido de uno indefinido, y uno local de uno global?
Un símbolo definido tiene en la columna Ndx el número de la sección donde está; uno indefinido dice UND. La columna Bind distingue GLOBAL (visible desde otros archivos) de LOCAL (propio del archivo, como una variable o función static) y de WEAK.
3. ¿Qué diferencia hay entre una t y una T de nm, y por qué importa en un undefined reference?
La T es una función global y la t, una local, es decir, static. El enlazador no une un símbolo local con las referencias de otros archivos. Por eso una función definida como static produce undefined reference desde otro módulo aunque el código exista: nm muestra la t y delata el problema.
4. ¿Por qué el compilador deja 00 00 00 00 en un call a otra función, y qué es la entrada R_X86_64_PLT32 sumar_hasta-0x4?
Porque todavía no sabe a qué distancia va a quedar la función. El hueco de cuatro bytes se llenará después, y la entrada de reubicación dice dónde está (el Offset), cómo calcular el valor (el tipo PLT32, una distancia relativa de 32 bits), a qué símbolo se refiere y qué sumar (el addend, −4).
5. ¿De dónde sale el −4 del addend, y cómo se calcula el valor que se escribe?
El procesador calcula una dirección relativa a %rip sumando el desplazamiento a la dirección de la instrucción siguiente, cuatro bytes después del campo. El valor a escribir es S − (P + 4) = S + A − P, con S la dirección del símbolo, P la del hueco y A = −4.
6. Comprueba la reubicación de call sumar_hasta: el hueco está en 0x1157 y sumar_hasta en 0x1182. ¿Qué bytes se escriben?
0x1182 − 4 − 0x1157 = 0x27, que en cuatro bytes little-endian es 27 00 00 00. En el desensamblado del ejecutable, la instrucción aparece como e8 27 00 00 00, que es lo que calculó el enlazador.
7. ¿Cuáles son los tres pasos del enlazador?
Resolver los símbolos (cada indefinido se empareja con una definición); asignar direcciones, juntando las secciones del mismo tipo de todos los objetos en el orden de la línea; y aplicar las reubicaciones, escribiendo en cada hueco el valor calculado con las direcciones definitivas.
8. ¿Qué pasa si una función tiene una definición débil en un archivo y una fuerte en otro?
Gana la fuerte, en cualquier orden de la línea de órdenes, y no hay error. Si solo hay una definición débil, se usa esa. Esto permite ofrecer una versión por omisión que otro módulo puede reemplazar sin modificar el original.
9. ¿Qué vale la dirección de un símbolo débil indefinido, y cómo lo aprovecha un programa?
Vale 0 (NULL), y el enlace no da error. El programa puede comprobar if (funcion != NULL) antes de llamarla, para usar una función opcional solo si algún módulo la define.
10. ¿Por qué dos archivos con int cuenta; enlazaban antes y hoy dan multiple definition?
Antes, gcc trataba una variable global sin valor y sin extern como un símbolo común, que el enlazador unificaba con las demás del mismo nombre. Desde gcc 10 el valor por omisión es -fno-common: cada una es una definición fuerte en .bss. La forma correcta es definir la variable en un solo .c y declararla con extern en los demás.
11. Un enlace falla con undefined reference to `sqrt', aunque se incluyó math.h. ¿Qué pasó?
math.h solo declara la función, lo que satisface al compilador. La definición está en la biblioteca matemática, que es aparte de libc, y hay que pedirla al enlazar con -lm. El error es del enlace, y por eso el mensaje cita el offset de la llamada y no una línea de C.
12. En el enlace sin datos.o aparecen advertencias como «creating DT_TEXTREL in a PIE». ¿Hay que corregirlas?
No: son consecuencias de los símbolos indefinidos, porque el enlazador, al no encontrar visitas ni limite, no puede resolver esas reubicaciones de la manera normal. Se corrige la causa, el archivo que falta, y las advertencias desaparecen con ella.
13. ¿Qué diferencia hay entre .symtab y .dynsym?
.symtab es la tabla completa de símbolos del programa, para herramientas como nm y gdb; no se necesita para ejecutarlo y strip la elimina. .dynsym es la tabla mínima de lo que cruza la frontera con las bibliotecas compartidas, que el cargador usa al arrancar el programa; strip la conserva.
14. ¿Para qué se combinan -ffunction-sections y -Wl,--gc-sections?
El enlazador solo puede descartar secciones enteras. Con -ffunction-sections el compilador pone cada función en su propia sección, y con --gc-sections el enlazador elimina las que no se alcanzan desde el punto de entrada. Con solo la segunda, cada objeto sigue siendo una única sección .text y no se descarta casi nada.
Para practicar
- Compila
datos.c,suma.cyprincipal.ccon-O2en lugar de-O0, y compara conobjdump -drlas reubicaciones desuma.o. ¿Cuántas quedan? ¿Siguen siendoR_X86_64_PC32? - Calcula a mano, con la fórmula S + A − P, la reubicación de la segunda referencia a
limite(la del offset 0x27 desuma.o) con las direcciones del ejecutable, y compárala con los bytes que muestraobjdump -d calculo. - Enlaza los tres objetos en otro orden (
datos.o suma.o principal.o), pide el mapa y verifica que cambian las direcciones pero el programa imprime lo mismo. ¿Dónde queda ahoramain? - Agrega una función
staticasuma.cy llámala desdesumar_hasta. Compila con-O0y con-O2, y mira connmsi el símbolo existe en cada caso. ¿Por qué puede desaparecer? - Escribe dos archivos con una función débil por omisión y una fuerte que la reemplaza, y agrega una tercera definición débil en otro archivo. Enlaza en varios órdenes y confirma qué definición se usa. ¿Qué pasa si hay dos definiciones fuertes?
- Define
int x;en dos archivos y enlázalos con-fcommon; luego cambia el tipo de uno along x;y vuelve a enlazar. Miranm -Sy explica por qué el enlace no se queja. - Escribe un programa que use
pthread_createosin, y reproduce los dos errores clásicos: olvidar-lmo-pthread. Diagnostícalos solo con el mensaje yman. - Compila con
-ffunction-sections -Wl,--gc-sectionsun programa con funciones sin usar y comparasizeynmcon y sin las opciones. Después hazstripy compara otra vez.