Creación de librerías

ARCO

Revision History
Revision 1.223 / 04 / 2003Revised by: David Villa
Revision 1.104 / 10 / 2002Revised by: David Villa
Formateado como docbook.
Revision 1.015 / 03 / 2000Revised by: David Villa
Inicial.

Guia básica para la creación de librerías estáticas y dinámicas en entorno GNU/Linux.


Table of Contents
1. Introducción
2. Librerías estáticas
2.1. Creación de una librería estática
2.2. Uso de una librería estática
3. Librerías dinámicas
3.1. Creación de una librería dinámica
3.2. Uso de una librería dinámica
3.3. Versión de una librería dinámica
3.3.1. Utilidad ldd
3.3.2. Utilidad ldconfig
4. Enlace dinámico en tiempo de ejecución
4.1. Ejemplo de enlace dinámico
5. Precarga de símbolos
Referencias

1. Introducción

Las librerías son una forma sencilla y versatil de modularizar y reutilizar código. En este documento se aborda el proceso de creación de librerías tanto estáticas como dinámicas, sus posibles usos y los recursos mínimos necesarios para gestionarlas de forma adecuada.

Los detalles de implementación y uso de las utilidades del sistema se obvian a propósito para no hacer este documento innecesariamente largo. Para solucionar cualquier duda sobre el uso de alguna función o utilidad mencionada será necesario recurrir a documentos complementarios como puede ser el manual en línea de GNU/Linux, la utilidad info u otro tipo de documentación.


2. Librerías estáticas

Una librería estática no es más que un conjunto de ficheros objeto empaquetados en un único archivo. Para crear una librería estática debemos seguir los siguientes pasos:

Por convenio, los nombres de todas las librerías estáticas comienzan por lib y tienen .a por extensión. Aunque estas restricciones no son obligatorias, es poco recomendable saltárselas pues el entorno ofrece facilidades de uso si se siguen dichas normas.


2.1. Creación de una librería estática

Supongamos que disponemos de los ficheros fich1.c y fich2.c que contienen el código de las funciones que deseamos incluir en la librería libfich.a. Para obtener los ficheros objeto compilamos como siempre:

   $ gcc -c -o fich1.o fich1.c 
	

y

   $ gcc -c -o fich2.o fich2.c 
	

A continuación empaquetamos los ficheros obtenidos con ar (un empaquetador similar a tar).

   $ ar rcs libfich.a fich1.o fich2.o
	

El significado de las opciones que se le dan a ar es el siguiente:

Table 1.

sconstruir un índice del contenido.
ccrear el paquete si no existe.
rreemplazar los ficheros si ya existían en el paquete.

Otras opciones de ar que pueden resultar útiles son:

Table 2.

tlista el contenido de un paquete (o librería).
xextrae un fichero de un paquete (o librería).

2.2. Uso de una librería estática

Para utilizar las funciones y símbolos contenidos en una librería estática es necesario enlazar dichos símbolos cuando se monta un ejecutable. Para ello, debemos indicar al compilador qué librerías queremos utilizar y el lugar donde se encuentran.

Si hemos escrito un programa apli1.c que hace uso de la librería que hemos creado en el punto anterior y queremos compilarlo debemos hacer lo siguiente:

Primero compilamos el programa:

   $ gcc -c -o apli1.o apli1.c
	

teniendo presente que en la compilación deben estar en ruta los ficheros de interfaz (.h) de la librería. De no ser así debe indicarse al compilador donde se encuentran dichos ficheros mediante la opción -I, algo como:

   $ gcc -c -o apli1.o apli1.c -Idir_lib
	

Después se monta el programa con la librería indicando donde está y cual es su nombre:

   $ gcc -o apli1 apli1.o -Ldir_lib -lfich
	

Consideraciones:

  • En el ejemplo se supone que la librería y su fichero de interfaz se encuentran en un directorio llamado dir_lib.

  • La opción -I se usa para indicar donde se encuentran ficheros de interfaz.

  • La opción -L sirve para indicar el directorio donde se encuentra la librería.

  • La opción -l es para indicar los nombres de la librerías que se van a usar, pero no debe escribirse ni el prefijo lib ni la extensión .a ya que el compilador espera que se sigan las normas de nombrado anteriormente citadas.

