Las librerías compartidas compartidas son cargadas por los programas al iniciar. Es común que muchos programas utilicen una misma librería (por ejemplo la libc, librería estándar del lenguaje C). Entonces, en vez de que cada programa enlace estáticamente su propia copia de la libc, ¿por qué no mejor mantener una única copia de la libc y que todos los programas que la necesiten la enlacen dinámicamente? Este es el concepto detrás de las librerías compartidas, el cual trae muchos beneficios (por ello es usado por todos los sistemas operativos), pero como todo, a veces puede fallar.

Este artículo explica someramente cómo funcionan las librerías compartidas en los sistemas operativos basados en Linux y los principales problemas que se pueden encontrar, junto con las herramientas de diagnóstico a utilizar.



Una librería es un archivo que representa un módulo que implementa un conjunto de funciones comunes. Por ejemplo en el header "stdio" (standard input/output) parte de la librería estándar del lenguaje C (libc) se proveen funciones comunes para el manejo de entrada salida como printf, scanf, fopen, fread, fwrite y más. Las librerías permiten que ciertas funciones de uso común por muchos programas puedan ser reutilizadas en lugar de reimplementadas en cada desarrollo.

Al momento de compilar un programa, éste puede hacer uso de cualquier librería disponible, donde cada una de ellas puede ser enlazada de forma estática o dinámica por el linker (ld). Cuando una librería se enlaza de manera estática, esta pasa a formar parte del binario ejecutable. Ahora bien, es común que diferentes programas hagan uso de una misma librería, con lo cual hacer que cada uno de ellos mantenga su propia copia de una misma librería es ineficiente. De modo simplificado, es preferible que todos ellos compartan una misma copia de la librería. Es por ello que se permite el enlazado dinámico. De esta forma, en lugar del ejecutable contener su propia copia de una librería, se enlaza a una librería compartida. Al momento de ejecutar el programa, el loader (programa que se encarga de cargar programas en memoria) se da cuenta que éste hace uso de librerías compartidas y pasa el control al dynamic linker/loader (ld.so) para resolver cada una de ellas.

Cuando una librería compartida es instalada correctamente, todos los programas que inician a partir de ese momento automáticamente utilizarán la nueva librería compartida (o versión). Sin embargo en la práctica es mucho más flexible y sofisticado que esto, ya que la estrategia adoptada por Linux permite actualizar una librería y aún soportar programas que requieran una versión anterior, no compatible hacia atrás, de la misma; sobrescribir una librería o incluso sólo una función específica de una librería cuando se ejecuta un programa en particular; y hacer todo esto mientras los programas están en ejecución utilizando las librerías existentes.

La herramienta ldd permite listar todas las dependencias de librerías compartidas que tiene un ejecutable, y a su vez comprobar si es posible enlazarlas correctamente:

root@debian:~# which ls
/bin/ls
root@debian:~# ldd /bin/ls
        linux-vdso.so.1 =>  (0x00007ffda74b7000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f531020d000)
        librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f5310005000)
        libacl.so.1 => /lib/x86_64-linux-gnu/libacl.so.1 (0x00007f530fdfb000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f530fa6e000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f530f86a000)
        /lib64/ld-linux-x86-64.so.2 (0x000055e4852ff000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f530f64d000)
        libattr.so.1 => /lib/x86_64-linux-gnu/libattr.so.1 (0x00007f530f448000)

Esta herramienta resulta de gran utilidad a la hora de resolver problemas con librerías faltantes.

Cada librería tiene un nombre real y un nombre especial llamado "soname". Ambos nombres tienen el prefijo "lib" (salvo las librerías de bajo nivel de C) y el nombre de la librería. Por ejemplo, para la librería ELF, se componen como "libelf".

[root@centos ~]# ll /usr/lib64/ | grep libelf
-rwxr-xr-x   1 root root   88456 may 10  2016 libelf-0.164.so
lrwxrwxrwx   1 root root      15 may 30  2016 libelf.so.1 -> libelf-0.164.so

El nombre especial "soname" utiliza la extensión ".so", seguida de un punto y un número de versión que se incrementa cada vez que la librería es actualizada. En un sistema funcionando el nombre soname es un enlace simbólico al nombre real correspondiente. El nombre real corresponde al nombre del archivo que contiene el código de la librería en sí. Este nombre incluye el versionado y release de la librería. Finalmente está el nombre que el compilador utiliza para enlazar una librería (podría llamarse "linker name"), que es simplemente el soname sin ningún número de versionado.

La separación entre estos nombres es la clave para manejar librerías compartidas. Los programas, cuando listan internamente las librerías que necesitan, deben utilizar el soname. Simultáneamente, cuando se crea una librería, debe hacerse con un nombre específico e información de versionado detallada. Esta se instala en alguno de los directorios especiales para librerías compartidas del sistema y se invoca al programa ldconfig. Este programa examina los archivos existentes y crea los sonames como enlaces simbólicos a los nombres reales, al mismo tiempo en que configura el archivo de caché /etc/ld.so.cache.

El nombre que usa el linker no lo configura ldconfig sino que se hace típicamente durante la instalación de la librería, como un link sinmbólico al soname.

