Fondamentaux des bibliothèques partagées sous Linux

Ce forum est dédié à apprendre le développement de programmes user mode sur Linux

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 114
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Fondamentaux des bibliothèques partagées sous Linux

Post by Hydraxx »

Fondamentaux des bibliothèques partagées sous Linux

Objectif

Les bibliothèques partagées permettent de placer du code commun dans un fichier séparé qui peut être utilisé par plusieurs programmes.

Elles permettent notamment :
  • de réduire la taille des exécutables ;
  • d'éviter de dupliquer le même code dans plusieurs programmes ;
  • de partager en mémoire certaines pages de code entre plusieurs processus ;
  • de mettre à jour une bibliothèque sans forcément relier tous les exécutables ;
  • de charger du code à l'exécution.
Sous Linux moderne, les bibliothèques partagées sont généralement des fichiers ELF portant l'extension :

Code: Select all

.so
Exemple :

Code: Select all

libm.so
libc.so.6
libpthread.so.0

1. Fichiers objets et édition de liens

Un fichier source C est d'abord compilé en fichier objet :

Code: Select all

gcc -c prog.c
gcc -c mod1.c
gcc -c mod2.c
On obtient :

Code: Select all

prog.o
mod1.o
mod2.o
Puis l'éditeur de liens rassemble les fichiers objets pour produire l'exécutable :

Code: Select all

gcc -o prog prog.o mod1.o mod2.o
Le programme ld est l'éditeur de liens utilisé en arrière-plan par gcc.

Schéma :

Code: Select all

source .c
   |
   v
compilation
   |
   v
fichier objet .o
   |
   v
édition de liens
   |
   v
exécutable

2. Bibliothèques statiques

Une bibliothèque statique regroupe plusieurs fichiers objets dans une archive :

Code: Select all

libdemo.a
Création :

Code: Select all

gcc -c mod1.c mod2.c
ar rcs libdemo.a mod1.o mod2.o
Liaison avec le programme :

Code: Select all

gcc -o prog prog.o -L. -ldemo
Ou directement :

Code: Select all

gcc -o prog prog.o ./libdemo.a
Principe :

Lors de l'édition de liens, le code nécessaire de la bibliothèque statique est intégré dans l'exécutable.

Conséquences :
  • l'exécutable est plus gros ;
  • chaque programme possède sa propre copie du code ;
  • pas besoin du fichier .a au moment de l'exécution ;
  • une mise à jour de la bibliothèque nécessite généralement de relier à nouveau le programme.

3. Bibliothèques partagées

Une bibliothèque partagée est chargée séparément de l'exécutable.

Exemple :

Code: Select all

libdemo.so
Contrairement à une bibliothèque statique, son code n'est pas copié entièrement dans l'exécutable.

L'exécutable contient surtout une référence vers la bibliothèque dont il dépend.

Avantages :
  • moins d'espace disque ;
  • moins de duplication du code en mémoire ;
  • plusieurs processus peuvent partager les pages de code en lecture seule ;
  • mise à jour plus simple d'une bibliothèque compatible ;
  • exécutables plus petits.
Inconvénients :
  • chargement et résolution de symboles plus complexes ;
  • dépendance au bon fichier .so présent sur le système ;
  • risque de problèmes de compatibilité ABI ;
  • coût de certaines relocations ;
  • possibilité d'erreurs au démarrage si une bibliothèque manque.

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

On compile généralement les fichiers objets avec :

Code: Select all

-fPIC
Exemple :

Code: Select all

gcc -fPIC -c mod1.c
gcc -fPIC -c mod2.c
Puis création de la bibliothèque :

Code: Select all

gcc -shared -o libdemo.so mod1.o mod2.o
Commande compacte :

Code: Select all

gcc -fPIC -shared -o libdemo.so mod1.c mod2.c

5. Position Independent Code : -fPIC

PIC = Position Independent Code

Le code d'une bibliothèque partagée peut être chargé à différentes adresses virtuelles selon les processus.

Il faut donc éviter que le code contienne inutilement des adresses absolues fixes.

L'option :

Code: Select all

-fPIC
demande au compilateur de produire du code pouvant être chargé à différentes adresses.

Cette propriété est fondamentale pour les bibliothèques partagées.

Schéma :

Code: Select all

même libdemo.so

processus A -> chargée à 0x7f...
processus B -> chargée à 0x7a...
processus C -> chargée à 0x7e...
Le code doit rester utilisable quelle que soit l'adresse choisie.


6. Relocations

Une relocation est une correction effectuée par l'éditeur de liens ou le chargeur afin qu'une référence symbolique pointe vers la bonne adresse.

Exemple conceptuel :

Code: Select all

appel de fonction foo()
        |
        v