A continuación se muestra un fichero makefile que permite automatizar todo el proceso, tanto la creación de la librería como del programa. En este caso se supone que todos los ficheros necesarios se encuentran en el directorio actual.

   CC=gcc
   CFLAGS=-Wall -ggdb -I./
   LDFLAGS=-L./
   LDLIBS=-lfich

   all: libfich.a apli1

   apli1: apli1.o

   libfich.a: fich1.o fich2.o
      $(AR) rcs $@ $^
	


3. Librerías dinámicas

La utilización de objetos dinámicos supone dejar pendiente en el montaje de la aplicación el enlace de dichos objetos. Cuando la aplicación está en ejecución, y sólo entonces, se produce el enlace (dinamic binding) con los objetos contenidos en la librería.

La creación de librerías dinámicas corre a cargo del enlazador o montador (en nuestro caso el ld) aunque también es posible indicar al compilador las opciones necesarias para el montaje y de ese modo, será él quien se encargue de pasarselas al montador.


3.1. Creación de una librería dinámica

Cuando se crea un objeto dinámico es necesario que dicho código objeto sea independiente de la posición, para conseguir este tipo de código debe especificarse al compilador la opción -fPIC (Position Independent Code). Dicho flag debe indicarse tanto en la compilación como en el montaje de la librería.

   $ gcc -fPIC -c -o fich1.o fich1.c
   $ gcc -fPIC -c -o fich2.o fich2.c
	

Para montar los objetos es necesario además indicar la opción -shared para que el resultado sea un fichero objeto 'compartible'.

   $ gcc -shared -fPIC -o libfich.so fich1.o fich2.o
	

Para compilar la librería dinámica puede utilizarse un makefile como este:

   CC=gcc
   CFLAGS=-Wall -ggdb -fPIC
   LDFLAGS=-fPIC -shared

   libfich.so: fich1.o fich2.o
      $(CC) $(LDFLAGS) -o $@ $^
	

En este caso, la librería tiene como extensión .so que significa shared object.


3.2. Uso de una librería dinámica

Para utilizar esta librería desde un programa no hay que hacer nada adicional; es exactamente igual que en el caso de la librería estática.

Al hacer uso de una librería, el compilador busca primero una versión dinámica (.so), si no la encuentra entonces busca la versión estática. Si se tienen las dos versiones de una librería y se quiere utilizar la versión estática debe indicarse al montador el flag -static.

Cuando un programa utiliza librerías dinámicas, el asistema necesita localizarlas en tiempo de ejecución (al contrario que con las librerías estáticas). Los lugares donde un programa busca las librerías dinámicas son los siguientes (en este orden):

  • En los directorios de la variable LD_LIBRARY_PATH.

  • En el fichero ld.so.cache.

  • En los directorios /usr/lib y /lib.

  • En los directorios contenidos en el fichero ld.so.conf.

Si el programa no encuentra la librería que necesita imprimirá un mensaje de error con el siguiente aspecto:

   $ ./apli1 
   apli1: error in loading shared libraries: libfich.so: cannot open shared object file: No such file or directory
      

Normalmente, lo más adecuado, es utilizar la variable de entorno LD_LIBRARY_PATH para indicar en qué directorios debe buscar:

   $ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/user/dir_lib
	

De este modo debe funcionar correctamente.


3.3. Versión de una librería dinámica

Una de las grandes ventajas del uso de librerías dinámicas, a parte de tener ficheros ejecutables más pequeños, es que podemos modificar la implementación de las librerías sin tener que recompilar los programas.

Para diferenciar las librerías que han sufrido modificaciones se utilizan los números de versión que tienen el siguiente significado (si se tiene una librería libejemplo.so.x.y.z siendo x, y, z números de versión):

Table 3.

x(major number) permite saber que todas las versiones con el mismo valor para este número son compatibles.
y(release) Cambia cuando se hace alguna modificación que afecta al interfaz de la librería.
z(epoch) Cambia cuando se arregla algún error o cambia alguna característica interna de la librería.

