Trois acteurs, souvent confondus, interviennent entre le code source et un programme qui s’exécute réellement : le linker, le loader, et le loader dynamique. Leurs tâches se chevauchent — tous trois s’occupent, d’une manière ou d’une autre, de résolution d’adresses — mais ils n’interviennent ni au même moment, ni avec la même portée.
Le linker (éditeur de liens)
Le linker combine plusieurs morceaux de code et de données en un unique exécutable chargeable en mémoire. Cette édition de liens peut avoir lieu à trois moments distincts : à la compilation, au chargement, ou à l’exécution — c’est précisément cette dernière modalité qui donne naissance au loader dynamique.
Ses deux tâches fondamentales sont la résolution de symboles, qui consiste à localiser chaque référence externe et à corriger le code de l’appelant en conséquence, et la relocalisation : le compilateur et l’assembleur produisent un code objet dont les adresses partent conventionnellement de zéro, et c’est le linker qui leur assigne leurs adresses de chargement définitives, en fusionnant les sections de même type et en ajustant chaque référence pour qu’elle pointe où il faut.
Le loader (chargeur)
Le loader effectue le chargement proprement dit du programme : copier l’image du programme depuis le disque vers la mémoire principale, dans un état prêt à l’exécution — ce qui peut impliquer l’allocation de stockage ou le mappage d’adresses virtuelles vers des pages disque. Sous Linux, c’est le shell qui invoque le loader, via execve, pour copier code et données en mémoire puis transférer le contrôle à la première instruction du programme.
Le loader dynamique : lier au moment de l’exécution
Le loader dynamique (ou lieur d’exécution, runtime linker) est le composant chargé de lier les bibliothèques partagées au moment de l’exécution, plutôt qu’à la compilation. Pour la plupart des programmes liés dynamiquement, le noyau détecte sa nécessité via l’entrée PT_INTERP de l’en-tête ELF, qui désigne le lieur d’exécution requis (typiquement /lib64/ld-linux-x86-64.so.2).
Le déroulé est le suivant : le gestionnaire ELF du noyau charge l’interpréteur en mémoire, lui passe le contrôle ; le lieur dynamique mappe les bibliothèques partagées requises dans l’espace d’adressage du programme, relocalise leur code et leurs données, résout les références de symboles de l’exécutable principal envers ces bibliothèques, puis transfère enfin le contrôle à l’application.
Ce qui distingue les trois
Le linker statique résout symboles et relocalisations avant l’exécution, en produisant un exécutable complet. Le loader charge cet exécutable en mémoire. Le loader dynamique fait, lui, une partie du travail du linker — résolution de symboles, chargement de bibliothèques — mais pendant le chargement ou l’exécution, pour gérer ce que le linker statique n’a pas pu figer à l’avance.
Le rôle central dans GOT et PLT
C’est dans la résolution des adresses via la GOT et la PLT que le loader dynamique prend tout son sens pour l’exploitation. Au premier appel d’une fonction de bibliothèque, le code passe par la PLT (.plt), qui redirige initialement vers une entrée de la GOT (.got.plt) pointant elle-même vers la PLT — déclenchant l’invocation du loader dynamique. Celui-ci localise la fonction réelle dans la bibliothèque chargée et écrit son adresse dans la GOT ; les appels suivants utilisent directement ce pointeur mis à jour, sans repasser par la résolution. C’est le principe du lazy binding : ne résoudre une fonction que lorsqu’elle sert vraiment, pour économiser des cycles au démarrage.
Cette écriture différée est précisément ce qui ouvre, avant RELRO, la voie au GOT overwrite : puisque la GOT doit rester inscriptible pour que le loader dynamique puisse y écrire, une vulnérabilité d’écriture arbitraire permet à un attaquant de substituer sa propre adresse à celle d’une fonction légitime.
Le linker statique est l’architecte qui planifie où ira chaque pièce du bâtiment avant la construction. Le loader est l’équipe qui assemble les murs sur le site. Le loader dynamique est le chef de chantier qui, à mesure que les occupants arrivent — les appels de fonctions —, trouve l’adresse des services externes et met à jour un panneau d’affichage central — la GOT — pour que chacun sache immédiatement où aller la fois suivante, sans avoir à rechercher l’adresse de nouveau.