résolution du symbole foo
        |
        v
adresse réelle de foo
Plus une bibliothèque contient de code dépendant d'adresses absolues, plus elle peut nécessiter de relocations à l'exécution.

Le PIC réduit ce problème.


7. Utiliser une bibliothèque partagée

Supposons :

Code: Select all

libdemo.so
prog.c
Compilation :

Code: Select all

gcc -o prog prog.c -L. -ldemo
Options :

Code: Select all

-L.
indique un répertoire où chercher les bibliothèques au moment de l'édition de liens.

Code: Select all

-ldemo
signifie chercher une bibliothèque nommée :

Code: Select all

libdemo.so
ou éventuellement :

Code: Select all

libdemo.a
Le préfixe lib et l'extension sont ajoutés automatiquement.


8. Différence entre édition de liens et chargement dynamique

Il faut distinguer deux moments.

À la compilation / édition de liens :

Le linker vérifie les symboles nécessaires et enregistre les dépendances de l'exécutable.

À l'exécution :

Le chargeur dynamique recherche les bibliothèques réelles et les mappe dans l'espace virtuel du processus.

Schéma :

Code: Select all

gcc / ld
   |
   v
exécutable ELF
   |
   v
lancement
   |
   v
dynamic linker
   |
   v
chargement des .so
   |
   v
résolution des symboles
   |
   v
main()

9. Le chargeur dynamique

Sous Linux ELF, le chargeur dynamique est généralement un programme de type :

Code: Select all

/lib64/ld-linux-x86-64.so.2
ou selon l'architecture :

Code: Select all

/lib/ld-linux.so.2
Il est responsable notamment de :
  • charger les bibliothèques dépendantes ;
  • chercher les fichiers .so ;
  • effectuer les relocations nécessaires ;
  • résoudre les symboles ;
  • préparer l'exécution avant le transfert vers le programme.

10. Dépendances ELF : DT_NEEDED

Lorsqu'un exécutable dépend d'une bibliothèque partagée, ELF contient des entrées indiquant les bibliothèques requises.

On peut voir les dépendances avec :

Code: Select all

readelf -d prog
ou :

Code: Select all

objdump -p prog
On peut observer des entrées :

Code: Select all

NEEDED
Exemple :

Code: Select all

Shared library: [libdemo.so.1]
Shared library: [libc.so.6]

11. ldd

La commande :

Code: Select all

ldd prog
affiche les bibliothèques partagées nécessaires et le fichier réellement sélectionné.

Exemple :

Code: Select all

libdemo.so.1 => /usr/lib/libdemo.so.1
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
C'est un outil essentiel pour diagnostiquer les dépendances dynamiques.


12. LD_LIBRARY_PATH

La variable d'environnement :

Code: Select all

LD_LIBRARY_PATH
ajoute des répertoires de recherche pour le chargeur dynamique.

Exemple :

Code: Select all

export LD_LIBRARY_PATH=/home/user/lib
./prog
Ou pour une seule commande :

Code: Select all

LD_LIBRARY_PATH=. ./prog
Attention :

LD_LIBRARY_PATH est très utile pour les tests et bibliothèques privées, mais n'est généralement pas la meilleure méthode d'installation système en production.


13. Répertoires de bibliothèques

Les bibliothèques peuvent notamment se trouver dans :

Code: Select all

/lib
/usr/lib
/usr/local/lib
/lib64
/usr/lib64
Les chemins exacts dépendent de l'architecture et de la distribution.


14. Soname

Une bibliothèque partagée possède souvent plusieurs noms.

Exemple :

Code: Select all

libdemo.so.1.2.3
libdemo.so.1
libdemo.so
Ces noms ont des rôles différents.

Real name

Nom du fichier réel :

Code: Select all

libdemo.so.1.2.3
Soname

Nom ABI majeur utilisé à l'exécution :

Code: Select all

libdemo.so.1
Linker name

Nom utilisé par l'éditeur de liens lors de :

Code: Select all

-ldemo
Il correspond généralement à :

Code: Select all

libdemo.so

15. Créer un soname

Exemple :

Code: Select all

gcc -fPIC -c mod1.c mod2.c

gcc -shared \
    -Wl,-soname,libdemo.so.1 \
    -o libdemo.so.1.2.3 \
    mod1.o mod2.o
Puis :

Code: Select all

ln -s libdemo.so.1.2.3 libdemo.so.1
ln -s libdemo.so.1 libdemo.so
Schéma :

Code: Select all

libdemo.so
    |
    v
libdemo.so.1
    |
    v
libdemo.so.1.2.3

16. Pourquoi le soname est important

Le soname permet de représenter une famille compatible d'une bibliothèque.

Exemple :

Code: Select all

