Featured image of post Comment des programmes séparés ne font plus qu'un : Les secrets de l'éditeur de liens et du chargeur

Comment des programmes séparés ne font plus qu'un : Les secrets de l'éditeur de liens et du chargeur

Introduction : Les maîtres invisibles de l’exécution logicielle

Chaque jour, les développeurs lancent des commandes telles que :

1
gcc -O2 main.c utils.c math.c -o myprogram

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 :

  1. Deux symboles forts identiques provoquent une erreur (multiple definition).
  2. Un symbole fort l’emporte toujours sur un symbole faible.
  3. 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 :

1
callq 0x00000000  # Déplacement temporaire
$$\text{Valeur} = S + A - P$$

($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().

comments powered by Disqus