Bibliothèque .so sous Linux

Une bibliothèque peut être vue comme une collection de fonctions autonomes pouvant être partagées entre plusieurs programmes. L'intérêt de ce principe de séparation entre le programme principal et les fonctions est de permettre de faire l'évolution des fonctions sans forcer à la recompilation de l'ensemble et d'offrir une plus grande modularité dans la conception de l'application.

Création d'une bibliothèque partagée

Considérons un fichier my_lib.c qui contiendra une fonction retournant le carré d'un nombre passé en paramètres.


int carre(int a)
{
  return(a*a);
}
	  
Nous allons ajouter un fichier d'entête my_lib.hqui va contenir les différentes fonctions que nous allons déclarer utilisables par les programmes externes.

#ifndef __MY_LIB__H__
#define __MY_LIB__H__
#include <stdio.h>
     
extern double carre(double);
      
#endif
	  
Pour transformer ce bout de programme en bibliothèque, il suffit de deux étapes de compilation.

Etape 1 : compilation avec du code indépendant en mémoire

Puisque notre bibliothèque doit être partagée par plusieurs programmes en mémoire, il n'est pas possible de créer une bibliothèque qui va charger ses données à des endroits précisément définis. C'est le chargeur de programme qui doit décider de l'emplacement de l'allocation mémoire de ces données propres à la bibliothèque. C'est la but du commutateur -fpic. Cela aurait pu également être -fPIC selon votre envie d'obtenir plus de code de debug.
gcc -c -Wall -Werror -fpic my_lib.c

Etape 2 : création de la bibliothèque depuis un fichier objet

Il nous faut transformer le programme objet obtenu à l'étape précédente en fichier de bibliothèque partagée.
gcc -shared -o libmy_lib.so my_lib.o
Si l'on veut produire du versioning sur les bibliothèques, il faut rajouter une option sur la ligne de compilation du genre
gcc -shared -Wl,-soname,libmy_lib.so.1 -o libmyl_lib.so.1.0 my_lib.o
L'option -Wl,-soname,libmy_lib.so.1 est passée au linker pour produire un fichier de bibliothèque de numéro de version égal à 1 pour libmy_lib.so.1. L'option -o est quant à elle passée à gcc pour produire un fichier nommé libmy_lib.so.1.0 correspondant à la version 1.0 de votre bibliothèque. Vous pouvez nommer les versions comme vous le voulez, en respectant tout de même le fait que c'est normalement version.revision[.micro] et que :

  • une version est normalement garante d'un ensemble de nouvelles fonctionnalités ou améliorations majeures, pouvant bloquer la compatibilité avec les anciennes versions des bibliothèques.
  • une revision est plutôt une correction de bugs dans la version principale et doit assurer la compatibilité avec toutes les évolutions dans une même version.
  • une micro est pour raffiner encore plus le principe de gestion des évolutions de la bibliothèque.

Utilisation d'une bibliothèque partagée

Compilation du programme avec les informations de la bibliothèque

Généralement lorsque l'on veut utiliser une bibliothèque simplement, il va nous falloir effectuer l'étape d'édition des liens avec une référence à la bibliothèque en question. Commençons par écrire un fichier test.c qui va nous permettre de tester cette bibliothèque.


#include <stdio.h>
#include "my_lib.h"


int main(int argc, char **argv)
{
  printf("Programme de test de la bibliotheque partagee\n");
  printf("Carre de 8 : %lf\n", carre(8.5));
  
  return(0);
}
	  
Comme avec toutes les bibliothèques partagées, vous devez inclure le fichier d'entête qui permettra au compilateur de retrouver les références aux différentes fonctions de la bibliothèque que vous voulez utiliser.
gcc -Wall -o test test.c -lmy_lib -L<repertoire_où_trouver_my_lib.so>
Lors de la phase de compilation, il suffit d'utiliser le nom de la bibliothèque sans le lib en début et le .so en fin de nom. l'option -L va préciser à l'éditeur de liens où trouver le fichier de bibliothèque en question. Pour nos besoins en développement, ce dossier sera le dossier local où se trouve le fichier my_lib.so que nous avons créé en début de document. Par défaut gcc va chercher dans le dossier /usr/local/lib puis dans /usr/lib. Préciser un autre chemin par l'option -L va permettre de modifier ce comportement.

Rendre disponible la bibliothèque lors de l'exécution du programme