libdemo.so.1.0
libdemo.so.1.1
libdemo.so.1.9
peuvent rester compatibles avec :

Code: Select all

libdemo.so.1
Mais une rupture ABI importante peut conduire à :

Code: Select all

libdemo.so.2
Ainsi, un programme lié à :

Code: Select all

libdemo.so.1
ne basculera pas automatiquement vers :

Code: Select all

libdemo.so.2

17. Version majeure et mineure

Convention courante :

Code: Select all

libdemo.so.major.minor
Exemple :

Code: Select all

libdemo.so.2.4
En général :
  • major change lorsque l'ABI devient incompatible ;
  • minor change pour une évolution compatible.
Ce n'est pas une loi imposée par ELF, mais une convention très importante.


18. Compatibilité ABI

Une bibliothèque reste compatible si le programme déjà compilé peut continuer à l'utiliser correctement.

Des changements généralement dangereux :
  • supprimer une fonction publique ;
  • changer ses paramètres ;
  • changer son type de retour ;
  • modifier une structure publique d'une façon incompatible ;
  • changer la signification d'un symbole exporté.
Des changements souvent compatibles :
  • corriger une implémentation interne ;
  • ajouter une nouvelle fonction sans supprimer l'ancienne ;
  • améliorer les performances sans changer l'interface publique.
API et ABI ne sont pas la même chose.

API = interface vue par le code source.

ABI = convention binaire réellement utilisée entre modules compilés.


19. Mise à jour d'une bibliothèque

Si une nouvelle version est compatible avec le même soname :

Code: Select all

libdemo.so.1.2.3
       ->
libdemo.so.1.2.4
il suffit généralement de remplacer le fichier et de mettre à jour le lien :

Code: Select all

libdemo.so.1
Les anciens programmes peuvent alors utiliser la nouvelle version sans recompilation.

Si l'ABI change :

Code: Select all

libdemo.so.1
       ->
libdemo.so.2
les deux versions peuvent cohabiter.


20. Installation des bibliothèques

Une bibliothèque système est généralement installée dans un répertoire connu du chargeur :

Code: Select all

/lib
/usr/lib
/usr/local/lib
Après installation, on utilise souvent :

Code: Select all

ldconfig
Cette commande :
  • analyse les répertoires de bibliothèques ;
  • met à jour les liens symboliques appropriés ;
  • met à jour le cache des bibliothèques.

21. /etc/ld.so.conf

Le fichier :

Code: Select all

/etc/ld.so.conf
et souvent les fichiers de :

Code: Select all

/etc/ld.so.conf.d/
définissent des répertoires supplémentaires pour le chargeur dynamique.

Après modification :

Code: Select all

sudo ldconfig

22. Cache du chargeur dynamique

Le chargeur peut utiliser un cache généré par :

Code: Select all

ldconfig
Cela évite de parcourir inutilement tous les répertoires à chaque lancement.


23. RPATH et RUNPATH

Un exécutable ELF peut contenir directement des chemins de recherche.

Avec gcc :

Code: Select all

gcc prog.c -L./lib -ldemo \
    -Wl,-rpath,/chemin/vers/lib
On peut voir ces entrées avec :

Code: Select all

readelf -d prog
Entrées possibles :

Code: Select all

DT_RPATH
DT_RUNPATH
Leur comportement et leur priorité ne sont pas exactement identiques.

Aujourd'hui, RUNPATH est généralement préféré à l'ancien RPATH.


24. $ORIGIN

Le chargeur dynamique comprend le token :

Code: Select all

$ORIGIN
Il représente le répertoire contenant l'exécutable ou l'objet concerné.

Exemple :

Code: Select all

-Wl,-rpath,'$ORIGIN/lib'
Organisation :

Code: Select all

app/
├── prog
└── lib/
    └── libdemo.so
Le programme peut alors chercher sa bibliothèque relativement à sa propre position.

Très pratique pour les applications portables.


25. Recherche des bibliothèques à l'exécution

La recherche exacte dépend de plusieurs paramètres et de la présence de DT_RPATH / DT_RUNPATH.

À retenir conceptuellement :
  • chemins intégrés dans l'objet ELF ;
  • LD_LIBRARY_PATH ;
  • cache du chargeur dynamique ;
  • répertoires système standards.
Le chargeur sélectionne ensuite la bibliothèque correspondant au nom demandé.


26. Résolution des symboles à l'exécution

Une bibliothèque partagée référence souvent des symboles qui ne sont pas encore résolus définitivement au moment de sa création.

Le chargeur dynamique doit déterminer quelle définition utiliser.

Exemple :

Code: Select all

prog
 |
 +--> foo()

libA.so
 |
 +--> foo()

libB.so
 |
 +--> foo()
