Lors d’une récente session de formation, une conversation avec un participant m’a poussé à vérifier les options nécessaires pour effectuer du débogage et des tests en couverture sur une bibliothèque partagée.
Une bibliothèque dynamique (fichier libXXXX.so
– so
pour Shared Object) est chargée dans la mémoire du processus au moment de son démarrage. Le fichier exécutable et la bibliothèque sont indépendants avant le lancement de l’application, et peuvent être maintenus séparément.
J’ai réalisé que certains points étaient loin d’être évidents, par exemple la gestion des numéros de version ou l’activation des tests en couverture. Voici un petit récapitulatif des étapes de mise au point d’une bibliothèque dynamique. Le premier article est consacré à la compilation, la gestion des versions et la création des liens symboliques nécessaires. Le second s’intéressera au débogage et au suivi pas-à-pas du code de la bibliothèque depuis une application. Le troisième décrira comment effectuer des tests en couverture sur le contenu de la bibliothèque.
Compilation et installation de la bibliothèque
Compilation du code de la bibliothèque
Commençons par créer une petite bibliothèque dynamique, avec une fonction relativement simple : l’implémentation de la fonction mathématique « factorielle ».
Je crée un répertoire de travail factorielle
regroupant tous les fichiers concernant cette bibliothèque. Nous y créons trois sous-répertoires :
src/
qui contiendra le code source de la bibliothèque,lib/
où seront regoupés les fichiers binaires et les liens symboliques décrits plus bas,include/
dans lequel les fichiers d’en-tête de la bibliothèque seront stockés.
[~]$ mkdir factorielle [~]$ mkdir factorielle/src [~]$ mkdir factorielle/include [~]$ mkdir factorielle/lib [~]$ cd factorielle [factorielle]$
Créons un fichier src/fact.c
implémentant notre fonction.
Et si vous pensez avoir trouvé un bug dans le code ci-dessous, ayez la gentillesse de lire l’article en entier avant de m’envoyer un mail de moquerie 😉
#include <fact.h> long long int factorielle(long int n) { long long int f = 1; do { f = f * n; n = n - 1; } while (n > 1); return f; }
Ce fichier commence par inclure son propre fichier d’en-tête, ce qui permet de s’assurer à la compilation de la concordance du prototype et de l’implémentation.
Le fichier include/fact.h
contient les lignes suivantes.
#ifndef LIB_FACT_H #define LIB_FACT_H long long int factorielle(long int n); #endif
Lors de la compilation de ce fichier nous fournirons sur la ligne de commande de gcc
les options:
-c
pour arrêtergcc
après la phase de compilation et obtenir ainsi un fichier objet (pas d’édition des liens).-I include/
qui indique àgcc
de rechercher les fichiers d’en-tête.h
dans le répertoireinclude/
en supplément des répertoires usuels (/usr/include
…).-fPIC
pour demander la génération d’un code relogeable (Position Independant Code) comme c’est nécessaire pour la création de bibliothèques partagées même si cette option n’est pas indispensable sur certaines architectures (x86 32 bits par exemple).
Voici un exemple de compilation.
[factorielle]$ ls src/ fact.c [factorielle]$ ls include/ fact.h [factorielle]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact.c [factorielle]$ ls src/ fact.c fact.o [factorielle]$
Cette compilation a généré un fichier objet fact.o
que nous utiliserons ci-après.
On notera que durant la phase de test et de mise au point, il ne faut utiliser aucune option d’optimisation, sinon le compilateur risque de modifier le code exécutable créé (en regroupant des blocs de code par exemple) et il n’y aura plus de correspondance exacte avec le fichier source.
Génération de la bibliothèque
La bibliothèque proprement dite est obtenue en invoquant gcc
avec l’option -shared
. Nous allons lui demander d’enregistrer la bibliothèque dans le fichier libfact.so.1.0
. Les numéros 1
et 0
correspondent respectivement aux numéros majeur et mineur de version de la bibliothèque.
Il est d’usage de considérer qu’un changement de numéro majeur représente une rupture de la compatibilité binaire de la bibliothèque et nécessite une recompilation des applications, alors qu’une variation du numéro mineur signifie des corrections ou des améliorations internes n’influant pas sur l’interface de programmation.
Nous allons indiquer à gcc
d’enregistrer dans l’en-tête de la bibliothèque son nom officiel incluant le numéro majeur de version avec l’option -Wl
. Celle-ci transmet au linker la chaîne de caractères qui la suit après avoir remplacé les virgules par des espaces. C’est donc l’option -soname libfact.so.1
qui est passée.
Il est conseillé de répêter les options passées lors de la compilation précédente, comme -fPIC
.
[factorielle]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.0 src/fact.o [factorielle]$ ls lib/ libfact.so.1.0 [factorielle]$
Nous disposons donc d’un fichier libfact.so.1.0
dont l’en-tête contient le nom libfact.so.1
Création des liens symboliques
Lorsque nous compilerons une application, nous préciserons à gcc
de la lier avec la bibliothèque fact
. Celui-ci recherchera un fichier libfact.so
. Et non pas libfact.so.1.0
. Aussi va-t-il falloir créer un lien symbolique pour indiquer le chemin vers le fichier. Ce lien est créé manuellement avec la commande ln
.
[factorielle]$ cd lib/ [lib]$ ln -sf libfact.so.1.0 libfact.so [lib]$ ls -l lib* lrwxrwxrwx 1 cpb cpb 14 2012-01-27 10:04 libfact.so -> libfact.so.1.0 -rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0 [lib]$
Lors de la compilation, gcc
enregistrera dans le fichier exécutable généré le nom de la bibliothèque qu’il a utilisé. Il s’agit du nom « officiel » qu’il trouve dans la section SONAME
que nous avons renseignée avec l’argument -Wl,-soname
précédement.
A l’exécution, le chargeur recherche donc la bibliothèque dont le numéro majeur correspond à celui utilisé lors de la compilation. Il va donc falloir qu’il trouve un fichier libfact.so.1
, ou plutôt un lien libfact.so.1
qui pointe vers libfact.so.1.0
.
La création du premier lien symbolique était nécessaire pour pouvoir compiler une application avec la bibliothèque, le second lien est indispensable pour pouvoir exécuter un programme lié avec elle. Ce lien est donc utilisé beaucoup plus fréquemment que le précédent. Pour simplifier la vie de l’administrateur, une commande nommée ldconfig
va l’aider à créer automatiquement les liens dont son système a besoin pour que les utilisateurs puissent exécuter les applications. Elle parcourt les répertoires-système contenant des bibliothèques (/lib
, /usr/lib
, /usr/local/lib
, etc. plus tous ceux indiqués dans /etc/ld.so.conf
) et crée sur chaque fichier de bibliothèque un lien avec le nom contenu dans sa section SONAME
. Nous allons en voir un exemple en forçant ldconfig
à explorer uniquement notre répertoire grâce à son option -n
.
[lib]$ ldconfig -n . [lib]$ ls -l lib* lrwxrwxrwx 1 cpb cpb 14 2012-01-27 10:04 libfact.so -> libfact.so.1.0 lrwxrwxrwx 1 cpb cpb 14 2012-01-27 10:05 libfact.so.1 -> libfact.so.1.0 -rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0 [lib]$
Les liens présents permettront donc de compiler une application nécessitant notre bibliothèque (par le biais de libfact.so
) puis de l’exécuter en s’assurant que la version majeure soit la bonne (grâce à libfact.so.1
).
Utilisation de la bibliothèque
Compilation d’une application
Écrivons un petit programme qui utilise notre bibliothèque. Le fichier factorielle.c
va invoquer notre fonction factorielle()
sur tous les nombres fournis sur sa ligne de commande.
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fact.h> int main (int argc, char * argv[]) { long int n; int i; if (argc < 2) { fprintf(stderr, "usage: %s valeurs...n", argv[0]); exit(EXIT_FAILURE); } for (i = 1; i < argc; i ++) if (sscanf(argv[i], "%ld", & n) == 1) fprintf(stdout, "%ld! = %lldn", n, factorielle(n)); return EXIT_SUCCESS; }
Ce programme se trouve dans le répertoire factorielle/test/
que nous créons pour l’occasion. Il inclut le fichier d’en-tête <fact.h>
. Il faudra donc que le compilateur puisse le trouver. Pour cela deux solutions :
- placer le fichier d’en-tête dans
/usr/include
,/usr/local/include
ou tout autre répertoire quegcc
consulte – ceci doit être réservé aux fichiers cruciaux pour plusieurs applications utiles pour l’ensemble du système, - laisser le fichier dans un répertoire spécifique à notre bibliothèque et indiquer à
gcc
où le trouver.
C’est naturellement la seconde option que je vais utiliser.
En outre, nous ajouterons en fin de ligne l’argument -lfact
qui demande au linker de réaliser l’édition des liens avec la bibliothèque libfact.so
. Comme pour le fichier d’en-tête il faudra préciser à gcc
où il pourra trouver le fichier libfact.so
que nous avons créé plus haut sous forme de lien symbolique. C’est le rôle de l’option -L
.
[factorielle]$ gcc -I ./include/ -L ./lib/ -o ./test/factorielle ./test/factorielle.c -lfact [factorielle]$ [factorielle]$ ls -l test/ total 12 -rwxrwxr-x 1 cpb cpb 7359 2012-01-27 10:41 factorielle -rw-r--r-- 1 cpb cpb 382 2012-01-25 18:29 factorielle.c [factorielle]$
Exécution de l’application
Si nous testons directement notre programme, son exécution échoue.
$ ./test/factorielle 4 5 6 ./test/factorielle: error while loading shared libraries: libfact.so.1: cannot open shared object file: No such file or directory $
En effet, l’éditeur de liens dynamique qui doit démarrer le processus ne sait pas où trouver la bibliothèque. On remarque au passage qu’il recherche bien le fichier libfact.so.1
(avec le numéro majeur comme extension). Si notre application est suffisamment importante pour être employée régulièrement par différents utilisateurs, il est légitime de placer les fichiers de bibliothèque dans /usr/local/lib
où le linker les trouvera. Toutefois si l’application est en phase de mise au point ou réservée à un emploi rare, on préférera laisser les bibliothèques dans un répertoire personnel. Dans ce cas, il faudra remplir (éventuellement dans un script de lancement) la variable d’environnement LD_LIBRARY_PATH
pour ajouter le chemin d’accès à ces fichiers.
[factorielle]$ export LD_LIBRARY_PATH=./lib/ [factorielle]$ ./test/factorielle 4 5 6 4! = 24 5! = 120 6! = 720 [factorielle]$
Bien sûr, le contenu de la variable LD_LIBRARY_PATH
peut être renseigné avec un chemin absolu plutôt que relatif si on souhaite lancer le programme exécutable depuis un emplacement quelconque.
Maintenance de la bibliothèque
Modification de version mineure
Notre bibliothèque semble fonctionner, nous pouvons commençer à nous livrer à des tests intensifs :
[factorielle]$ ./test/factorielle 3 3! = 6 [factorielle]$
Très bien !
[factorielle]$ ./test/factorielle 2 2! = 2 [factorielle]$
Parfait !
[factorielle]$ ./test/factorielle 1 1! = 1 [factorielle]$
Aucun souci.
[factorielle]$ ./test/factorielle 0 0! = 0 [factorielle]$
Aïe !
Et oui, par convention, il est posé que 0! = 1
(vous pouvez vérifier sur Wikipédia si vous le souhaitez). Notre programme est donc défectueux. La correction est relativement simple, il suffit de remplacer la boucle
do { f = f * n; n = n - 1; } while (n > 1);
par
while (n > 1) { f = f * n; n = n - 1; }
C’est ce que j’ai fait dans le fichier fact-2.c
. En principe je devrais garder le même nom de fichier source et le remplacer simplement pour la nouvelle version de la bibliothèque. Je veux ici conserver la version précédente simplement à titre pédagogique.
Je vais le compiler, puis générer une nouvelle version de bibliothèque en incrémentant le numéro mineur. L’interface de la fonction n’étant pas modifiée, les fichiers exécutables qui en dépendent doivent continuer à fonctionner normalement.
[factorielle]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-2.c [factorielle]$ gcc -fPIC -shared -Wl,-soname,libfact.so.1 -o lib/libfact.so.1.1 src/fact.o
Notre bibliothèque a été re-générée dans un nouveau nom de fichier, aussi faut-il relancer la commande ldconfig
.
[factorielle]$ ldconfig -n lib/ [factorielle]$ ls -l lib/ total 16 lrwxrwxrwx 1 cpb cpb 12 2012-01-27 10:04 libfact.so -> libfact.so.1 lrwxrwxrwx 1 cpb cpb 14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1 -rwxrwxr-x 1 cpb cpb 6659 2012-01-27 10:02 libfact.so.1.0 -rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1 [factorielle]$ [factorielle]$ ./test/factorielle 0 1 2 0! = 1 1! = 1 2! = 2 [factorielle]$
Notre programme fonctionne correctement pour 0!
, et l’ancienne version de la bibliothèque n’étant plus utilisée, il est possible de l’effacer.
[factorielle]$ rm -f lib/libfact.so.1.0 [factorielle]$
Modification de version majeure
Après quelques essais, nous arrivons face à un nouveau problème avec notre bibliothèque.
[factorielle]$ ./test/factorielle -3 -3! = 1 [factorielle]$
Notre fonction renvoie une valeur lorsqu’on lui passe un nombre négatif. La véritable factorielle mathématique n’est définie que sur l’ensemble des entiers naturels, pas pour les entiers relatifs négatifs. Notre fonction devrait donc signaler l’erreur d’argument et non pas renvoyer une valeur, cohérente il est vrai mais trompeuse.
Nous choisissons de modifier l’interface de notre routine, qui va prendre en argument un pointeur sur un entier long long
dans lequel elle stockera le résultat, et renverra une valeur de réussite (zéro) ou d’échec (-1). Cette modification d’interface va impliquer une adaptation et une recompilation des applications utilisant la bibliothèque, aussi devrons-nous changer de version majeure.
La nouvelle fonction fact-3.c
est définie comme suit.
int factorielle(long int n, long long int * result) { * result = 1; if (n < 0) return -1; do { (*result) = (*result) * n; n = n - 1; } while (n > 1); return 0; }
Bien sur, on modifie le fichier d’en-tête en (fact-3.h
):
#ifndef LIB_FACT_H #define LIB_FACT_H int factorielle(long int n, long long int * result); #endif
Compilons notre bibliothèque comme précédemment.
[factorielle]$ gcc -c -I include/ -fPIC -Wall -o src/fact.o src/fact-3.c [factorielle]$ gcc -fPIC -shared -Wl,-soname,libfact.so.2 -o lib/libfact.so.2.0 src/fact.o [factorielle]$ ldconfig -n lib/ [factorielle]$ ls -l lib/ total 16 lrwxrwxrwx 1 cpb cpb 12 2012-01-27 10:04 libfact.so -> libfact.so.1 lrwxrwxrwx 1 cpb cpb 14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1 -rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1 lrwxrwxrwx 1 cpb cpb 14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0 -rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0 [factorielle]$ cd lib/ [lib]$ ln -sf libfact.so.2 libfact.so [lib]$ ls -l total 16 lrwxrwxrwx 1 cpb cpb 12 2012-01-27 10:27 libfact.so -> libfact.so.2 lrwxrwxrwx 1 cpb cpb 14 2012-01-27 10:11 libfact.so.1 -> libfact.so.1.1 -rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:10 libfact.so.1.1 lrwxrwxrwx 1 cpb cpb 14 2012-01-27 10:26 libfact.so.2 -> libfact.so.2.0 -rwxrwxr-x 1 cpb cpb 6661 2012-01-27 10:26 libfact.so.2.0 [lib]$ cd .. [factorielle]$
Les liens sont en place pour compiler une nouvelle version du programme de test (factorielle-2.c
).
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fact.h> int main (int argc, char * argv[]) { long int n; long long int f; int i; if (argc < 2) { fprintf(stderr, "usage: %s valeurs...n", argv[0]); exit(EXIT_FAILURE); } for (i = 1; i < argc; i ++) if (sscanf(argv[i], "%ld", & n) == 1) { if (factorielle(n, & f) == 0) fprintf(stdout, "%ld! = %lldn", n, f); else fprintf(stdout, "%ld! n'existe pasn", n); } return EXIT_SUCCESS; }
Compilons et essayons-le :
[factorielle]$ gcc -I ./include/ -L ./lib/ -o ./test/factorielle-2 ./test/factorielle-2.c -lfact [factorielle]$ ./test/factorielle-2 3 0 -3 3! = 6 0! = 0 -3! n'existe pas [factorielle]$
Cette fois notre programme se comporte correctement. On peut noter que la présence de l’ancienne version majeure permet à notre précédent exécutable de continuer à fonctionner.
[factorielle]$ ./test/factorielle 3 0 -3 3! = 6 0! = 1 -3! = 1 [factorielle]$
Conclusion
La gestion des numéros majeurs et mineurs de version pour les bibliothèques dynamiques offre les avantages suivants :
- Les modifications internes uniquement, représentées par des évolutions du numéro mineur, permettent aux exécutables déjà compilés de fonctionner directement avec la nouvelle version de la bibliothèque et de bénéficier – sans recompilation – des améliorations.
- Les transformations de l’interface externe de la bibliothèque impliquent une recompilation (éventuellement après adaptation) pour pouvoir fonctionner.
Plusieurs versions majeures de la bibliothèque peuvent cohabiter simultanément permettant un fonctionnement correct de différentes générations d’une application. Toutefois les nouvelles compilations utiliseront la version majeure pointée par le lien symbolique contenant uniquement le nom de la bibliothèque (libfact.so
)
Nous verrons dans le prochain article comment déboguer le code de la bibliothèque dynamique en effectuant un suivi pas-à-pas de l’exécution et en examinant le contenu de ses variables.