Skip to content

Latest commit

 

History

History
585 lines (389 loc) · 27.2 KB

File metadata and controls

585 lines (389 loc) · 27.2 KB

🔝 Retour au Sommaire

2.5.3 Dépendances dynamiques et résolution

Niveau : Débutant
Prérequis : Section 2.5.1 (Compilation étape par étape), Section 2.5.2 (Inspection des binaires)
Objectifs : Comprendre le mécanisme complet de résolution des librairies partagées sur Linux, maîtriser les outils de diagnostic (ldd, ldconfig, LD_LIBRARY_PATH, LD_DEBUG), et savoir résoudre les problèmes courants de dépendances dynamiques.


Dans la section précédente, nous avons vu que ldd liste les librairies partagées dont dépend un exécutable, et que readelf -d affiche les entrées NEEDED inscrites dans le binaire. Mais une question fondamentale reste ouverte : comment Linux trouve-t-il concrètement ces librairies au moment du lancement ?

Cette question n'a rien d'académique. Dès que vous quitterez le territoire confortable des librairies système installées par apt, vous rencontrerez des situations où un binaire refuse de se lancer parce qu'il ne trouve pas une librairie, ou pire — il en charge une mauvaise version. Comprendre le mécanisme de résolution, c'est savoir diagnostiquer et corriger ces situations en quelques minutes plutôt qu'en quelques heures.


Le dynamic linker : chef d'orchestre du chargement

Quand vous exécutez ./main, le noyau Linux ne passe pas directement le contrôle à votre code. Il lit l'en-tête ELF du binaire, y trouve le champ INTERP qui désigne le dynamic linker (aussi appelé runtime linker, loader, ou interpréteur ELF), et c'est à lui qu'il confie l'exécution.

readelf -l main | grep interpreter
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

