1. Qué es una biblioteca dinámica
Una biblioteca dinámica (también llamada compartida, o shared object, de ahí la extensión .so) es un archivo ELF como el ejecutable, pero pensado para ser cargado por otros. Se diferencia de la estática en tres cosas:
- El enlace es incompleto a propósito. Al enlazar el programa, el enlazador verifica que los símbolos existan, pero no copia el código: deja en el ejecutable una entrada
NEEDEDcon el nombre de la biblioteca y una tabla de símbolos por resolver. - El trabajo final lo hace el cargador al ejecutar. Al arrancar el proceso, el núcleo no carga directamente tu programa: carga primero el intérprete (
/lib64/ld-linux-x86-64.so.2, conocido comold.so), y éste busca cadaNEEDED, lo mapea en memoria y resuelve las referencias. - El código se comparte. Las páginas de código de la biblioteca se mapean desde el mismo archivo en todos los procesos que la usan, de modo que hay una sola copia en la memoria física. Cada proceso conserva, claro, sus propios datos.
Usaremos el mismo ejemplo del tema 32: dos módulos (numeros.c y texto.c), un encabezado único en inc/util.h y un cliente prog.c. Esta vez agregamos dos detalles para ver mejor lo que pasa: una variable global llamadas que cuenta cuántas funciones numéricas se han usado, y una función version_util() en version.c que devuelve la versión de la biblioteca como texto.
/* inc/util.h */
#ifndef UTIL_H
#define UTIL_H
#include <stddef.h>
extern int llamadas;
int maximo(const int *v, size_t n);
double promedio(const int *v, size_t n);
void invertir(char *s);
void mayusculas(char *s);
const char *version_util(void);
#endif
/* numeros.c */
#include "util.h"
int llamadas;
int maximo(const int *v, size_t n)
{
llamadas++;
int m = v[0];
for (size_t i = 1; i < n; i++)
if (v[i] > m)
m = v[i];
return m;
}
double promedio(const int *v, size_t n)
{
llamadas++;
long s = 0;
for (size_t i = 0; i < n; i++)
s += v[i];
return (double)s / (double)n;
}
/* version.c */
#include "util.h"
const char *version_util(void)
{
return "1.0";
}
/* prog.c */
#include <stdio.h>
#include "util.h"
int main(void)
{
int v[] = {4, 9, 2, 7};
char s[] = "enlace";
printf("maximo=%d promedio=%.2f\n", maximo(v, 4), promedio(v, 4));
invertir(s);
printf("%s (util %s, %d llamadas)\n", s, version_util(), llamadas);
return 0;
}
2. Crearla: -fPIC y -shared
Hacen falta dos cambios respecto de una biblioteca estática. Al compilar cada fuente se agrega -fPIC, y al enlazar se usa -shared en lugar de ar:
$ gcc -std=c17 -Wall -Wextra -Iinc -fPIC -c numeros.c texto.c version.c
$ mkdir libs
$ gcc -shared -o libs/libbasico.so numeros.o texto.o version.o
$ file libs/libbasico.so | sed 's/, BuildID.*//'
libs/libbasico.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked
$ nm -D libs/libbasico.so | grep -E ' (T|U|B|D) '
0000000000001259 T invertir
000000000000401c B llamadas
0000000000001139 T maximo
00000000000012e6 T mayusculas
00000000000011b7 T promedio
U strlen@GLIBC_2.2.5
U toupper@GLIBC_2.2.5
0000000000001325 T version_util
El resultado es un shared object ELF. nm -D muestra la tabla de símbolos dinámicos, la que usa el cargador (-D es de dynamic): lo que la biblioteca ofrece (T, y B para la variable llamadas) y lo que le falta (U): strlen y toupper se resolverán contra libc.so.6, con la versión de símbolo GLIBC_2.2.5 que pide cada una. A diferencia del .a del tema anterior, una biblioteca dinámica sí recuerda de qué otras depende: lo registra con entradas NEEDED, que se ven con readelf -d:
$ readelf -d libs/libbasico.so | grep -E 'SONAME|NEEDED'
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
¿Qué es -fPIC?
Son las siglas de Position Independent Code, código independiente de la posición. Una biblioteca dinámica se mapea en la dirección que el sistema elija en cada ejecución, y varios procesos pueden verla en direcciones distintas. Para que una sola copia del código sirva en todas, ese código no puede contener direcciones absolutas: accede a sus propias funciones con saltos relativos y a las variables globales a través de una tabla (la GOT, Global Offset Table) que el cargador rellena. Como el código no necesita ajustes al cargarlo, queda de solo lectura y se puede compartir entre procesos.
Si se omite, el enlazador lo detecta cuando el código accede a datos globales. Para demostrarlo hay que apagar a propósito el modo que gcc usa por defecto, porque en una distribución moderna gcc compila con -fPIE (el ejecutable de posición independiente del tema 31) y ese código ya es casi reubicable:
$ gcc -std=c17 -Wall -Wextra -Iinc -fno-pic -c numeros.c -o np.o
$ gcc -shared -o np.so np.o
/usr/bin/x86_64-linux-gnu-ld.bfd: np.o: warning: relocation against `llamadas' in read-only section `.text'
/usr/bin/x86_64-linux-gnu-ld.bfd: np.o: relocation R_X86_64_PC32 against symbol `llamadas' can not be used when making a shared object; recompile with -fPIC
/usr/bin/x86_64-linux-gnu-ld.bfd: final link failed: bad value
collect2: error: ld returned 1 exit status
El mensaje nombra la solución (recompile with -fPIC). La regla es sencilla: todo objeto que vaya a una biblioteca dinámica se compila con -fPIC, aunque en algún caso concreto el enlace no se queje. El costo en rendimiento es pequeño en x86-64 y casi nunca es una razón para no usarlo.
3. Usarla y no encontrarla
El enlace del programa es idéntico al de la biblioteca estática: -L para el directorio y -l para el nombre. Si en el directorio hay libbasico.so, el enlazador la prefiere a un libbasico.a (lo vemos en la sección 9):
$ gcc -std=c17 -Wall -Wextra -Iinc -c prog.c
$ gcc -o prog prog.o -Llibs -lbasico
$ ./prog
./prog: error while loading shared libraries: libbasico.so: cannot open shared object file: No such file or directory
$ echo $?
127
El enlace fue bien, pero la ejecución falla antes de llegar a main. Es el error más típico de este tema, y la primera cosa para entenderlo es la diferencia entre dos momentos: -L le dice al enlazador dónde mirar al construir el programa; no deja ninguna huella para el cargador. Cuando se ejecuta, ld.so empieza de cero a buscar libbasico.so y no sabe nada de libs/.
La herramienta para ver el estado de las dependencias es ldd:
$ ldd prog
linux-vdso.so.1 (0x00007ffcef59f000)
libbasico.so => not found
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007faf47a00000)
/lib64/ld-linux-x86-64.so.2 (0x00007faf47dfd000)
$ readelf -d prog | grep -E 'NEEDED|RPATH|RUNPATH'
0x0000000000000001 (NEEDED) Shared library: [libbasico.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
ldd simula la carga y muestra, para cada dependencia, el archivo que encontraría o not found. La salida es la lista del readelf -d resuelta: libbasico.so y libc.so.6 son las entradas NEEDED; linux-vdso.so.1 no es un archivo, sino una pequeña biblioteca que el propio núcleo inyecta en cada proceso (para llamadas al sistema rápidas, como la hora), y el último renglón es el intérprete.
Una advertencia sobre ldd. En algunas implementaciones ldd ejecuta el programa con una variable especial para que el cargador imprima la lista. Con un programa que no es de confianza, eso es ejecutarlo. Para inspeccionar un binario ajeno es más seguro readelf -d (que solo lee el archivo, pero no resuelve nada) o objdump -p.
4. El soname y los nombres con versión
Hasta aquí, el nombre que quedó anotado en el programa es el del archivo (libbasico.so). Eso es frágil: la biblioteca evoluciona, y un programa enlazado contra la versión 1 no puede funcionar con una versión 2 incompatible aunque ambas se llamen igual. Para resolverlo, las bibliotecas dinámicas llevan un nombre interno, el soname (shared object name), que se graba en la propia biblioteca y se copia al ejecutable como su NEEDED.
La convención usa tres nombres para un mismo archivo:
| Nombre | Quién lo usa | Qué es |
|---|---|---|
libbasico.so.1.0.0 | Nadie directamente | El archivo real, con la versión completa (real name). |
libbasico.so.1 | El cargador, al ejecutar | El soname: enlace simbólico al archivo real. Cambia solo cuando cambia la interfaz de forma incompatible. |
libbasico.so | El enlazador, al compilar | El «nombre del enlazador» (linker name): enlace simbólico que permite escribir -lbasico. |
Se arma con la opción -Wl,-soname,… y dos enlaces simbólicos:
$ gcc -shared -Wl,-soname,libbasico.so.1 -o libs/libbasico.so.1.0.0 numeros.o texto.o version.o
$ ln -s libbasico.so.1.0.0 libs/libbasico.so.1
$ ln -s libbasico.so.1 libs/libbasico.so
$ ls -l libs | awk 'NR>1{print $9,$10,$11}'
libbasico.so -> libbasico.so.1
libbasico.so.1 -> libbasico.so.1.0.0
libbasico.so.1.0.0
$ readelf -d libs/libbasico.so.1.0.0 | grep SONAME
0x000000000000000e (SONAME) Library soname: [libbasico.so.1]
$ gcc -o prog prog.o -Llibs -lbasico
$ readelf -d prog | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libbasico.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
El programa se enlazó con -lbasico (a través del enlace libbasico.so), pero lo que quedó anotado es libbasico.so.1, el soname. Eso es lo que buscará el cargador. De esta forma, una versión nueva y compatible, libbasico.so.1.1.0, se instala cambiando el enlace libbasico.so.1 y todos los programas la aprovechan; una versión incompatible se publica como libbasico.so.2, que convive con la anterior, y los programas viejos siguen usando la 1 hasta que se recompilen.
libbasico.so; el cargador, por el soname que el ejecutable guardó.Los números de la versión siguen una convención de tres partes, mayor.menor.parche: el mayor (el del soname) cambia cuando se rompe la compatibilidad binaria (se quita o cambia una función, o la forma de una estructura pública), el menor cuando se agrega algo sin romper nada, y el de parche con las correcciones internas. Lo importante: lo que el programa exige es solo el mayor, y la compatibilidad binaria (ABI, Application Binary Interface) es una promesa del autor de la biblioteca, que ni gcc ni el cargador verifican.
Una última herramienta: el comando ldconfig -n directorio crea los enlaces del soname a partir de los archivos de ese directorio. No necesita permisos si el directorio es tuyo; borramos libbasico.so.1 y la regeneramos:
$ rm libs/libbasico.so.1
$ ldconfig -n libs; ls -l libs | awk 'NR>1{print $9,$10,$11}'
libbasico.so -> libbasico.so.1
libbasico.so.1 -> libbasico.so.1.0.0
libbasico.so.1.0.0
Lo que ldconfig -n arregla es el enlace del soname (lee el SONAME de cada archivo); el enlace libbasico.so, que solo importa para compilar, existe porque ya lo habíamos creado y lo sigue ofreciendo el empaquetador (en Debian/Ubuntu va en el paquete -dev, por eso instalar solo una biblioteca no basta para compilar con ella).
5. Cómo busca el cargador
Cuando ld.so necesita libbasico.so.1 busca, en este orden:
- Las rutas del
RPATHdel ejecutable (si lo tiene y no tieneRUNPATH; sección 6). - La variable de entorno
LD_LIBRARY_PATH. - Las rutas del
RUNPATHdel ejecutable. - La caché
/etc/ld.so.cache, que armaldconfig(sección 7). - Los directorios por defecto:
/liby/usr/lib(con los subdirectorios de la arquitectura, como/lib/x86_64-linux-gnu).
Hay una forma de verlo trabajar sin adivinar: la variable LD_DEBUG=libs hace que el cargador cuente lo que intenta. Con el programa sin encontrar su biblioteca:
$ LD_DEBUG=libs ./prog 2>&1 | head -9 | cut -c1-110
538: find library=libbasico.so.1 [0]; searching
538: search cache=/etc/ld.so.cache
538: search path=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3:/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2:...
538: trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libbasico.so.1
538: (no such file)
538: trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libbasico.so.1
538: (no such file)
538: trying file=/lib/x86_64-linux-gnu/libbasico.so.1
538: (no such file)
Primero la caché, luego los directorios del sistema (probando antes los subdirectorios glibc-hwcaps con variantes optimizadas para procesadores más nuevos), y al final el error. El número es el pid del proceso. Esta salida es la mejor respuesta a «¿por qué no encuentra mi biblioteca?»: muestra cada ruta que se probó.
LD_LIBRARY_PATH
La forma rápida de decirle al cargador dónde buscar es la variable LD_LIBRARY_PATH, con directorios separados por :. Se puede poner solo para un comando:
$ LD_LIBRARY_PATH=libs ./prog
maximo=9 promedio=5.50
ecalne (util 1.0, 2 llamadas)
$ LD_LIBRARY_PATH=libs ldd prog
linux-vdso.so.1 (0x00007ffe74b1b000)
libbasico.so.1 => libs/libbasico.so.1 (0x00007b119a6d1000)
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007b119a400000)
/lib64/ld-linux-x86-64.so.2 (0x00007b119a6dd000)
Es muy útil para probar y para desarrollo, pero es una mala herramienta para distribuir un programa: depende de cómo lo lancen (un menú gráfico, un servicio de systemd o cron no heredan tu entorno), afecta a todos los programas que arranquen con esa variable, y el cargador la ignora por completo en los programas con setuid, por seguridad. Tampoco es lo mismo que LIBRARY_PATH del tema 32: aquella es del enlazador (en la compilación), ésta es del cargador (al ejecutar).
Un riesgo de seguridad. Quien controla LD_LIBRARY_PATH controla qué código se carga dentro de un programa. Por eso el cargador la desactiva en ejecutables setuid y nunca debe apuntar a directorios donde otros puedan escribir (/tmp, por ejemplo). Tampoco es buena idea dejarla permanentemente en el ~/.bashrc: es una fuente clásica de «solo falla en mi máquina».
6. Incrustar la ruta: rpath y $ORIGIN
Si la biblioteca no va a instalarse en el sistema sino a viajar junto con el programa, lo más limpio es grabar la ruta de búsqueda dentro del ejecutable. Se hace con -Wl,-rpath,RUTA. Con una ruta absoluta el programa solo sirve en esa ubicación; con $ORIGIN, que el cargador reemplaza por el directorio donde está el ejecutable, la ruta es relativa al programa, y el conjunto puede moverse:
$ gcc -o prog_rp prog.o -Llibs -lbasico -Wl,-rpath,'$ORIGIN/libs'
$ readelf -d prog_rp | grep -E 'RUNPATH|RPATH'
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/libs]
$ ./prog_rp
maximo=9 promedio=5.50
ecalne (util 1.0, 2 llamadas)
$ mkdir mov && cp prog_rp mov/ && cp -r libs mov/
$ ./mov/prog_rp
maximo=9 promedio=5.50
ecalne (util 1.0, 2 llamadas)
El programa funciona sin variables de entorno, y también desde la copia en mov/, porque busca su biblioteca en libs/ al lado de donde esté él. Es la técnica que usan los programas que se distribuyen como un directorio autocontenido.
Hay dos variantes de la entrada, y la diferencia importa: RPATH es la antigua y se busca antes que LD_LIBRARY_PATH; RUNPATH es la moderna y se busca después. gcc genera RUNPATH por defecto en las distribuciones actuales; con -Wl,--disable-new-dtags se pide el antiguo. Para comprobarlo se arma una versión 2.0 de la biblioteca en el directorio v2/ y se la ofrece con LD_LIBRARY_PATH a dos ejecutables, uno con cada tipo:
$ gcc -o prog_rp2 prog.o -Llibs -lbasico -Wl,-rpath,'$ORIGIN/libs' -Wl,--disable-new-dtags
$ readelf -d prog_rp2 | grep -E 'RUNPATH|RPATH'
0x000000000000000f (RPATH) Library rpath: [$ORIGIN/libs]
$ LD_LIBRARY_PATH=v2 ./prog_rp
maximo=9 promedio=5.50
ecalne (util 2.0, 2 llamadas)
$ LD_LIBRARY_PATH=v2 ./prog_rp2
maximo=9 promedio=5.50
ecalne (util 1.0, 2 llamadas)
$ LD_LIBRARY_PATH=v2 LD_DEBUG=libs ./prog_rp 2>&1 | grep -E 'search path=|calling init: .*basico' | cut -c1-120
472: search path=v2/glibc-hwcaps/x86-64-v3:v2/glibc-hwcaps/x86-64-v2:v2 (LD_LIBRARY_PATH)
472: calling init: v2/libbasico.so.1
Con RUNPATH (prog_rp), la variable gana y se carga la versión 2.0; con RPATH (prog_rp2), manda la ruta incrustada y la variable se ignora. La razón por la que se reemplazó RPATH es esa: con RUNPATH el usuario conserva la posibilidad de redirigir una biblioteca con LD_LIBRARY_PATH, por ejemplo para probar una versión nueva o depurar. El mismo LD_DEBUG lo confirma: el cargador encontró libbasico.so.1 en v2/ y ya no siguió buscando.
Una trampa conocida: si un directorio de instalación fijo termina en el rpath de un ejecutable, actualizar la biblioteca en otro lugar no tiene efecto. El rpath de un programa se puede modificar después de compilado con la herramienta patchelf (en los repositorios, no viene en todas las instalaciones) si hace falta.
7. La caché del sistema: ldconfig
Buscar en veinte directorios cada vez que arranca un programa sería lento. Por eso el cargador consulta primero un archivo binario, /etc/ld.so.cache, que contiene una tabla «nombre de biblioteca → ruta». Lo genera el comando ldconfig, que recorre los directorios por defecto más los que se listan en /etc/ld.so.conf y en los archivos de /etc/ld.so.conf.d/ (uno o más directorios por línea). Se puede consultar:
$ ldconfig -p | head -1
322 libs found in cache `/etc/ld.so.cache'
$ ldconfig -p | grep -E 'libc.so.6|libm.so.6'
libm.so.6 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libm.so.6
libc.so.6 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libc.so.6
Para instalar nuestra biblioteca de forma que todos los programas del sistema la encuentren, el procedimiento (como administrador) es: copiar el archivo real al directorio, por convención /usr/local/lib, que ya figura en la configuración; ejecutar sudo ldconfig, que actualiza la caché y crea el enlace del soname; y comprobar con ldconfig -p | grep basico. Si se usa un directorio propio, se agrega un archivo en /etc/ld.so.conf.d/ con la ruta y se vuelve a ejecutar ldconfig. Cuando la instalación se hace con un gestor de paquetes, el ldconfig corre solo al final.
Para probar el mecanismo sin tocar el sistema, ldconfig acepta una caché y una configuración propias, con -C y -f:
$ echo "$PWD/libs" > mi.conf
$ ldconfig -C mi.cache -f mi.conf
$ ldconfig -C mi.cache -p | grep basico
libbasico.so.1 (libc6,x86-64) => /tmp/t33/libs/libbasico.so.1
libbasico.so (libc6,x86-64) => /tmp/t33/libs/libbasico.so
La caché registra el soname y el nombre del enlazador, y apunta al enlace. El cargador usaría esa tabla si fuera /etc/ld.so.cache. El resultado de la caché se queda desactualizado si se instala una biblioteca nueva sin ejecutar ldconfig: otra causa clásica de «el archivo está ahí, pero no lo encuentra».
8. Actualizar sin reenlazar y LD_PRELOAD
La gran ventaja de lo anterior se ve en la práctica. Con un programa ya compilado, ejecutado con RUNPATH en libs/, se reemplaza el archivo real por la versión 2.0 (compatible, mismo soname) sin tocar el programa:
$ ./prog_rp
maximo=9 promedio=5.50
ecalne (util 1.0, 2 llamadas)
$ cp v2/libbasico.so.1 libs/libbasico.so.1.0.0
$ ./prog_rp
maximo=9 promedio=5.50
ecalne (util 2.0, 2 llamadas)
El mismo ejecutable (prog_rp no se recompiló ni se volvió a enlazar) imprime ahora util 2.0. Es el mecanismo con el que una corrección de seguridad de libc o de openssl llega a todos los programas del sistema con una sola actualización. Y es también el peligro: si la versión «nueva» cambia una firma sin cambiar el soname, los programas viejos fallan o, peor, se comportan mal sin avisar. Es la promesa de ABI de la que hablamos en la sección 4.
El mismo principio permite una técnica de depuración muy usada, la interposición de símbolos. La variable LD_PRELOAD lista bibliotecas que se cargan antes que las demás, y el cargador resuelve cada símbolo con la primera que lo defina. Una biblioteca de una función:
/* mi_max.c */
#include <stddef.h>
int maximo(const int *v, size_t n)
{
(void)v; (void)n;
return -1;
}
$ gcc -std=c17 -Wall -Wextra -fPIC -shared -o mi_max.so mi_max.c
$ LD_PRELOAD=./mi_max.so ./prog_rp
maximo=-1 promedio=5.50
ecalne (util 2.0, 1 llamadas)
El programa no se tocó, y la función maximo que ejecuta es la de mi_max.so; promedio sigue siendo la original. Notar que el contador llamadas vale 1, no 2: la versión sustituta no incrementa la variable. Es lo que hacen las herramientas que registran las llamadas a malloc o simulan fallos para probar cómo reacciona un programa. Con las bibliotecas estáticas del tema 32 el mismo efecto se lograba al enlazar (un objeto propio ganaba a la biblioteca); aquí se consigue al ejecutar, sin recompilar. Como el cargador la ignora en programas setuid, no sirve para escalar privilegios.
Si alguna vez la depuración te muestra una función «imposible» en una biblioteca, comprobar echo $LD_PRELOAD y el archivo /etc/ld.so.preload es un diagnóstico barato.
Cómo se ve en memoria
Mapeada en el proceso, la biblioteca ocupa varias regiones, cada una con sus permisos. El programa mapas.c lee /proc/self/maps y muestra las líneas de libbasico:
$ ./mapas
2
7e3def6c4000-7e3def6c5000 r--p 00000000 00:47 77 .../libs_ok/libbasico.so.1.0.0
7e3def6c5000-7e3def6c6000 r-xp 00001000 00:47 77 .../libs_ok/libbasico.so.1.0.0
7e3def6c6000-7e3def6c7000 r--p 00002000 00:47 77 .../libs_ok/libbasico.so.1.0.0
7e3def6c7000-7e3def6c8000 r--p 00002000 00:47 77 .../libs_ok/libbasico.so.1.0.0
7e3def6c8000-7e3def6c9000 rw-p 00003000 00:47 77 .../libs_ok/libbasico.so.1.0.0
De arriba abajo: la cabecera ELF (solo lectura), el código (r-xp, la región que se comparte entre procesos: lectura y ejecución, sin escritura), los datos de solo lectura, los datos que el cargador completa y deja protegidos, y los datos escribibles (rw-p, donde vive llamadas). La p final significa privado: cada proceso tiene su propia copia de lo que escribe. La dirección de arranque (7e3def6c4000) cambia en cada ejecución, y es la razón de ser de -fPIC.
9. Dinámica o estática cuando hay ambas
Si en el mismo directorio conviven libbasico.so y libbasico.a, -lbasico toma la dinámica. Para forzar la estática hay tres formas (todas las del tema 32), y ldd confirma qué resultó:
$ ar rcs libs/libbasico.a numeros.o texto.o version.o
$ gcc -o p_dyn prog.o -Llibs -lbasico
$ LD_LIBRARY_PATH=libs ldd p_dyn | grep -c basico
1
$ gcc -o p_sta prog.o -Llibs -l:libbasico.a
$ ldd p_sta | grep -c basico
0
$ gcc -o p_sta2 prog.o -Llibs -Wl,-Bstatic -lbasico -Wl,-Bdynamic
$ ldd p_sta2 | grep -c basico
0
$ gcc -o p_sta3 prog.o libs/libbasico.a
$ ldd p_sta3 | grep -c basico
0
La del .a sale con 0: el código está dentro del ejecutable y no hay dependencia que buscar. Cuál elegir es el compromiso de la tabla del tema 32: la dinámica comparte memoria, se actualiza sola y obliga a resolver la búsqueda; la estática es autosuficiente pero hay que reenlazar para actualizarla. Una biblioteca que se publica para que otros la usen suele ofrecer las dos.
Una biblioteca dinámica, además, puede tener símbolos sin definir sin que el enlace se queje, porque la resolución final queda para el ejecutable y el cargador. El error aparece más tarde, cuando alguien la usa:
/* roto.c */
int externa(void);
int f(void) { return externa(); }
$ gcc -shared -fPIC -o libs/libroto.so roto.c && echo "libroto.so creada sin error"
libroto.so creada sin error
$ gcc -shared -fPIC -Wl,-z,defs -o libs/libroto.so roto.c
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccGa8Pfm.o: in function `f':
roto.c:(.text+0x9): undefined reference to `externa'
collect2: error: ld returned 1 exit status
Sin opciones, la biblioteca se crea aunque externa no exista en ninguna parte. Con -Wl,-z,defs el error aparece al construirla, que es donde conviene verlo: hace que los símbolos sin definir de la biblioteca sean un error de enlace. Es una buena opción por defecto para bibliotecas propias. (Aun sin esa opción, quien intente enlazar un programa con libroto.so verá libs/libroto.so: undefined reference to `externa', y el problema sale antes de ejecutar.)
10. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
Compilar los objetos sin -fPIC | relocation … can not be used when making a shared object; recompile with -fPIC. | Compilar todo lo que va a la .so con -fPIC. |
Enlazar con -L y ejecutar sin decirle nada al cargador | error while loading shared libraries: …: cannot open shared object file, código 127. | LD_LIBRARY_PATH para probar; rpath con $ORIGIN o ldconfig para instalar. |
Confundir -L y LIBRARY_PATH (enlace) con LD_LIBRARY_PATH (ejecución) | El programa enlaza bien y no arranca. | Cada una en su momento; ldd muestra qué ve el cargador. |
| Olvidar el soname | El ejecutable queda anotado con el nombre del archivo; cualquier cambio de nombre o versión lo rompe. | -Wl,-soname,libNOMBRE.so.N y los enlaces simbólicos. |
| Cambiar la interfaz sin subir el número mayor | Los programas viejos cargan la nueva y fallan o se comportan mal sin aviso. | Subir el mayor del soname (libNOMBRE.so.2) ante todo cambio incompatible. |
Instalar la biblioteca en un directorio y no ejecutar ldconfig | El archivo existe y el cargador no lo encuentra, porque la caché es vieja. | sudo ldconfig tras instalar; ldconfig -p | grep nombre para comprobar. |
Dejar LD_LIBRARY_PATH fija en el ~/.bashrc | Cambia qué bibliotecas usan programas ajenos; «en mi máquina funciona». | Ponerla solo en el comando que la necesita, o usar rpath. |
Poner /tmp o un directorio compartido en LD_LIBRARY_PATH o en el rpath | Otro usuario puede dejar una biblioteca falsa que se cargará en tu programa. | Solo directorios que controles. |
Escribir -Wl,-rpath,$ORIGIN/libs sin comillas | El shell expande $ORIGIN (vacío) y la ruta queda como /libs. | Comillas simples: '$ORIGIN/libs'. En un Makefile (tema 35) se duplica el signo: $$ORIGIN. |
Esperar que LD_LIBRARY_PATH gane a un rpath | Con RPATH (el antiguo) la variable se ignora; con RUNPATH funciona. | readelf -d para ver cuál tiene el binario. |
Ejecutar ldd sobre un binario que no es de confianza | En algunas implementaciones, ldd ejecuta el programa. | readelf -d u objdump -p. |
| Confiar en que la biblioteca no tiene símbolos sin definir | El error se descubre cuando alguien enlaza o ejecuta. | -Wl,-z,defs al construirla. |
Distribuir el .so sin el encabezado o sin el enlace libNOMBRE.so | Quien quiera compilar contra ella no puede. | Entregar encabezados y el enlace del enlazador (el paquete -dev). |
11. Resumen
- Una biblioteca dinámica (
.so) se enlaza sin copiar su código: el ejecutable guarda una entradaNEEDEDy el cargadorld.sola busca y la mapea al arrancar. El código se comparte entre procesos. - Se crea compilando con
-fPIC(código independiente de la posición) y enlazando congcc -shared.nm -Dyreadelf -dmuestran sus símbolos dinámicos y sus dependencias. - El soname (
-Wl,-soname,libX.so.1) es el nombre que se graba en el ejecutable. La convención usa tres nombres: archivo reallibX.so.1.0.0, sonamelibX.so.1y nombre del enlazadorlibX.so, con enlaces simbólicos. El número mayor cambia solo con incompatibilidades de ABI. -Ly-lsirven al compilar; el cargador busca al ejecutar, con su propio orden:RPATH,LD_LIBRARY_PATH,RUNPATH, la caché/etc/ld.so.cachey/lib,/usr/lib.lddyLD_DEBUG=libsmuestran qué encuentra.LD_LIBRARY_PATHes para pruebas y desarrollo. Para distribuir se usa-Wl,-rpath,'$ORIGIN/libs'(la biblioteca viaja con el programa) o se instala en un directorio conocido y se ejecutaldconfig, que actualiza la caché y crea los enlaces del soname.- Actualizar la
.soactualiza todos los programas que la usan, sin reenlazar; es la ventaja principal y el riesgo si la ABI cambia sin subir el mayor. LD_PRELOADcarga antes una biblioteca propia y sustituye símbolos sin recompilar;-Wl,-z,defsevita crear bibliotecas con símbolos sin definir; con.ay.soen el mismo directorio gana la dinámica salvo que se pida-l:libX.ao-Bstatic.
12. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿En qué se diferencia, al ejecutar, un programa enlazado con una biblioteca estática de uno enlazado con una dinámica?
Con la estática, el código de la biblioteca se copió dentro del ejecutable y no hace falta nada más. Con la dinámica, el ejecutable solo guarda el nombre de la biblioteca (NEEDED); al arrancar, el cargador ld.so la busca, la mapea en memoria y resuelve las referencias. Por eso puede fallar al ejecutar si no la encuentra.
2. ¿Qué hace -fPIC y por qué es necesario para una biblioteca dinámica?
Genera código independiente de la posición: sin direcciones absolutas, con saltos relativos y accesos a datos globales a través de la GOT. Una biblioteca dinámica se carga en una dirección distinta en cada proceso y ejecución, y para compartir una sola copia del código el código no puede depender de dónde esté.
3. ¿Cuál es la diferencia entre -fPIC y -shared?
-fPIC es una opción de compilación: cambia el código que genera cada .c. -shared es una opción de enlace: pide producir una biblioteca dinámica en lugar de un ejecutable. Se usan las dos, una en cada paso.
4. El programa enlazó sin problemas pero al ejecutar da cannot open shared object file. ¿Qué pasó y cómo se resuelve?
El enlazador encontró la biblioteca gracias a -L, pero eso no se graba en el ejecutable; el cargador, al ejecutar, busca en su propia lista (LD_LIBRARY_PATH, RUNPATH, la caché de ldconfig y los directorios del sistema) y no la halla. Se resuelve con LD_LIBRARY_PATH (para probar), con -Wl,-rpath,'$ORIGIN/…' o instalándola y ejecutando ldconfig.
5. ¿Qué hacen ldd y readelf -d? ¿En qué se parecen y en qué se diferencian?
readelf -d lee la sección dinámica del archivo y lista las entradas NEEDED, SONAME, RPATH, RUNPATH: lo que el binario pide. ldd además resuelve cada dependencia como lo haría el cargador y muestra la ruta o not found, incluyendo las dependencias de las dependencias. Con binarios no confiables conviene readelf, porque ldd puede ejecutar el programa.
6. ¿Qué es el soname y por qué se usan tres nombres para una biblioteca?
El soname es el nombre interno grabado en la biblioteca (libbasico.so.1), que se copia al ejecutable como su NEEDED. Los tres nombres son: el archivo real con la versión completa (libbasico.so.1.0.0), el soname (enlace al real) que usa el cargador, y el nombre del enlazador libbasico.so (enlace al soname) que usa -lbasico. Así se pueden instalar versiones compatibles reemplazando el archivo real, y versiones incompatibles como un soname nuevo que convive con el viejo.
7. ¿Cuándo debe cambiar el número mayor del soname?
Cuando se rompe la compatibilidad binaria: se elimina o cambia la firma de una función pública, se altera una estructura que los clientes usan directamente, etc. Agregar funciones o corregir errores sin cambiar la interfaz solo modifica el menor o el parche. Ni el compilador ni el cargador comprueban que se cumpla; es responsabilidad del autor.
8. ¿En qué orden busca el cargador las bibliotecas?
RPATH del ejecutable (si no hay RUNPATH), LD_LIBRARY_PATH, RUNPATH, la caché /etc/ld.so.cache y por último los directorios por defecto /lib y /usr/lib. Con LD_DEBUG=libs se ve el recorrido real.
9. ¿Qué ventaja tiene $ORIGIN en el rpath, y por qué se escribe entre comillas simples?
El cargador lo reemplaza por el directorio del ejecutable, de modo que la ruta es relativa al programa y el conjunto (programa y bibliotecas) se puede mover o instalar en cualquier lugar. Las comillas evitan que el shell interprete $ORIGIN como una variable (vacía) antes de que llegue a gcc.
10. ¿Qué diferencia hay entre RPATH y RUNPATH?
Los dos son rutas incrustadas en el ejecutable. RPATH (antiguo) se consulta antes que LD_LIBRARY_PATH, que queda sin efecto sobre esas bibliotecas; RUNPATH (actual, el que genera gcc por defecto) se consulta después, de modo que el usuario puede redirigir con la variable. Con -Wl,--disable-new-dtags se pide RPATH.
11. ¿Para qué sirve ldconfig y qué pasa si se instala una biblioteca sin ejecutarlo?
Recorre los directorios por defecto y los configurados en /etc/ld.so.conf y /etc/ld.so.conf.d/, crea los enlaces del soname y regenera la caché /etc/ld.so.cache, que es lo primero que consulta el cargador. Sin ejecutarlo, la biblioteca nueva puede no estar en la caché y el cargador no la encuentra aunque el archivo exista.
12. Se corrige un error en libbasico.so.1.0.0 y se reemplaza el archivo. ¿Hay que recompilar los programas? ¿Y si el cambio altera la firma de maximo?
Si el cambio es compatible, no: los programas cargan el archivo nuevo en su próxima ejecución, porque su NEEDED es el soname y éste apunta al archivo nuevo. Si la firma cambia, la ABI se rompe: hay que publicar la biblioteca con otro soname (.so.2) y recompilar los programas que quieran usarla; si no, los viejos fallarán o se comportarán mal.
13. ¿Qué hace LD_PRELOAD y para qué se usa?
Hace que el cargador cargue primero las bibliotecas indicadas, de modo que sus símbolos tienen prioridad sobre los de las demás. Permite reemplazar una función (por ejemplo maximo o malloc) en un programa ya compilado, y se usa para depurar, registrar llamadas o simular fallos. Se ignora en programas setuid.
14. ¿Para qué sirve -Wl,-z,defs al construir una biblioteca?
Por defecto una .so puede tener símbolos sin definir y el error recién aparece al enlazar o ejecutar un programa que la use. Con -z defs el enlazador lo rechaza al crear la biblioteca, que es donde se detecta antes.
Para practicar
- Convierte tu
libgeo.adel tema 32 enlibgeo.socon sonamelibgeo.so.1y los tres nombres. Enlaza un programa y comprueba conreadelf -dquéNEEDEDquedó. - Intenta ejecutarlo sin ninguna ayuda para el cargador y anota el mensaje. Resuélvelo de tres maneras: con
LD_LIBRARY_PATH, con-Wl,-rpath,'$ORIGIN/…'y con una caché privada deldconfig -C. - Con
LD_DEBUG=libsen cada uno de los casos anteriores, anota qué rutas prueba el cargador y en qué orden. ¿Cuál se prueba primero conRPATH? ¿Y conRUNPATH? - Mueve el directorio del programa y su biblioteca a otro lugar. ¿Sigue funcionando con
$ORIGIN? ¿Y si usaste una ruta absoluta en elrpath? - Publica una versión «1.1» de
libgeo.so.1con una función más, sustituye el archivo real y comprueba que el programa viejo sigue funcionando sin recompilar. Después cambia la firma de una función sin subir el soname, ejecuta el programa viejo y describe lo que pasa. - Escribe un
mi_malloc.socon una funciónmallocque cuente las llamadas (llamando a la original, que obtienes condlsym(RTLD_NEXT, …), tema 34) y úsalo conLD_PRELOADsobre un programa tuyo. - Crea una biblioteca con una dependencia sin definir y comprueba que se crea sin error; después añade
-Wl,-z,defsy compara. - Corre
lddsobre/bin/bashy/bin/ls(o sobre un binario que tengas) y busca conreadelf -delSONAMEde cada biblioteca de la lista. ¿Cuáles son los sonames delibcy delibm?