Para que los números de versión no sean sólo el nombre del fichero de la librería sino que las utilidades del sistema puedan gestionar dicha información es necesario indicar al enlazador el nombre y versión de la librería para que lo incluya en el interior del archivo .so.

Para que los números de versión no sean sólo el nombre del fichero de la librería sino que las utilidades del sistema puedan gestionar dicha información es necesario indicar al enlazador el nombre y versión de la librería para que lo incluya en el interior del archivo .so.

Para pasar valores al enlazador desde el compilador se utiliza el flag -Wl escribiendo a continuación las opciones que queremos pasar al enlazador separadas por comas en lugar de por espacios. Veamos cómo crear la librería libfich.so del ejemplo anterior de manera que sea la versión libfich.so.1.0.0.

   $ gcc -Wl,-soname,libfich.so.1.0.0 -shared -fPIC -o libfich.so.1.0.0 fich1.o fich2.o
	

Con un makefile sería:

   CC=gcc
   CFLAGS=-Wall -ggdb -fPIC
   LDFLAGS=-fPIC -shared
   libfich.so.1.0.0: fich1.o fich2.o
      $(CC) -Wl,-soname,$@ $(LDFLAGS) -o $@ $^
	

El problema de utilizar estos nombres es que cuando se compila un programa que utiliza una librería dinámica el compilador busca un fichero con extensión .so simplemente. Para solucionar esto, basta con crear un enlace simbólico con el nombre que el compilador está buscando, es decir:

   $ ln -s libfich.so.1.0.0 libfich.so
	

Si compilamos un programa con una librería para la que se ha especificado un soname con los 3 números, el programa sólo funcionará si encuentra exactamente esa versión. Sin embargo podemos especificar un soname más genérico incluyendo sólo 1 o 2 de los números de versión.

De este modo el programa funcionará sin recompilar con cualquiera de las versiones de la librería con tal que coincida el soname con el que fue compilado. Este sistema permite cambiar a versiones más recientes de las librerías de una forma limpia y rápida. El inconveniente es que necesitamos nuevos enlaces simbólicos pues el programa en ejecución buscará una librería con el soname con el que fue compilado. Veamos un ejemplo:

   $ gcc -Wl,-soname,liba.so.1.0 -shared -fPIC -o liba.so.1.0.0 a.o
	

Hemos creado una librería con nombre liba.so.1.0.0 y soname liba.1.0 a partir del objeto a.o. Para compilar un programa con esta librería debe existir un enlace liba.so que apunte a liba.so.1.0.0 después ya podemos compilar el programa. Sería algo como:

   $ ln -s liba.so.1.0.0 liba.so
   $ gcc -o apli2 apli2.o -L. -la
	

Pero cuando intentamos ejecutar la aplicación obtenemos el siguiente error:

   $ ./apli2
   apli2: error in loading shared libraries: liba.so.1.0: cannot open shared object file: No such file or directory
	

Esto ocurre aunque el directorio esté en el LD_LIBRARY_PATH. Para arreglarlo hay que rear un enlace del siguiente modo:

   $ ln -s liba.so.1.0.0 liba.so.1.0
	


3.3.1. Utilidad ldd

Podemos saber con que versión está compilado un programa por medio del programa ldd; Veamos como funciona:

   $ ldd apli2
   liba.so.1.0 => /home/user/liba.so.1.0 (0x40023000)
   libc.so.6 => /lib/libc.so.6 (0x40025000)
   libdl.so.2 => /lib/libdl.so.2 (0x4010d000)
   /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
	  

Si ahora modificamos la librería y creamos una nueva versión llamada liba.so.1.0.2 pero antenemos el soname liba.so.1.0 la aplicación seguirá funcionando sin más que cambiar el nlace simbólico para que apunte a la nueva librería.


3.3.2. Utilidad ldconfig

Como el asunto de los enlaces puede llegar a ser muy tedioso existe una utilidad llamada dconfig que hace todo esto por nosotros. Como siempre, veamos como automatizar todo con un makefile:

   CC=gcc 
   CFLAGS=-Wall -ggdb -fPIC
   LDFLAGS=-fPIC -shared
   NOMBRE_LIB=liba.so.1.0.0
   SONAME=liba.so.1.0
   $(NOMBRE_LIB): a.o
      $(CC) -Wl,-soname,$(SONAME) $(LDFLAGS) -o $@ $^
      ln -s $(NOMBRE_LIB) liba.so
      ldconfig -vn ./

   clean:
      $(RM) *.o core liba.so*
	  