Le symbole réellement choisi dépend de l'ordre et des règles de résolution des symboles ELF.


27. Interposition de symboles

Un symbole d'une bibliothèque peut parfois être remplacé par une définition trouvée plus tôt dans l'espace global de résolution.

Cette propriété est appelée :

Code: Select all

symbol interposition
Elle permet certaines techniques utiles mais peut également provoquer des effets subtils.


28. Lien statique à la place du lien dynamique

On peut demander une liaison statique :

Code: Select all

gcc -static -o prog prog.c
Ou choisir explicitement un fichier :

Code: Select all

gcc prog.o /chemin/libdemo.a -o prog
Avantages :
  • exécutable plus autonome ;
  • pas besoin des bibliothèques dynamiques correspondantes à l'exécution.
Inconvénients :
  • exécutable plus gros ;
  • duplication du code ;
  • mises à jour de bibliothèques non automatiquement récupérées ;
  • certaines bibliothèques ou fonctionnalités système se prêtent mal au lien statique total.

Résumé chapitre 41

Code: Select all

STATIC
libdemo.a
    |
    v
code intégré dans l'exécutable


SHARED
libdemo.so
    |
    v
référence enregistrée dans l'exécutable
    |
    v
chargement par ld-linux au runtime
Commandes essentielles :

Code: Select all

gcc -fPIC -c mod.c
gcc -shared -o libdemo.so mod.o

gcc prog.c -L. -ldemo -o prog

ldd prog
readelf -d prog
objdump -p prog

ldconfig

Chapitre 42 — Fonctions avancées des bibliothèques partagées

Le chapitre précédent décrit le chargement automatique des bibliothèques requises par un programme.

Linux permet aussi de charger une bibliothèque manuellement pendant l'exécution.

Cette technique repose principalement sur l'API :

Code: Select all

dlopen()
dlsym()
dlerror()
dlclose()
Header :

Code: Select all

#include <dlfcn.h>
Selon l'environnement, on peut lier avec :

Code: Select all

-ldl

1. Chargement dynamique explicite

Principe :

Code: Select all

programme démarre
      |
      v
dlopen()
      |
      v
charge libplugin.so
      |
      v
dlsym()
      |
      v
cherche une fonction
      |
      v
appel de la fonction
      |
      v
dlclose()
Cette approche est très utilisée pour :
  • plugins ;
  • modules optionnels ;
  • backends chargés dynamiquement ;
  • extensions ;
  • systèmes de pilotes utilisateurs ;
  • logiciels pouvant fonctionner avec plusieurs implémentations.

2. dlopen()

Prototype :

Code: Select all

void *dlopen(const char *filename, int flags);
Exemple :

Code: Select all

void *handle;

handle = dlopen("./libdemo.so", RTLD_LAZY);

if (handle == NULL) {
    // erreur
}
Retour :
  • handle valide en cas de succès ;
  • NULL en cas d'erreur.
Le handle est ensuite utilisé par dlsym() et dlclose().


3. Recherche du fichier avec dlopen()

Si filename contient un slash :

Code: Select all

./libdemo.so
/home/user/lib/libdemo.so
il est interprété comme un chemin.

Sinon :

Code: Select all

dlopen("libdemo.so", ...);
le chargeur applique ses règles normales de recherche des bibliothèques.


4. Compteur de références

Une même bibliothèque peut être ouverte plusieurs fois.

Le système maintient conceptuellement un compteur de références.

Exemple :

Code: Select all

dlopen()
dlopen()
dlclose()
dlclose()
La bibliothèque n'est normalement éligible au déchargement qu'une fois les références correspondantes libérées.

Une bibliothèque peut aussi rester chargée si elle est encore nécessaire comme dépendance d'un autre objet.


5. RTLD_LAZY

Exemple :

Code: Select all

dlopen("libdemo.so", RTLD_LAZY);
Les références vers les fonctions peuvent être résolues seulement lorsqu'elles sont réellement utilisées.

Avantage :
  • chargement initial potentiellement plus rapide.
Inconvénient :
  • certaines erreurs de symboles peuvent n'apparaître qu'au premier appel.

6. RTLD_NOW

Exemple :

Code: Select all

dlopen("libdemo.so", RTLD_NOW);
Le chargeur demande une résolution immédiate des symboles nécessaires.

Cela permet de détecter les problèmes plus tôt.

Différence :

Code: Select all

RTLD_LAZY
 -> résolution différée de certaines fonctions

RTLD_NOW
 -> résolution immédiate

7. RTLD_GLOBAL

Exemple :

Code: Select all

dlopen("libdemo.so", RTLD_NOW | RTLD_GLOBAL);
Les symboles de cette bibliothèque peuvent participer à la résolution de symboles pour d'autres bibliothèques chargées ensuite.