Las librerías compartidas deben almacenarse en directorios conocidos. Generalmente las distribuciones siguen el estándar FHS. De acuerdo a este estándar, la mayoría de las librerías compartidas deben instalarse en /usr/lib. Sin embargo, las librerías necesarias para el inicio del sistema (startup) deben instalarse en /lib, y las librerías que no son parte del sistema en /usr/local/lib.

Al iniciar un ejecutable ELF en Linux, se lanza inmediatamente el loader, y este encuentra y carga todas las librerías que necesita el programa. La lista de directorios a examinar se almacena en el archivo /etc/ld.so.conf:

root@debian:~# cat /etc/ld.so.conf
include /etc/ld.so.conf.d/*.conf

root@debian:~# ll /etc/ld.so.conf.d/
total 8
-rw-r--r-- 1 root root 44 Jul  8  2014 libc.conf
-rw-r--r-- 1 root root 68 Jul  8  2014 x86_64-linux-gnu.conf
root@debian:~# cat /etc/ld.so.conf.d/*
# libc default configuration
/usr/local/lib
# Multiarch support
/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu

Realizar una búsqueda en todos los directorios existentes en este archivo (o éstos, como se observa en el ejemplo anterior) todas y cada una de las veces que se lanza un programa sería terriblemente ineficiente. Con lo cual un mecanismo de caching es utilizado. El programa ldconfig se encarga de este proceso, el cual acelera el acceso a las librerías.

root@debian:~# file /etc/ld.so.cache 
/etc/ld.so.cache: data

El único requisito es que ldconfig debe ser ejecutado cada vez que se agrega, elimina o actualiza cualquier librería. Típicamente el gestor de paquetes se encarga de esta tarea.

La caché del linker es una especie de lista que contiene las rutas absolutas de absolutamente todas las librerías que pueden ser utilizadas en el sistema:

root@debian:~# cat /etc/ld.so.cache | tr -dc '[:print:]' | tail -c 500; echo
so.1.5libanl.so.1/lib/x86_64-linux-gnu/libanl.so.1libanl.so/usr/lib/x86_64-linux-gnu/libanl.solibaio.so.1/lib/x86_64-linux-gnu/libaio.so.1libacl.so.1/lib/x86_64-linux-gnu/libacl.so.1libSegFault.so/lib/x86_64-linux-gnu/libSegFault.solibGeoIPUpdate.so.0/usr/lib/libGeoIPUpdate.so.0libGeoIP.so.1/usr/lib/libGeoIP.so.1libBrokenLocale.so.1/lib/x86_64-linux-gnu/libBrokenLocale.so.1libBrokenLocale.so/usr/lib/x86_64-linux-gnu/libBrokenLocale.sold-linux-x86-64.so.2/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

Existen algunas variables de entorno que permiten omitir este proceso. En Linux, la variable de entorno LD_LIBRARY_PATH contiene un conjunto de directorios (separados con punto y coma) donde se deben buscar librerías antes que el conjunto estándar. Esto resulta útil para debugguear una nueva librería o errores con una aplicación. Cabe destacar que esta variable no funciona en todos los sistemas Unix y no debe ser modificada como parte de un proceso de instalación, ya que está pensada para desarrollo y testeo solamente.

Otra variable de gran utilidad es LD_DEBUG. Esta permite obtener información detallada sobre el proceso de linkeo. Al invocar cualquier programa seteando esta variable en help se listan los posibles valores que puede adoptar y se aborta la ejecución:

root@debian:~# LD_DEBUG=help ls
Valid options for the LD_DEBUG environment variable are:

  libs        display library search paths
  reloc       display relocation processing
  files       display progress for input file
  symbols     display symbol table processing
  bindings    display information about symbol binding
  versions    display version dependencies
  all         all previous options combined
  statistics  display relocation statistics
  unused      determined unused DSOs
  help        display this help message and exit

To direct the debugging output into a file instead of standard output
a filename can be specified using the LD_DEBUG_OUTPUT environment variable.

A través de esta variable es posible identificar versiones de librerías utilizadas por un programa, rutas de búsqueda, bindings y procesamiento de símbolos (es posible determinar cómo se realiza la búsqueda de funciones dentro de las librerías enlazadas).

Ahora bien, tal como se ha visto hasta el momento, la forma correcta de agregar una nueva librería en el sistema consiste en instalarla en uno de los directorios de búsqueda de ld.so y ejecutar ldconfig. También es posible agregar un nuevo directorio de búsqueda, por ejemplo si hemos compilado e instalado manualmente cierta pieza de software y deseamos que sus librerías puedan ser utilizadas por otros programas. A tal fin se debe agregar la ruta al directorio en el archivo /etc/ld.so.conf, o mejor crear un nuevo archivo con extensión .conf dentro del directorio /etc/ld.so.conf.d/ (tal como se ha visto en la configuración anterior).

Hay otra forma, un tanto más rudimentaria, para permitir el uso de una librería (o directorio de librerías) que consiste en agregar el directorio en cuestión a la variable de entorno PATH. Esta técnica sin embargo permite acotar el uso de estas librerías a lo largo de todo el sistema. Por ejemplo permite que un único usuario pueda disponer de las mismas, pero el resto de los usuarios del sistema no, utilizando un PATH personalizado en su profile de Bash.

Por ejemplo, en cierto sistema CentOS se ha instalado un cliente de Informix y se ha personalizado la variable de entorno PATH para el usuario "postgres":

[postgres@centos ~]$ echo $PATH
/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/usr/local/informix/bin:/usr/local/informix/lib:/usr/local/informix/lib/esql:/usr/local/bin:/usr/local/pgsql/bin:/usr/local/pgsql/lib:/home/postgres/bin

Como se observa, se han agregado a la variable PATH las rutas /usr/local/informix/lib y /usr/local/informix/lib/esql, las cuales corresponden con directorios que contienen librerías de Informix, a fin de que puedan ser utilizadas por otros programas.

El comando which permite encontrar archivos en rutas dentro del PATH. Esto permite buscar rápidamente una librería y verificar si el enlazado dinámico se completa correctamente con ldd:

[root@centos ~]# export PATH="$PATH:/usr/local/informix/lib:/usr/local/informix/lib/esql"
[root@centos ~]# which ifx_fdw.so
/usr/local/pgsql/lib/ifx_fdw.so
[root@centos ~]# ldd $(which ifx_fdw.so)
        linux-vdso.so.1 =>  (0x00007fff3beb2000)
        libifsql.so => /usr/local/informix/lib/esql/libifsql.so (0x00007f9e29008000)
        libifasf.so => /usr/local/informix/lib/libifasf.so (0x00007f9e28dc0000)
        libifgen.so => /usr/local/informix/lib/esql/libifgen.so (0x00007f9e28b5d000)
        libifos.so => /usr/local/informix/lib/esql/libifos.so (0x00007f9e2893c000)
        libifgls.so => /usr/local/informix/lib/esql/libifgls.so (0x00007f9e286ea000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f9e284cd000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f9e28249000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007f9e28045000)
        libcrypt.so.1 => /lib64/libcrypt.so.1 (0x00007f9e27e0e000)
        libifglx.so => /usr/local/informix/lib/esql/libifglx.so (0x00007f9e27c0d000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f9e27879000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f9e29478000)
        libfreebl3.so => /lib64/libfreebl3.so (0x00007f9e27676000)

Esta librería en particular estaba arrojando un error al intentar utilizarla como usuario "postgres". Aunque se observa que como "root" todo aparentemente es correcto.

Al pasar a "postgres" sin embargo, se observa que no se encuentra la librería libifsql.so, a pesar de estar instalada correctamente en una de las rutas agregadas al PATH (/usr/local/informix/lib/esql/libifsql.so):

[postgres@centos ~]$ ldd /usr/local/pgsql/lib/ifx_fdw.so
        linux-vdso.so.1 =>  (0x00007ffefafd3000)
        libifsql.so => not found
        libifasf.so => /usr/local/informix/lib/libifasf.so (0x00007fa36a38a000)
        libifgen.so => /usr/local/informix/lib/esql/libifgen.so (0x00007fa36a127000)
        libifos.so => /usr/local/informix/lib/esql/libifos.so (0x00007fa369f06000)
        libifgls.so => /usr/local/informix/lib/esql/libifgls.so (0x00007fa369cb4000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fa369a97000)
        libm.so.6 => /lib64/libm.so.6 (0x00007fa369813000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007fa36960f000)
        libcrypt.so.1 => /lib64/libcrypt.so.1 (0x00007fa3693d8000)
        libifglx.so => /usr/local/informix/lib/esql/libifglx.so (0x00007fa3691d7000)
        libc.so.6 => /lib64/libc.so.6 (0x00007fa368e43000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fa36a7ef000)
        libfreebl3.so => /lib64/libfreebl3.so (0x00007fa368c40000)
[postgres@centos ~]$ which libifsql.so
/usr/bin/which: no libifsql.so in (/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/usr/local/informix/bin:/usr/local/informix/comandos:/usr/local/informix/lib/esql:/usr/local/bin:/usr/local/pgsql/bin:/usr/local/pgsql/lib:/home/postgres/bin)

En estos casos es donde juegan los permisos. Más allá de haber instalado correctamente una librería, o haber agregado su ruta de instalación al PATH, es necesario que el usuario en cuestión tenga permiso de lectura sobre la misma. En este caso en particular el usuario "postgres" no tiene permiso de acceso al directorio:

[postgres@centos ~]$ ll /usr/local/informix/lib/esql
ls: cannot access /usr/local/informix/lib/esql: Permission denied

Esta es la razón por la cual a veces falla el linkeo dinámico de librerías compartidas en algunos programas. Suele ocurrir que ldd marca como "not found" una librería, cuando en realidad no es que no se ha encontrado, sino que no se tiene permiso de acceso a la misma.

Más allá de que las rutas sean correctas, se debe contar con los permisos de acceso necesarios sobre cada una de ellas.

Referencias


Tal vez pueda interesarte


Compartí este artículo