4. Enlace dinámico en tiempo de ejecución

Cuando un programa se enlaza con una librería dinámica, este obtiene información de como debe manipular los objetos que en ella se encuentran para cuando llegue el momento de la ejecución.

Existe otra posibilidad de utilizar el código que contiene una librería dinámica. Este método permite definir la interfaz de acceso a una librería y en tiempo de ejecución acceder a la librería sin tener ningún conocimiento previo. Esta es la forma en la que habitualmente se implementan los plugins.

El acceso se consigue por medio de las siguientes funciones:

void* dlopen( const char* nomfich, int indicador );

Abre la librería dinámica nomfich (debe ser la ruta absoluta) y devuelve un manejador de la librería. El argumento indicador es el modo en que se cargará la librería.

const char* dlerror( void );

Devuelve una cadena con la descripción del último error ocurrido.

void* dlsym( void* manejador, char* símbolo );

Devuelve la dirección del símbolo cuyo nombre corresponde con el argumento simbolo ubicado en la librería referenciado por manejador, que es un valor devuelto dlopen().

int dlclose( void* manejador );

Descarga la librería dinámica referenciada por medio de manejador.


4.1. Ejemplo de enlace dinámico

Disponemos de una librería dinámica llamada libhola.so compilada a partir del siguiente código:

#include <stdio.h>
void rehola(int i)
{
   printf("Hola mundo por %d vez.\n", i);
}
	  

Y tenemos un fichero con el siguiente código llamado apli3.c:

#include <stdio.h>
#include <dlfcn.h>
#define RUTA_LIBHOLA "/home/user/libhola.so"
int main(){
   int i = 3;
   void *pLibHola;                       /* manejador de la libreria libhola */
   void (*pFuncion)(int);                /* símbolo que importamos de libhola */
   if ((pLibHola = dlopen(RUTA_LIBHOLA, RTLD_LAZY)) ==NULL) {
      fprintf(stderr, dlerror());
      return 1;
   }
   if ((pFuncion = dlsym(pLibHola, "rehola")) == NULL) {
      fprintf(stderr, dlerror());
      return 1;
   }
   if ((pFuncion = dlsym(pLibHola, "rehola")) == NULL) {
      fprintf(stderr, dlerror());
      return 1;
   }
   pFuncion(i);
   dlclose(pLibHola);
   return(0);
}
	

Este código se compila simplemente con:

   $ gcc -o apli3 apli3.c -ldl
	

La opción -ldl es necesaria para poder utilizar la librería libdl.so que es la encargada de implementar los enlaces dinámicos, pero en ningún momento se han hecho referencias a la librería libhola, sólo al lugar donde se encuentra.

Si ejecutamos el programa vemos lo siguiente:

   $ ./apli3
   Hola mundo por 3 vez.
	


5. Precarga de símbolos

Esta es otra posibilidad más para utilizar código externo de forma dinámica. Consiste en cargar una librería dinámica antes de un programa. Dicha librería dinámica contendrá símbolos referenciados por el programa.

Supongamos que un programa apli4 hace uso del símbolo FuncionG que se encuentra en la librería dinámica libDinamica.soc. Pues bien, podemos escribir una librería diferente libOtra.so que implemente la FuncionG a condición de que cumpla su prototipo, pero cuya funcionalidad puede ser completamente distinta. Después, con este método, se puede hacer que el programa utilice el símbolo que hay en nuestra librería en lugar del que hay en la librería libDinamica.so.

Hay dos formas para conseguir dicho comportamiento:

En el documento Depuración Avanzada en GNU/Linux, de esta misma serie de documentos, aparecen varios ejemplos del uso de este método.


Referencias

[1] Escuela Superior de Informática, Aplicaciones de desarrollo en GNU/Linux, 2001.

[2] http://www.dwheeler.com/program-library/.