Lors de l'exécution du programme :
./test
vous devriez obtenir ceci : ./test : error while loading shared libraries: libmy_lib.so: cannot open shared object file: no such file or directory Par défaut, le chargeur dynamique ld.so vérifie l'accès aux bibliothèques selon le schéma :

  1. la variable d'environnement $LD_LIBRARY_PATH, qui contient les chemins des différentes bibliothèques partagées. C'est plutôt à conseiller dans le cadre d'un environnement de développement car modifier le contenu de cette variable pourrait bloquer le fonctionnement d'autres programmes qui y faisaient référence.
  2. lecture du fichier /etc/ld/so/cache
  3. répertoires par défaut /lib, /usr/local/lib, /usr/lib
Si aucun point n'est satisfait, le programme se plante.

Utilisation de LD_LIBRARY_PATH

Par défaut, il ne devrait rien y avoir de défini dans cette variable d'environnement. Pour vous en assurer faites
echo $LD_LIBRARY_PATH
Ajoutez votre chemin dans cette variable d'environnement par
export LD_LIBRARY_PATH=/home/username/%dossier_de_la_lib%:$LD_LIBRARY_PATH
Vous pouvez maintenant utiliser votre programme avec la bibliothèque.
Si vous rencontrez un message du genre ./test: error while loading shared libraries: libmy_lib.so.1: cannot open shared object file: no such file or directory c'est que vous n'avez pas le fichier de bibliothèque pointant sur la bonne version du fichier. Pour déterminer ce dont vous avez besoin comme version de fichier, il vous suffit de faire
ldd ./test
Cela affiche la liste des bibliothèques partagées utilisées par le programme. A vous ensuite de réaliser le lien vers la bonne version par une commande du genre
ln -s libmy_lib.so.1.0 libmy_lib.so.1

Utilisation du fichier du loader dynamique

Si aucune solution précédente n'a fonctionné, vous pouvez toujours référencer votre bibliothèque dans le fichier de configuration du chargeur dynamique de bibliothèques. C'est une opération qui va rendre ce fichier disponible pour l'ensemble du système. Il faut cependant disposer des droits root pour réaliser ces manipulations car vous allez devoir écrire dans des endroits qui ne sont pas standards pour l'utilisateur et ensuite modifier des variables du système.

Si vous désirez utiliser cette méthode, il faut enlever les éventuelles modifications que vous avez pu inscrire dans la variable d'environnement $LD_LIBRARY_PATH en faisant soit unset LD_LIBRARY_PATH si rien n'était noté avant vos modifications (pour ne pas mettre en péril un autre programme), soit en éditant la variable d'environnement pour retirer vos informations.

