🔝 Retour au Sommaire
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.
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 :
- Chargement : il lit les entrées
NEEDEDdu binaire et localise chaque librairie partagée sur le système de fichiers. - Mapping mémoire : il mappe chaque librairie dans l'espace d'adressage virtuel du processus avec
mmap. - 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).
- Relocations : il ajuste les adresses dans le code et les données pour refléter les positions de chargement effectives.
- Initialisation : il exécute les fonctions d'initialisation des librairies (constructeurs globaux C++, fonctions marquées
__attribute__((constructor))). - Transfert de contrôle : il appelle
_start(le point d'entrée du runtime C), qui initialise l'environnement puis appelle votremain().
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.
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.
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 RPATHDT_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.
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
./mainC'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.
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.
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 -201847 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.
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.
| 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é : siDT_RUNPATHest présent,DT_RPATHest ignoré. Les deux ne coexistent pas fonctionnellement. Les linkers modernes (GCC avec-Wl,-rpath) produisentDT_RUNPATHpar défaut.
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.
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 -VuSi 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.
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.
LD_DEBUG=libs ./main 2>&1 | head -40La 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.
LD_DEBUG=symbols ./main 2>&1 | head -60Cette 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.
LD_DEBUG=bindings ./main 2>&1 | head -40Le 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.
LD_DEBUG=reloc ./main 2>&1 | head -40Affiche les opérations de relocation — les ajustements d'adresses effectués une fois les librairies chargées à leur position mémoire effective.
LD_DEBUG=all ./main 2>&1 | head -100Le 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 Pour voir toutes les catégories de traçage supportées :
LD_DEBUG=help ./mainValid 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 unusedC'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-devSi la librairie est présente sur le disque mais pas trouvée par le dynamic linker, vérifiez le cache :
ldconfig -p | grep libexampleSi 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 ./mainSolution permanente (production) :
echo "/chemin/vers/lib" | sudo tee /etc/ld.so.conf.d/example.conf
sudo ldconfig 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 libexampleSi 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.
./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_functionSi 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.
./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 --versionEt comparez avec les versions requises par votre binaire :
objdump -T main | grep GLIBC | sed 's/.*GLIBC_/GLIBC_/' | sort -VuLa 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.
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_staticmain_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.
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 ./mainCette 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 --helpL'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 "" ./mainJusqu'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_dlopenL'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_castet 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.
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 ./mainOu à la compilation :
g++ main.o mathutils.o -o main -Wl,-z,nowL'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.
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++.