8. RTLD_LOCAL

Exemple :

Code: Select all

dlopen("libdemo.so", RTLD_NOW | RTLD_LOCAL);
Les symboles chargés ne sont pas placés dans l'espace global de résolution.

C'est généralement le comportement par défaut.

Résumé :

Code: Select all

RTLD_GLOBAL
 -> symboles visibles pour d'autres objets

RTLD_LOCAL
 -> symboles limités au groupe chargé

9. RTLD_NOLOAD

RTLD_NOLOAD permet notamment de vérifier si une bibliothèque est déjà chargée sans nécessairement la charger une nouvelle fois.

Exemple conceptuel :

Code: Select all

dlopen("libdemo.so", RTLD_NOLOAD | RTLD_NOW);

10. RTLD_NODELETE

Sous Linux, RTLD_NODELETE permet de demander que la bibliothèque ne soit pas réellement retirée de l'espace d'adressage lors de dlclose().

Cela évite notamment de réinitialiser certaines données statiques lors d'un futur rechargement.


11. RTLD_DEEPBIND

RTLD_DEEPBIND modifie les priorités de résolution afin de favoriser les symboles propres à la bibliothèque chargée par rapport à certains symboles globaux déjà présents.

C'est une extension GNU/Linux et non un mécanisme POSIX portable.


12. dlerror()

Prototype :

Code: Select all

char *dlerror(void);
Elle retourne une chaîne décrivant la dernière erreur liée aux fonctions dl*.

Exemple :

Code: Select all

void *h = dlopen("./libdemo.so", RTLD_NOW);

if (h == NULL) {
    fprintf(stderr, "%s\n", dlerror());
}
Important avec dlsym()

Un symbole peut théoriquement avoir une adresse NULL.

Il faut donc utiliser dlerror() correctement pour distinguer :
  • symbole trouvé à une valeur NULL ;
  • échec réel de dlsym().
Pattern :

Code: Select all

dlerror();

void *p = dlsym(handle, "foo");

char *err = dlerror();

if (err != NULL) {
    // erreur
}

13. dlsym()

Prototype :

Code: Select all

void *dlsym(void *handle, const char *symbol);
Exemple :

Code: Select all

void *addr;

addr = dlsym(handle, "foo");
dlsym() recherche un symbole dans la bibliothèque et ses dépendances selon les règles applicables.

Le symbole peut représenter :
  • une fonction ;
  • une variable globale.

14. Utiliser une fonction retournée par dlsym()

Exemple :

Code: Select all

int (*func)(int);

func = (int (*)(int)) dlsym(handle, "foo");

if (func != NULL) {
    int result = func(10);
}
Sur les systèmes POSIX modernes, cette technique est couramment utilisée pour convertir le résultat de dlsym() en pointeur de fonction.


15. Utiliser une variable retournée par dlsym()

Exemple :

Code: Select all

int *value;

value = (int *) dlsym(handle, "global_value");

if (value != NULL) {
    printf("%d\n", *value);
}

16. RTLD_DEFAULT

Au lieu d'un handle obtenu avec dlopen(), dlsym() peut recevoir :

Code: Select all

RTLD_DEFAULT
Cela demande une recherche selon l'espace normal de symboles visible au programme.

Exemple :

Code: Select all

void *p = dlsym(RTLD_DEFAULT, "malloc");

17. RTLD_NEXT

RTLD_NEXT permet de chercher la prochaine définition du symbole après l'objet courant.

Très utile lorsqu'on interpose une fonction mais qu'on veut appeler ensuite l'implémentation originale.

Exemple conceptuel :

Code: Select all

malloc() remplacé
      |
      v
dlsym(RTLD_NEXT, "malloc")
      |
      v
vrai malloc suivant

18. dlclose()

Prototype :

Code: Select all

int dlclose(void *handle);
Exemple :

Code: Select all

if (dlclose(handle) != 0) {
    fprintf(stderr, "%s\n", dlerror());
}
Elle décrémente le nombre de références associées au handle.

Lorsque le compteur atteint zéro et qu'aucune dépendance ne nécessite encore la bibliothèque, celle-ci peut être déchargée.


19. dladdr()

dladdr() permet d'obtenir des informations concernant l'adresse d'un symbole chargé.

Prototype conceptuel :

Code: Select all

int dladdr(const void *addr, Dl_info *info);
Structure :

Code: Select all

Dl_info
Informations typiques :
  • nom du fichier contenant l'adresse ;
  • adresse de base de la bibliothèque ;
  • nom du symbole correspondant ;
  • adresse exacte du symbole.
Très utile pour :
  • debug ;
  • diagnostic ;
  • introspection ;
  • trouver à quelle bibliothèque appartient une fonction.

