Introduction : Les maîtres invisibles de l’exécution logicielle
Chaque jour, les développeurs lancent des commandes telles que :
| |
On qualifie souvent ce processus de simple « compilation ». Pourtant, le compilateur ne réalise que la moitié du travail. La seconde moitié—assembler harmonieusement des dizaines de fichiers objets et de bibliothèques, calculer les adresses virtuelles et charger l’exécutable en mémoire—est assurée par deux piliers du système : l’éditeur de liens (Linker) et le chargeur (Loader).
flowchart TD
subgraph BuildPipeline ["Du code source à l'exécution"]
SourceA["main.c"] -->|Compilateur| ObjA["main.o (Fichier objet)"]
SourceB["utils.c"] -->|Compilateur| ObjB["utils.o (Fichier objet)"]
Lib["libc.so / libm.a\n(Bibliothèques)"]
ObjA --> Linker["★ Éditeur de liens (ld)\n[Résolution de symboles & Relocalisation]"]
ObjB --> Linker
Lib --> Linker
Linker --> Exec["myprogram (Binaire exécutable ELF)"]
end
Exec -->|execve| Loader["★ Chargeur (Noyau & ld-linux.so)\n[Mappage mémoire & Liaison dynamique]"]
Loader --> CPU["Exécution CPU (_start -> main)"]
style BuildPipeline fill:#f9f9f9,stroke:#333,stroke-width:2px
style Linker fill:#ffe6e6,stroke:#d9534f,stroke-width:2px
style Loader fill:#e6f3ff,stroke:#0066cc,stroke-width:2px
Sans éditeur de liens, la programmation modulaire serait impossible : nous devrions écrire des millions de lignes de code dans un fichier unique.
1. Le format ELF : Anatomie d’un fichier objet
Un fichier intermédiaire (.o) est un fichier objet relocalisable au format ELF (Executable and Linkable Format) :
.text: Code machine exécutable..rodata: Données constantes en lecture seule..data: Variables globales et statiques initialisées..bss: Variables globales non initialisées. Ne consomme aucun octet sur le disque ! La mémoire est allouée et mise à zéro à l’exécution..symtab: Table des symboles (fonctions et variables)..rela.text: Table de relocalisation indiquant les adresses temporaires à corriger.
2. Résolution des symboles (Symbol Resolution)
L’éditeur de liens associe chaque référence externe à sa définition réelle.
Symboles forts et faibles :
- Fort : Définitions de fonctions et variables globales initialisées (
int x = 1;). - Faible : Variables globales non initialisées (
int x;).
Règles d’arbitrage :
- Deux symboles forts identiques provoquent une erreur (
multiple definition). - Un symbole fort l’emporte toujours sur un symbole faible.
- Entre plusieurs symboles faibles, le linker en choisit un arbitrairement (source de bogues sournois).
Ordre des bibliothèques statiques : L’éditeur de liens analyse la ligne de commande de gauche à droite en une seule passe. C’est pourquoi -lm doit obligatoirement figurer après main.c.
3. Relocalisation (Relocation)
Le compilateur place des zéros provisoires pour les fonctions externes :
| |
($S$ : adresse du symbole, $A$ : addend, $P$ : position de l’instruction).
4. Liaison dynamique : PIC, GOT et PLT
Pour éviter de dupliquer les bibliothèques sur disque et en mémoire, les systèmes modernes utilisent des bibliothèques partagées (.so).
Elles nécessitent du Code Indépendant de la Position (PIC) grâce à deux tables :
- GOT (Global Offset Table) : Stocke les adresses réelles en mémoire.
- PLT (Procedure Linkage Table) : Table de saut dans la section code.
Grâce à la liaison paresseuse (Lazy Binding), l’adresse d’une fonction n’est résolue par le lieur dynamique (ld-linux.so) que lors de son tout premier appel.
5. Le chargeur : De _start à main()
Lors de l’exécution, l’appel système execve mappe les segments ELF en mémoire virtuelle (mmap), configure la pile (argc, argv, envp) et saute vers _start.
La fonction d’initialisation de la libc exécute les constructeurs globaux dans .init_array avant d’appeler enfin la fonction main().