Avec les nouvelles versions du chargeur ld.so, le fichier /etc/ld.so.conf ne devrait contenir qu'une ligne spécifiant un dossier dans lequel se trouvent toutes les références aux bibliothèques.
include /etc/ld.so.conf.d/*.conf Ce dossier contient simplement des fichiers précisant les chemins où les différentes bibliothèques partagées se situent. Dans ce cas, crééz simplement un fichier du genre sudo nano libmy_lib.conf dans le dossier /etc/ld.so.conf.d/ et dans le fichier spécifiez le chemin dans lequel se trouve votre bibliothèque.

Exécutez ensuite sudo ldconfig pour inscrire votre chemin de bibliothèque pour les programmes qui pourraient y faire appel.

Determiner quelles bibliothèques sont utilisées par un programme

La commande ldd permet de déterminer les bibliothèques partagées requises par un programme. Ainsi, pour GCC, nous obtenons

	    # ldd /usr/bin/gcc
	    linux-vdso.so.1 (0x00007ffdfc7ac000)
	    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fa35dff2000)
	    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa35dc47000)
	    /lib64/ld-linux-x86-64.so.2 (0x00007fa35e2f3000)
	  
Une référence manquante est ainsi facilement détectable car elle serait simplement marquée not found.

Afficher les symboles d'un programme

La commande nm permet d'afficher les symboles contenus dans les fichiers et objets.

	    # nm test

	    0000000000600bf8 B __bss_start
                             U carre
	    0000000000600bf8 D _edata
	    0000000000600c00 B _end
	    0000000000400814 T _fini
                             U foo
                             w __gmon_start__
	    00000000004005b8 T _init
                             w _ITM_deregisterTMCloneTable
                             w _ITM_registerTMCloneTable
                             w _Jv_RegisterClasses
                             U __libc_start_main
                             U printf
                             U puts
	  
Les types de symboles sont les suivants
Type de symbole Description
A La valeur du symbole est absolue et ne sera pas modifiée par une édition de liens ultérieure
B Section DATA non-encore initialisée
D Section DATA initialisée
T Section de code normale
U Symbole utilisé mais non-encore initialisé. Dépend d'une bibliothèque tierce
W Symbole doublement défini. La définition pourra être faite dans une autre bibliothèque pour résoudre les dépendances.

Lister les symboles d'une bibliothèque

La commande readelf permet de lister les symboles contenus dans un objet, notamment d'une bibliothèque partagée.

# readelf -s /usr/bin/gcc

	    Table de symboles « .dynsym » contient 145 entrées :
Num:    Valeur         Tail Type    Lien   Vis      Ndx Nom
 0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
 1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __uflow@GLIBC_2.2.5 (2)
 2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND mkstemps@GLIBC_2.11 (3)
 3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND getenv@GLIBC_2.2.5 (2)
 4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dl_iterate_phdr@GLIBC_2.2.5 (2)
 5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __pthread_key_create
 6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND putchar@GLIBC_2.2.5 (2)
 7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strcasecmp@GLIBC_2.2.5 (2)
 8: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND localtime@GLIBC_2.2.5 (2)
 9: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND strverscmp@GLIBC_2.2.5 (2)
10: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND abort@GLIBC_2.2.5 (2)
11: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __errno_location@GLIBC_2.2.5 (2)
12: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND unlink@GLIBC_2.2.5 (2)
	  
Consultez la page de manuel sur readelf pour plus d'informations.

Utilisation d'une bibliothèque en tant que plugin (ou greffon) d'un programme

L'autre façon d'utiliser une bibliothèque partagée est de la manipuler sous la forme d'un greffon (un plugin en bon jargon). Nous allons avoir une bibliothèque partagée qui sera utilisée plutôt depuis un seul programme pour lui apporter des fonctionnalités à la demande, plutôt qu'une bibliothèque partagée entre plusieurs programmes qui donnera accès à des fonctions utilisables par tous les programmes.

Prenons un nouvel exemple dans lequel un plugin contient deux fonctions mathématiques avancées à savoir Addition et Multiplication (niveau Math Sup svp). Commenons par le fichier Include plugin.h

	    #ifndef __PLUGIN__H__
	    #define __PLUGIN__H__

	    int Addition(int, int);
	    int Multiplication(int, int);
	    
	    #endif
	  
Poursuivons par le fichier de description de code plugin.c
	    #include <stdio.h>

	    int Addition(int a, int b)
	    {
	    return (a+b);
	    }

	    int Multiplication(int a, int b)
	    {
	    return (a * b);
	    }
	  
Si nous compilons et produisons l'édition des liens de cette bibliothèque par
	    # gcc -Wall -Werror -fPIC -c plugin.c
	    # gcc -shared -o plugin.so plugin.o
	  
Nous obtenons un fichier ressemblant beaucoup à une bibliothèque partagée telle que définie dans les paragraphes précédents. Il y a un élément auquel il faut cependant prendre garde. Lorsque ces bibliothèques ont été créées, c'était pour les utiliser avec le Langage C. Si vous les utilisez avec du C++ par exemple, le compilateur C++ va avoir la fâcheuse tendance à modifier les noms des objets de bibliothèque pour les mettre à sa sauce, ce qui va casser les dépendances et rendre vos bibliothèques inutilisables. Pour résoudre ce problème, forcer la description "C" pour la génération des bibliothèques, même si vous utilisez un compilateur C++. Il faut pour cela modifier le fichier include comme ceci pour préciser au compilateur de gérer les fonctions externes en pur "C"
	    #ifndef __PLUGIN__H__
	    #define __PLUGIN__H__

	    #ifdef __cplusplus
	    extern "C" {
	    #endif
	    
	    int Addition(int, int);
	    int Multiplication(int, int);

	    #ifdef __cplusplus
	    }
	    #endif
	    
	    #endif
	  

Une fois que le plugin est prêt à l'usage, il ne reste qu'à écrire le programme principal.

Références

http://docs.python.org/library/ctypes.html : partie de la documentation Python relatant l’usage des conventions d’appels pour permettre l’utilisation de DLLs (Windows) et de fichiers .SO (Unix) avec Python.

© LGB - 201x+