20. Exporter des symboles du programme principal

Normalement, tous les symboles du programme principal ne sont pas forcément visibles par les bibliothèques chargées dynamiquement.

On peut demander leur export avec :

Code: Select all

-rdynamic
Exemple :

Code: Select all

gcc -rdynamic -o prog prog.c -ldl
Ou via le linker :

Code: Select all

-Wl,--export-dynamic
Cela peut être nécessaire lorsqu'un plugin doit appeler une fonction définie directement dans le programme principal.


21. Visibilité des symboles

Une bibliothèque ne devrait pas forcément exporter tous ses symboles internes.

Exporter inutilement beaucoup de symboles :
  • augmente la taille de la table dynamique ;
  • peut créer des collisions ;
  • augmente la surface d'ABI publique ;
  • peut provoquer des interpositions inattendues.
Il est donc préférable d'exporter seulement l'interface publique.


22. static

En C :

Code: Select all

static void helper(void)
{
}
Cette fonction possède une liaison interne au fichier source et n'est pas exportée comme symbole global utilisable par les autres objets.


23. GCC visibility attribute

GCC permet notamment :

Code: Select all

__attribute__((visibility("hidden")))
Exemple :

Code: Select all

__attribute__((visibility("hidden")))
void internal_function(void)
{
}
Ce symbole n'est pas exposé normalement dans l'interface dynamique publique.

On peut également contrôler la visibilité générale avec :

Code: Select all

-fvisibility=hidden
puis rendre explicitement visibles uniquement les symboles publics.


24. Linker version scripts

Un version script est un fichier texte donné au linker.

Exemple :

Code: Select all

gcc -shared -o libdemo.so *.o \
    -Wl,--version-script,version.map
Il peut contrôler :
  • les symboles exportés ;
  • les symboles cachés ;
  • les versions associées aux symboles.

25. Exemple de contrôle de visibilité par script

Fichier :

Code: Select all

version.map
Contenu :

Code: Select all

VERS_1 {
    global:
        public_function;
    local:
        *;
};
Cela signifie :

Code: Select all

public_function
 -> exportée

tous les autres symboles
 -> locaux/cachés

26. Symbol versioning

Linux permet à une même bibliothèque de fournir plusieurs versions d'un même symbole.

Exemple conceptuel :

Code: Select all

foo@VER_1
foo@@VER_2
Cela permet :
  • aux anciens programmes de continuer à utiliser l'ancienne ABI ;
  • aux nouveaux programmes d'utiliser une nouvelle implémentation ;
  • de faire évoluer une bibliothèque sans casser immédiatement les anciens binaires.

27. Version par défaut

Dans la notation ELF :

Code: Select all

foo@VER_1
représente une version précise.

Code: Select all

foo@@VER_2
avec deux arobases représente généralement la version par défaut utilisée lors des nouvelles éditions de liens.


28. Dépendances entre versions

Un version script peut exprimer :

Code: Select all

VER_1 {
    global:
        foo;
};

VER_2 {
    global:
        bar;
} VER_1;
Ici :

Code: Select all

VER_2 dépend de VER_1
Cela construit une hiérarchie de versions.


29. Initialisation automatique d'une bibliothèque

Une fonction peut être exécutée automatiquement lorsqu'une bibliothèque est chargée.

Avec GCC :

Code: Select all

__attribute__((constructor))
static void init_library(void)
{
    // initialisation
}
Cette fonction est appelée automatiquement au chargement.


30. Finalisation automatique

Même principe :

Code: Select all

__attribute__((destructor))
static void fini_library(void)
{
    // nettoyage
}
Elle est appelée lors du déchargement ou de la fin du processus selon le contexte.


31. _init() et _fini()

Historiquement, il était possible d'utiliser :

Code: Select all

_init()
_fini()
Mais les attributs :

Code: Select all

constructor
destructor
sont aujourd'hui généralement préférés avec GCC car ils sont plus flexibles et permettent plusieurs fonctions d'initialisation/finalisation.


32. LD_PRELOAD

La variable :

Code: Select all

LD_PRELOAD
demande au chargeur de charger certaines bibliothèques avant les autres.

Exemple :

Code: Select all

LD_PRELOAD=./libhook.so ./prog
Cela permet notamment de fournir une définition d'un symbole avant celle des bibliothèques habituelles.

Applications légitimes :
  • debug ;
  • instrumentation ;
  • profiling ;
  • tests ;
  • compatibilité ;
  • interception de fonctions.

33. Exemple conceptuel d'interposition

Programme :

Code: Select all

malloc()
Normalement :

Code: Select all

prog
 |
 v
libc malloc()
Avec :

Code: Select all