Le dynamic linker /lib64/ld-linux-x86-64.so.2 (qui est lui-même un composant de la glibc) effectue alors les opérations suivantes dans cet ordre :

  1. Chargement : il lit les entrées NEEDED du binaire et localise chaque librairie partagée sur le système de fichiers.
  2. Mapping mémoire : il mappe chaque librairie dans l'espace d'adressage virtuel du processus avec mmap.
  3. Résolution des symboles : il résout les références croisées entre le binaire et les librairies (et entre les librairies elles-mêmes, car elles peuvent dépendre les unes des autres).
  4. Relocations : il ajuste les adresses dans le code et les données pour refléter les positions de chargement effectives.
  5. Initialisation : il exécute les fonctions d'initialisation des librairies (constructeurs globaux C++, fonctions marquées __attribute__((constructor))).
  6. Transfert de contrôle : il appelle _start (le point d'entrée du runtime C), qui initialise l'environnement puis appelle votre main().

Tout cela se produit avant la première ligne de votre main(). Sur un programme avec de nombreuses dépendances, ce processus peut prendre un temps mesurable — c'est l'un des arguments en faveur du linkage statique pour les petits utilitaires en ligne de commande.


L'algorithme de recherche des librairies

Quand le dynamic linker doit localiser une librairie (par exemple libstdc++.so.6), il consulte plusieurs sources dans un ordre précis et déterministe. Comprendre cet ordre est essentiel pour le diagnostic.

1. DT_RPATH (déprécié)

Le binaire peut contenir un champ DT_RPATH inscrit au moment de la compilation. C'est un chemin (ou une liste de chemins séparés par :) codé en dur dans l'exécutable :

readelf -d main | grep RPATH

DT_RPATH est un mécanisme ancien, aujourd'hui déprécié au profit de DT_RUNPATH. La différence cruciale est que DT_RPATH est consulté avant LD_LIBRARY_PATH, ce qui empêche l'utilisateur de rediriger vers une autre librairie. C'est considéré comme un comportement trop rigide, d'où la préférence pour DT_RUNPATH.

Pour notre programme simple, cette commande ne retourne rien : aucun chemin personnalisé n'est inscrit.

2. LD_LIBRARY_PATH (variable d'environnement)

La variable d'environnement LD_LIBRARY_PATH contient une liste de répertoires séparés par : dans lesquels le dynamic linker cherche les librairies avant de consulter le cache système :

export LD_LIBRARY_PATH=/opt/mylibs/lib:/home/user/libs
./main

C'est le mécanisme le plus rapide à utiliser pour le développement et le dépannage. Mais il a des inconvénients sérieux qui le rendent inadapté à un usage permanent. Il affecte tous les programmes lancés dans le même environnement, pas seulement le vôtre. Il peut provoquer des conflits si une librairie dans votre chemin personnalisé masque une librairie système. Et il est ignoré pour les binaires setuid/setgid (pour des raisons de sécurité évidentes).

En pratique, LD_LIBRARY_PATH est un outil de développement et de test. Pour la production, on préfère DT_RUNPATH ou l'installation dans un chemin système.

3. DT_RUNPATH (recommandé)

Comme DT_RPATH, DT_RUNPATH est un chemin inscrit dans le binaire au moment de la compilation, mais il est consulté après LD_LIBRARY_PATH, ce qui laisse à l'utilisateur la possibilité de rediriger au besoin :

# Compiler avec un RUNPATH
g++ main.o mathutils.o -o main -Wl,-rpath,/opt/mylibs/lib

# Vérifier
readelf -d main | grep RUNPATH
 0x000000000000001d (RUNPATH)            Library runpath: [/opt/mylibs/lib]

Le token spécial $ORIGIN est particulièrement utile : il est remplacé à l'exécution par le répertoire contenant l'exécutable. Cela permet de créer des distributions autonomes où les librairies sont livrées à côté du binaire :

g++ main.o mathutils.o -o main -Wl,-rpath,'$ORIGIN/lib'

Avec cette configuration, si l'exécutable est dans /opt/monapp/bin/main, le dynamic linker cherchera les librairies dans /opt/monapp/bin/lib/. C'est un pattern très courant pour les applications distribuées hors des gestionnaires de paquets.

4. Le cache ld.so.cache

Si la librairie n'a pas été trouvée dans les étapes précédentes, le dynamic linker consulte le fichier /etc/ld.so.cache. Ce fichier binaire est un index qui associe chaque nom de librairie à son chemin complet sur le système. Il est généré par la commande ldconfig à partir des répertoires configurés.

# Afficher le contenu du cache sous forme lisible
ldconfig -p | head -20
1847 libs found in cache `/etc/ld.so.cache'
    libz.so.1 (libc6,x86-64) => /lib/x86_64-linux-gnu/libz.so.1
    libxml2.so.2 (libc6,x86-64) => /lib/x86_64-linux-gnu/libxml2.so.2
    ...
# Chercher une librairie spécifique
ldconfig -p | grep libstdc++
    libstdc++.so.6 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstdc++.so.6

Le cache est reconstruit à partir de la configuration stockée dans /etc/ld.so.conf et les fichiers du répertoire /etc/ld.so.conf.d/ :

cat /etc/ld.so.conf  
ls /etc/ld.so.conf.d/  
cat /etc/ld.so.conf.d/*.conf  

Chaque fichier .conf contient un ou plusieurs chemins de répertoires. Quand une librairie est installée dans un chemin non standard, il faut ajouter ce chemin et régénérer le cache :

# Exemple : ajouter un chemin personnalisé
echo "/opt/mylibs/lib" | sudo tee /etc/ld.so.conf.d/mylibs.conf  
sudo ldconfig  

Après cette opération, les librairies de /opt/mylibs/lib sont indexées dans le cache et trouvées automatiquement par le dynamic linker.

5. Les chemins par défaut

En dernier recours, le dynamic linker cherche dans les chemins compilés en dur dans la glibc. Sur un système Ubuntu 64 bits, ces chemins sont typiquement /lib/x86_64-linux-gnu, /usr/lib/x86_64-linux-gnu, /lib, et /usr/lib. Ces répertoires contiennent les librairies installées par les paquets système et sont toujours consultés, même si le cache est corrompu ou absent.

Récapitulatif de l'ordre de recherche

Priorité Source Modifiable par Persistant
1 DT_RPATH (déprécié) Développeur (à la compilation) Oui (inscrit dans le binaire)
2 LD_LIBRARY_PATH Utilisateur (variable d'environnement) Non (durée du shell)
3 DT_RUNPATH Développeur (à la compilation) Oui (inscrit dans le binaire)
4 /etc/ld.so.cache Administrateur (ldconfig) Oui (fichier système)
5 Chemins par défaut Compilé dans la glibc Oui (fixe)

⚠️ Subtilité : si DT_RUNPATH est présent, DT_RPATH est ignoré. Les deux ne coexistent pas fonctionnellement. Les linkers modernes (GCC avec -Wl,-rpath) produisent DT_RUNPATH par défaut.


Le versioning des librairies partagées (SONAME)

Sur Linux, les librairies partagées suivent une convention de nommage à trois niveaux qui permet la coexistence de versions multiples et les mises à jour sans casser les binaires existants.

Prenons l'exemple de libstdc++ :

ls -la /lib/x86_64-linux-gnu/libstdc++*
libstdc++.so.6 -> libstdc++.so.6.0.33  
libstdc++.so.6.0.33  

Trois noms sont en jeu. Le real name (libstdc++.so.6.0.33) est le fichier physique contenant le code. Les numéros 6.0.33 suivent le schéma majeur.mineur.patch. Le SONAME (libstdc++.so.6) est un lien symbolique vers le real name. C'est le nom inscrit dans le champ NEEDED de vos exécutables. Le numéro majeur (6) ne change que lors d'une rupture d'ABI. Le linker name (libstdc++.so, sans numéro de version) est un lien symbolique utilisé uniquement au moment de la compilation (quand vous écrivez -lstdc++). Il n'est généralement présent que dans les paquets -dev.

Vérifions le SONAME inscrit dans la librairie elle-même :

readelf -d /lib/x86_64-linux-gnu/libstdc++.so.6 | grep SONAME
 0x000000000000000e (SONAME)             Library soname: [libstdc++.so.6]

Ce mécanisme permet les mises à jour transparentes. Si une nouvelle version libstdc++.so.6.0.34 est installée, le lien symbolique libstdc++.so.6 est mis à jour par ldconfig, et tous les binaires compilés contre le SONAME libstdc++.so.6 bénéficient automatiquement de la nouvelle version — sans recompilation, à condition que l'ABI n'ait pas changé (même numéro majeur).

En revanche, si une librairie passe de libfoo.so.2 à libfoo.so.3 (changement de majeur), les anciens binaires continuent de chercher libfoo.so.2, et les deux versions peuvent coexister sur le même système. C'est un avantage fondamental du linkage dynamique sur Linux.


Version requirements de la glibc

Au-delà du SONAME, la glibc utilise un mécanisme de symbol versioning plus fin. Chaque symbole exporté par la glibc est associé à la version dans laquelle il est apparu (par exemple GLIBC_2.17, GLIBC_2.34). Un exécutable enregistre la version la plus récente des symboles qu'il utilise.

# Voir les versions requises
readelf -V main | grep -A 3 'Version needs'

Ou, plus simplement :

objdump -T main | grep GLIBC | sed 's/.*GLIBC_/GLIBC_/' | sort -Vu

Si vous compilez sur Ubuntu 24.04 (glibc 2.39) et tentez d'exécuter le binaire sur un serveur CentOS 7 (glibc 2.17), le dynamic linker refusera de charger le programme si celui-ci utilise des symboles apparus après la version 2.17. Le message d'erreur ressemble à :

./main: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by ./main)

Trois stratégies existent pour gérer cette situation. La première est de compiler sur un système avec une glibc aussi ancienne que la cible la plus ancienne que vous visez — c'est pourquoi les images Docker de build utilisent souvent des distributions volontairement anciennes. La deuxième est de lier statiquement la libc (avec -static), au prix de la taille et de certaines limitations. La troisième est d'utiliser des outils comme zig cc ou des images de build dédiées qui permettent de cibler une version spécifique de la glibc.

📎 La section 37 (Dockerisation) et la section 38.6 (Cross-compilation) traitent de ces stratégies dans un contexte DevOps.


LD_DEBUG : Tracer le dynamic linker

Quand un problème de résolution se présente, LD_DEBUG est l'outil de diagnostic le plus puissant. Cette variable d'environnement active le mode verbose du dynamic linker, qui affiche sur stderr chaque étape de son travail.

Tracer la recherche des librairies

LD_DEBUG=libs ./main 2>&1 | head -40

La sortie montre, pour chaque librairie NEEDED, les répertoires dans lesquels le dynamic linker cherche et le résultat :

     find library=libstdc++.so.6 [0]; searching
      search cache=/etc/ld.so.cache
       trying file=/lib/x86_64-linux-gnu/libstdc++.so.6
     find library=libm.so.6 [0]; searching
      search cache=/etc/ld.so.cache
       trying file=/lib/x86_64-linux-gnu/libm.so.6
     ...

On voit clairement que le cache est consulté en premier, et que chaque librairie est trouvée du premier coup grâce à l'index de ld.so.cache. Si une librairie n'était pas dans le cache, vous verriez le linker essayer les chemins par défaut un par un.

Tracer la résolution des symboles

LD_DEBUG=symbols ./main 2>&1 | head -60

Cette variante montre la résolution de chaque symbole : quel binaire le demande, dans quelle librairie il est trouvé. C'est verbeux (des milliers de lignes pour un programme simple), mais précieux quand un symbole est résolu dans une librairie inattendue.

Tracer les bindings (résolution finale)

LD_DEBUG=bindings ./main 2>&1 | head -40

Le mode bindings affiche le résultat final de chaque résolution : le symbole X du fichier A est lié à la définition dans le fichier B à l'adresse Y.

Tracer les relocations

LD_DEBUG=reloc ./main 2>&1 | head -40

Affiche les opérations de relocation — les ajustements d'adresses effectués une fois les librairies chargées à leur position mémoire effective.

Toutes les traces

LD_DEBUG=all ./main 2>&1 | head -100

Le mode all combine toutes les catégories. C'est souvent trop verbeux pour être lu directement, mais utile quand on redirige vers un fichier pour une analyse ciblée :

LD_DEBUG=all ./main 2> ld_debug.log  
grep "not found" ld_debug.log  
grep "libstdc++" ld_debug.log | head -10  

Liste des catégories disponibles

Pour voir toutes les catégories de traçage supportées :

LD_DEBUG=help ./main
Valid options for the LD_DEBUG environment variable are:

  libs        display library search paths
  reloc       display relocation processing
  files       display progress for input file
  symbols     display symbol table processing
  bindings    display information about symbol binding
  versions    display version dependencies
  scopes      display scope information
  all         all previous options combined
  statistics  display relocation statistics
  unused      determined unused DSOs
  help        display this help message and exit

La catégorie unused est particulièrement intéressante pour l'optimisation : elle identifie les librairies chargées mais dont aucun symbole n'est réellement utilisé. Cela peut arriver quand une dépendance transitive est présente dans le binaire sans être nécessaire.

LD_DEBUG=unused ./main 2>&1 | grep unused

Diagnostic des problèmes courants

Problème 1 : cannot open shared object file

C'est l'erreur la plus fréquente liée aux dépendances dynamiques :

./main: error while loading shared libraries: libexample.so.1:
cannot open shared object file: No such file or directory

La démarche de diagnostic suit un enchaînement logique. Commencez par vérifier que la librairie existe sur le système :

# Recherche sur tout le système de fichiers
find / -name "libexample*" 2>/dev/null

# Ou via le gestionnaire de paquets
dpkg -S libexample 2>/dev/null  
apt-file search libexample  

Si la librairie n'est pas installée, il faut installer le paquet correspondant. Sur Ubuntu, les librairies de développement sont généralement dans des paquets suffixés -dev :

sudo apt install libexample-dev

Si la librairie est présente sur le disque mais pas trouvée par le dynamic linker, vérifiez le cache :

ldconfig -p | grep libexample

Si elle n'apparaît pas dans le cache, son répertoire n'est pas configuré. Deux options s'offrent à vous selon le contexte.

Solution temporaire (développement) :

LD_LIBRARY_PATH=/chemin/vers/lib ./main

Solution permanente (production) :

echo "/chemin/vers/lib" | sudo tee /etc/ld.so.conf.d/example.conf  
sudo ldconfig  

Problème 2 : Mauvaise version chargée

Parfois, une librairie est trouvée mais ce n'est pas la bonne version. Le programme se lance mais plante avec des erreurs mystérieuses, ou affiche un comportement inattendu. Utilisez ldd pour vérifier exactement quelle version est chargée :

ldd main | grep libexample

Si le chemin résolu ne correspond pas à vos attentes, utilisez LD_DEBUG=libs pour comprendre d'où vient la résolution. Le problème est souvent un LD_LIBRARY_PATH hérité d'un autre contexte, ou un lien symbolique qui pointe vers la mauvaise version.

Problème 3 : symbol lookup error / undefined symbol

./main: symbol lookup error: ./main: undefined symbol: _Z11foo_functionv

Contrairement à l'erreur undefined reference qui survient au moment du linkage (étape 4 de la compilation), cette erreur survient au moment de l'exécution. Elle signifie que la librairie a été trouvée et chargée, mais qu'elle ne contient pas le symbole attendu. La cause la plus fréquente est un décalage de version : le programme a été compilé contre une version de la librairie qui exportait ce symbole, mais la version installée sur le système est différente.

Le diagnostic consiste à vérifier que le symbole est bien présent dans la librairie chargée :

nm -CD /lib/x86_64-linux-gnu/libexample.so | grep foo_function

Si le symbole est absent, c'est une incompatibilité de version. Si le symbole est présent mais avec une signature différente (mangling différent), c'est une incompatibilité d'ABI — le programme et la librairie ont été compilés avec des déclarations différentes de la même fonction.

Problème 4 : GLIBC_X.XX not found

./main: /lib64/libc.so.6: version `GLIBC_2.34' not found (required by ./main)

Ce message indique que votre binaire a été compilé sur un système avec une glibc plus récente que celle de la machine cible. Il n'existe pas de solution simple sur la machine cible : on ne peut pas facilement mettre à jour la glibc sans mettre à jour la distribution entière.

Vérifiez la version de la glibc installée :

ldd --version

Et comparez avec les versions requises par votre binaire :

objdump -T main | grep GLIBC | sed 's/.*GLIBC_/GLIBC_/' | sort -Vu

La solution est de recompiler le binaire sur un système dont la glibc est au moins aussi ancienne que la cible la plus contrainte de votre parc de déploiement.


Linkage statique vs dynamique : Synthèse pratique

Pour boucler cette section, comparons concrètement les deux modes de linkage sur notre programme :

# Linkage dynamique (par défaut)
g++ main.cpp mathutils.cpp -o main_dynamic

# Linkage statique
g++ -static main.cpp mathutils.cpp -o main_static
# Comparaison des tailles
ls -lh main_dynamic main_static
-rwxr-xr-x 1 user user   22K  ... main_dynamic
-rwxr-xr-x 1 user user  2.4M  ... main_static

La différence de taille est spectaculaire : le binaire statique embarque l'intégralité de libstdc++, libm, et libc. En contrepartie, il est entièrement autonome :

ldd main_static
    not a dynamic executable
file main_static
main_static: ELF 64-bit LSB executable, x86-64, version 1 (GNU),  
statically linked, BuildID[sha1]=..., for GNU/Linux 3.2.0, not stripped  

Chaque approche a ses cas d'usage légitimes :

Le linkage dynamique est le choix par défaut et convient à la majorité des situations. Les librairies partagées sont chargées une seule fois en mémoire même si plusieurs processus les utilisent, les correctifs de sécurité dans une librairie partagée bénéficient automatiquement à tous les programmes qui l'utilisent, et la taille de chaque binaire reste minimale.

Le linkage statique est pertinent pour les petits utilitaires en ligne de commande que l'on veut distribuer sous forme d'un unique fichier sans aucune dépendance, pour les conteneurs Docker minimalistes (images scratch ou distroless) où aucune librairie partagée n'est disponible, et pour les environnements où la cohérence exacte des dépendances est critique (certains contextes embarqués ou de sécurité).

📎 La section 27.4 approfondit le choix entre linkage statique et dynamique dans le contexte de la gestion des dépendances d'un projet. La section 37.5 traite des images Docker distroless qui favorisent le linkage statique.


Invoquer le dynamic linker directement

Une technique méconnue mais utile : on peut invoquer le dynamic linker comme un programme à part entière, en lui passant un exécutable en argument. Cela permet de tester la résolution sans exécuter le main() du programme :

/lib64/ld-linux-x86-64.so.2 --list ./main

Cette commande produit un résultat similaire à ldd, mais de manière plus directe. Elle est utile dans des environnements où ldd n'est pas disponible (conteneurs minimaux, par exemple).

On peut aussi obtenir des informations sur le dynamic linker lui-même :

/lib64/ld-linux-x86-64.so.2 --help

L'option --inhibit-rpath permet de désactiver le RPATH/RUNPATH d'un binaire pour tester la résolution sans ces chemins :

/lib64/ld-linux-x86-64.so.2 --inhibit-rpath "" ./main

Chargement à la demande : dlopen et dlsym

Jusqu'ici, toutes les dépendances étaient déclarées dans le binaire et résolues automatiquement au lancement. Linux offre aussi la possibilité de charger une librairie partagée à l'exécution, de manière programmatique, avec les fonctions dlopen et dlsym. C'est ce qu'on appelle le chargement dynamique explicite (par opposition au chargement dynamique implicite vu jusqu'ici).

Voici un aperçu du mécanisme :

// exemple_dlopen.cpp
#include <dlfcn.h>
#include <iostream>

int main() {
    // Charger la librairie mathématique à l'exécution
    void* handle = dlopen("libm.so.6", RTLD_LAZY);
    if (!handle) {
        std::cerr << "Erreur dlopen : " << dlerror() << std::endl;
        return 1;
    }

    // Chercher le symbole sqrt
    using sqrt_fn = double(*)(double);
    auto my_sqrt = reinterpret_cast<sqrt_fn>(dlsym(handle, "sqrt"));
    if (!my_sqrt) {
        std::cerr << "Erreur dlsym : " << dlerror() << std::endl;
        dlclose(handle);
        return 1;
    }

    std::cout << "sqrt(144) = " << my_sqrt(144.0) << std::endl;

    dlclose(handle);
    return 0;
}
g++ exemple_dlopen.cpp -o exemple_dlopen -ldl
./exemple_dlopen

L'option -ldl lie la librairie libdl, qui fournit dlopen, dlsym, dlclose et dlerror. Le flag RTLD_LAZY indique que les symboles sont résolus au moment de leur premier appel (résolution paresseuse), par opposition à RTLD_NOW qui résout tout au moment du dlopen.

Ce mécanisme est utilisé dans plusieurs contextes : les systèmes de plugins (charger des extensions sans recompiler le programme principal), les wrappers de librairies optionnelles (utiliser une fonctionnalité si la librairie est disponible, offrir une alternative sinon), et les tests (injecter des mocks au runtime).

💡 Ce code utilise reinterpret_cast et des pointeurs bruts, des techniques que nous couvrirons en sections 3.3.2 et 5.3. À ce stade, l'objectif est de montrer le mécanisme, pas d'en maîtriser chaque détail syntaxique.


Lazy binding et PLT/GOT

Par défaut, le dynamic linker utilise le lazy binding (résolution paresseuse) : les adresses des fonctions de librairies partagées ne sont résolues que lors de leur premier appel, pas au chargement du programme. Ce mécanisme repose sur deux structures présentes dans le binaire ELF.

La PLT (Procedure Linkage Table) contient un petit bout de code pour chaque fonction externe. Lors du premier appel, ce code redirige vers le dynamic linker pour résoudre l'adresse réelle, puis modifie la GOT pour que les appels suivants aillent directement à la bonne adresse.

La GOT (Global Offset Table) est une table d'adresses en mémoire inscriptible. Initialement, chaque entrée pointe vers le code de résolution dans la PLT. Après résolution, elle pointe directement vers la fonction cible.

On peut observer ces structures :

objdump -d -j .plt -C main | head -30  
readelf -r main | grep JUMP_SLOT | head -10  

Le lazy binding améliore le temps de démarrage (seules les fonctions réellement appelées sont résolues), mais introduit un coût au premier appel de chaque fonction. Pour les applications où le temps de démarrage importe moins que la latence du premier appel, on peut forcer la résolution immédiate de tous les symboles :

LD_BIND_NOW=1 ./main

Ou à la compilation :

g++ main.o mathutils.o -o main -Wl,-z,now

L'option -z,now est aussi une mesure de sécurité : en résolvant tout au démarrage, la GOT peut être marquée en lecture seule après l'initialisation (technique appelée RELRO complet — Relocation Read-Only), ce qui empêche un attaquant de détourner des appels de fonctions en écrivant dans la GOT.

# Vérifier si RELRO est activé
readelf -l main | grep GNU_RELRO  
readelf -d main | grep BIND_NOW  

📎 La section 45.4 (Compilation avec protections) détaille RELRO, ASLR, PIE et les autres mécanismes de sécurité à la compilation.


En résumé

Le mécanisme de résolution des dépendances dynamiques est un système en couches, avec un ordre de priorité bien défini : DT_RPATH, LD_LIBRARY_PATH, DT_RUNPATH, ld.so.cache, et enfin les chemins par défaut. Connaître cet ordre permet de comprendre immédiatement pourquoi une librairie est (ou n'est pas) trouvée.

Le versioning SONAME garantit la coexistence de versions multiples et les mises à jour transparentes, tandis que le symbol versioning de la glibc peut causer des incompatibilités entre distributions.

LD_DEBUG est l'outil de diagnostic ultime : il rend visible chaque décision du dynamic linker et élimine les conjectures.

Le lazy binding via PLT/GOT optimise le temps de démarrage mais peut être désactivé au profit de la sécurité (RELRO complet) ou de la prédictibilité des performances.

Enfin, le choix entre linkage statique et dynamique n'est pas binaire dans la pratique. De nombreux projets adoptent une approche hybride : linkage dynamique pour les librairies système stables (libc, libstdc++) et linkage statique pour les dépendances spécifiques au projet dont on veut contrôler exactement la version.

Prochaine section : 2.6 — Options de compilation critiques, où nous explorerons les flags qui transforment la qualité de la compilation : warnings, optimisation, débogage, et sélection du standard C++.

⏭️ Options de compilation critiques