Mise au point de bibliothèque dynamique (1/3)

Publié par cpb
Jan 28 2012

(English translation here)

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.soso 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êter gcc 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épertoire include/ 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 que gcc 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.

Bibliothèque dynamique version 1.0

Bibliothèque dynamique version 1.0

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]$
Bibliothèque dynamique version 1.1

Bibliothèque dynamique version 1.1

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]$
Bibliothèque dynamique version 2.0

Bibliothèque dynamique version 2.0

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.

 

URL de trackback pour cette page