LD_PRELOAD=./libhook.so
on peut obtenir :

Code: Select all

prog
 |
 v
libhook malloc()
 |
 v
RTLD_NEXT
 |
 v
libc malloc()

34. Restrictions de sécurité de LD_PRELOAD

Pour les programmes exécutés dans certains modes privilégiés ou sécurisés, le chargeur ignore ou limite des variables telles que :

Code: Select all

LD_PRELOAD
LD_LIBRARY_PATH
LD_DEBUG
Cela évite qu'un utilisateur injecte arbitrairement une bibliothèque dans un programme privilégié.


35. /etc/ld.so.preload

Linux permet également de précharger des bibliothèques via :

Code: Select all

/etc/ld.so.preload
Ce mécanisme agit de façon système et doit être utilisé avec beaucoup de prudence.


36. LD_DEBUG

La variable :

Code: Select all

LD_DEBUG
permet d'afficher des informations internes sur le fonctionnement du chargeur dynamique.

Exemple :

Code: Select all

LD_DEBUG=libs ./prog
Catégories utiles :

Code: Select all

libs
reloc
files
symbols
bindings
versions
statistics
unused
all
help

37. Exemples LD_DEBUG

Voir les recherches de bibliothèques :

Code: Select all

LD_DEBUG=libs ./prog
Voir la résolution des symboles :

Code: Select all

LD_DEBUG=symbols ./prog
Voir les liaisons :

Code: Select all

LD_DEBUG=bindings ./prog
Tout afficher :

Code: Select all

LD_DEBUG=all ./prog

38. Rediriger LD_DEBUG

La sortie de LD_DEBUG peut être volumineuse.

Linux permet notamment d'utiliser :

Code: Select all

LD_DEBUG_OUTPUT
Exemple :

Code: Select all

LD_DEBUG=libs LD_DEBUG_OUTPUT=/tmp/trace ./prog

39. Exemple complet dlopen + dlsym

Bibliothèque :

Code: Select all

/* plugin.c */

#include <stdio.h>

void hello(void)
{
    printf("Hello depuis le plugin\n");
}
Compilation :

Code: Select all

gcc -fPIC -shared plugin.c -o libplugin.so
Programme :

Code: Select all

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

