Curso de Sistemas Operativos Creación de Librerías CURSO DE SISTEMAS OPERATIVOS CREACIÓN DE LIBRERÍAS EN GNU/LINUX www.ciberia.ya.com/eingenieria/ssoo David Villa Alises <[email protected]> (VERSIÓN DEL DOCUMENTO: 1.1) 1.− INTRODUCCIÓN Las librerías son una forma sencilla y potente 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: • • • Codificar los módulos de la forma habitual. Obtener los ficheros objeto utilizando el compilador. Empaquetar los ficheros objeto que integran la librería por medio de la utilidad ar. Por convenio, los nombres de todas las librerías estáticas comienzan por lib− y tiene .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: s construir un índice del contenido. c crear el paquete si no existe. r reemplazar los ficheros si ya existían en el paquete. Otras opciones de ar que pueden resultar útiles t lista el contenido de un paquete (o librería). x extrae un fichero de un paquete (o librería). Página 1 de 7 Curso de Sistemas Operativos Creación de Librerías 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; si no es así debe indicarse al compilador donde se encuentran dichos ficheros haciendo uso de la opción −I: $ gcc −c −o apli1.o apli1.c −Idir_lib Después se monta el programa con la librería indicando donde está la librería 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 el sufijo .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 directoria 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 comunicárselas al montador. Página 2 de 7 Curso de Sistemas Operativos Creación de Librerías 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. Para utilizar esta librería desde un programa no hay que hacer nada adicional; la forma de hacerlo 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 una 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. 3.2.− Uso de una librería dinámica Cuando un programa utiliza librerías dinámicas 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 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. Página 3 de 7 Curso de Sistemas Operativos Creación de Librerías 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): x (mayor 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 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 Página 4 de 7 Curso de Sistemas Operativos Creación de Librerías 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 crear 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 mantenemos el soname liba.so.1.0 la aplicación seguirá funcionando sin más que cambiar el enlace 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 ldconfig 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 en que se ejecute. 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 ella sin tener ningún conocimiento previo. Esta es la forma en la que habitualmente se implementan los plugin. El acceso se hace 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. Página 5 de 7 Curso de Sistemas Operativos Creación de Librerías 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 y que se llama apli3.c: #include <stdio.h> #include <dlfcn.h> #define RUTA_LIBHOLA "/home/user/libhola.so" int main(){ int i = 3; void *pLibHola; void (*pFuncion)(int); /* manejador de la libreria libhola */ /* 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; } 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. Página 6 de 7 Curso de Sistemas Operativos Creación de Librerías 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.so. 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: • Utilizar la variable de entorno LD_PRELOAD de la siguiente manera: LD_PRELOAD=/home/user/libOtra.so apli4 • Por medio del fichero /etc/ld.so.preload que contendrá los nombres de las librerías cuyos símbolos se les dará prioridad en la carga. En el documento Depuración Avanzada en GNU/Linux, de esta misma serie de documentos, aparecen varios ejemplos del uso de este método. 6.− REFERENCIAS • Curso de Enseñanzas Propias Aplicaciones de desarrollo en GNU/Linux. Escuela Superior de Informática, Ciudad Real, 2001. • Manual en línea de GNU/Linux. Este documento es de dominio público. Se puede imprimir y distribuir libre de gastos en su forma original, incluyendo una lista de los autores. Si se altera o se utilizan partes de éste dentro de otro documento, la lista de autores debe incluir todos los autores originales y el autor o los autores que hayan realizado los cambios. Así mismo el documento modificado debe incluir este texto. En caso de que este documento sea utilizado con fines comerciales, se aplicarán los términos de la GNU General Public License. Página 7 de 7