int main(void)
{
    void *handle;
    void (*hello)(void);
    char *error;

    handle = dlopen("./libplugin.so", RTLD_NOW);

    if (handle == NULL) {
        fprintf(stderr, "%s\n", dlerror());
        return EXIT_FAILURE;
    }

    dlerror();

    hello = (void (*)(void)) dlsym(handle, "hello");

    error = dlerror();

    if (error != NULL) {
        fprintf(stderr, "%s\n", error);
        dlclose(handle);
        return EXIT_FAILURE;
    }

    hello();

    if (dlclose(handle) != 0) {
        fprintf(stderr, "%s\n", dlerror());
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
Compilation du programme :

Code: Select all

gcc main.c -o prog -ldl
Exécution :

Code: Select all

./prog

40. Schéma global des bibliothèques dynamiques

Code: Select all

                    COMPILATION

mod1.c ----> mod1.o \
mod2.c ----> mod2.o  \
                      +--> libdemo.so
mod3.c ----> mod3.o  /
                     /
                   -fPIC
                   -shared


                    LINK

prog.c
  |
  v
prog.o
  |
  +---- -ldemo
  |
  v
prog ELF
  |
  +---- DT_NEEDED = libdemo.so.1


                    EXÉCUTION

execve()
  |
  v
kernel
  |
  v
ld-linux
  |
  +--> cherche libdemo.so.1
  |
  +--> mmap bibliothèque
  |
  +--> relocations
  |
  +--> résolution symboles
  |
  +--> constructors
  |
  v
main()

41. Carte mentale

Code: Select all

BIBLIOTHÈQUES LINUX
|
+-- statiques
|   |
|   +-- .a
|   +-- ar
|   +-- code copié dans l'exécutable
|
+-- partagées
    |
    +-- .so
    +-- -fPIC
    +-- -shared
    |
    +-- chargement
    |   |
    |   +-- ld-linux
    |   +-- DT_NEEDED
    |   +-- LD_LIBRARY_PATH
    |   +-- RPATH / RUNPATH
    |   +-- $ORIGIN
    |   +-- ldconfig
    |
    +-- versions
    |   |
    |   +-- real name
    |   +-- soname
    |   +-- linker name
    |   +-- ABI major
    |
    +-- outils
    |   |
    |   +-- ldd
    |   +-- readelf
    |   +-- objdump
    |
    +-- chargement manuel
    |   |
    |   +-- dlopen
    |   +-- dlsym
    |   +-- dlerror
    |   +-- dlclose
    |   +-- dladdr
    |
    +-- flags
    |   |
    |   +-- RTLD_LAZY
    |   +-- RTLD_NOW
    |   +-- RTLD_GLOBAL
    |   +-- RTLD_LOCAL
    |   +-- RTLD_NOLOAD
    |   +-- RTLD_NODELETE
    |   +-- RTLD_DEEPBIND
    |
    +-- symboles
    |   |
    |   +-- visibilité
    |   +-- interposition
    |   +-- version scripts
    |   +-- symbol versioning
    |   +-- RTLD_DEFAULT
    |   +-- RTLD_NEXT
    |
    +-- hooks / debug
        |
        +-- LD_PRELOAD
        +-- LD_DEBUG

42. Commandes à connaître

Créer un objet PIC :

Code: Select all

gcc -fPIC -c lib.c
Créer une bibliothèque partagée :

Code: Select all

gcc -shared -o libdemo.so lib.o
Créer avec soname :

Code: Select all

gcc -shared \
    -Wl,-soname,libdemo.so.1 \
    -o libdemo.so.1.0 \
    lib.o
Créer les liens :

Code: Select all

ln -s libdemo.so.1.0 libdemo.so.1
ln -s libdemo.so.1 libdemo.so
Lier un programme :

Code: Select all

gcc prog.c -L. -ldemo -o prog
Tester localement :

Code: Select all

LD_LIBRARY_PATH=. ./prog
Afficher les dépendances :

Code: Select all

ldd ./prog
Afficher les informations ELF dynamiques :

Code: Select all

readelf -d ./prog
Afficher les symboles :

Code: Select all

readelf -Ws libdemo.so
ou :

Code: Select all

nm -D libdemo.so
Installer / actualiser le cache :

Code: Select all

sudo ldconfig
Debug du chargeur :

Code: Select all

LD_DEBUG=libs ./prog
Précharger :

Code: Select all

LD_PRELOAD=./libhook.so ./prog

43. API essentielles du chapitre 42

Code: Select all

dlopen()
    -> charge une bibliothèque

dlsym()
    -> récupère l'adresse d'un symbole

dlerror()
    -> récupère une erreur dl*

dlclose()
    -> libère une référence sur une bibliothèque

dladdr()
    -> obtient des informations à partir d'une adresse
Flags essentiels :

Code: Select all

RTLD_LAZY
RTLD_NOW
RTLD_GLOBAL
RTLD_LOCAL
À connaître ensuite :

Code: Select all

RTLD_NOLOAD
RTLD_NODELETE
RTLD_DEEPBIND
RTLD_DEFAULT
RTLD_NEXT

44. À retenir absolument
  • .a = bibliothèque statique.
  • .so = bibliothèque partagée.
  • -fPIC produit du code indépendant de sa position.
  • -shared crée une bibliothèque partagée.
  • -lxxx cherche libxxx.so ou libxxx.a.
  • -L ajoute un répertoire de recherche au linker.
  • ld-linux charge les bibliothèques au runtime.
  • DT_NEEDED stocke les dépendances ELF.
  • soname représente la version ABI majeure d'une bibliothèque.
  • ldconfig gère le cache et les liens des bibliothèques système.
  • LD_LIBRARY_PATH modifie les chemins de recherche au runtime.
  • RPATH / RUNPATH stockent des chemins directement dans l'ELF.
  • $ORIGIN permet un chemin relatif à l'exécutable ou à l'objet.
  • dlopen() charge une .so à la demande.
  • dlsym() récupère une fonction ou variable par son nom.
  • dlerror() permet de diagnostiquer les erreurs.
  • dlclose() retire une référence à la bibliothèque.
  • RTLD_NOW résout immédiatement.
  • RTLD_LAZY peut différer la résolution des fonctions.
  • RTLD_GLOBAL rend les symboles disponibles aux objets chargés ensuite.
  • RTLD_LOCAL limite leur visibilité.
  • LD_PRELOAD permet l'interposition de symboles.
  • LD_DEBUG permet d'observer le travail du linker dynamique.
  • version scripts permettent de contrôler les symboles exportés et leur version.
  • constructor/destructor exécutent automatiquement du code lors du chargement/déchargement.
Idée centrale :

Une bibliothèque partagée n'est pas simplement "un fichier contenant des fonctions".

Elle fait partie d'un mécanisme complet impliquant :

Code: Select all

ELF
+
linker
+
dynamic linker
+
PIC
+
relocations
+
symboles
+
ABI
+
soname
+
chemins de recherche
+
chargement dynamique
Comprendre ce mécanisme est indispensable pour comprendre en profondeur comment Linux construit, charge et exécute les programmes.

Who is online

Users browsing this forum: No registered users and 1 guest