C'est une façon d'obtenir des corrections de code (ajustant les adresses en fonction de l'emplacement du code dans la mémoire virtuelle, qui peut varier d'un processus à l'autre) sans avoir à maintenir une copie séparée du code pour chaque processus. Le PLT, ou table de liaison de procédure, est l'une des structures qui facilite l'utilisation du chargement et du liage dynamiques (une autre est la GOT, ou table des offsets globaux).
Consultez le diagramme suivant, qui montre à la fois votre code appelant et le code de bibliothèque (que vous appelez) cartographiés à différentes adresses virtuelles dans deux processus différents, A
et B
. Il n'y a qu'une seule copie de chaque morceau de code en mémoire réelle, les différentes adresses virtuelles à l'intérieur de chaque processus étant mappées sur cette adresse réelle) :
Processus A
Adresses (virtuelles):
0x1234 0x8888
+-------------+ +---------+ +---------+
| | | Privé | | |
| | | PLT/GOT | | |
| Partagé | +---------+ | Partagé |
===== application =============== bibliothèque =====
| code | +---------+ | code |
| | | Privé | | |
| | | PLT/GOT | | |
+-------------+ +---------+ +---------+
0x2020 0x6666
Processus B
Lorsque la bibliothèque partagée est introduite dans l'espace d'adressage, des entrées sont construites dans le PLT et/ou GOT spécifiques au processus qui, lors de la première utilisation, effectueront certaines corrections pour rendre les choses plus rapides. Les utilisations ultérieures contourneront ensuite la correction car elle ne sera plus nécessaire.
Le processus se déroule de la manière suivante.
printf@plt
est en réalité un petit segment qui (au final) appelle la vraie fonction printf
, en modifiant des choses en cours de route pour rendre les appels suivants plus rapides.
La vraie fonction printf
est cartographiée dans un emplacement arbitraire dans un processus donné (espace d'adresse virtuelle), tout comme le code qui essaie de l'appeler.
Ainsi, pour permettre le partage approprié du code appelant (côté gauche ci-dessus) et du code appelé (côté droit), vous ne pouvez pas appliquer de corrections au code appelant directement car cela "endommagerait" son fonctionnement dans les autres processus (ce qui n'aurait pas d'importance s'il était mappé à la même emplacement dans chaque processus mais c'est une restriction un peu ennuyeuse, surtout si autre chose avait déjà été mappé là-bas).
Ainsi, le PLT
est une zone plus petite et spécifique au processus à une adresse calculée de manière fiable à l'exécution qui n'est pas partagée entre les processus, de sorte que tout processus donné est libre de le modifier comme il le souhaite, sans effets indésirables sur les autres processus.
Examinons le processus un peu plus en détail. Le diagramme ci-dessus ne montre pas l'adresse du PLT/GOT car elle peut être trouvée en utilisant une adresse relative au compteur de programme actuel. Cela est attesté par votre recherche relative au PC :
: jmpq *0x2004c2(%rip) ; 0x600860 <_GOT_+24>
En utilisant un code indépendant de la position dans la bibliothèque appelée, avec le PLT/GOT, le premier appel à la fonction printf@plt
(donc dans le PLT) est une opération à plusieurs étapes, au cours de laquelle les actions suivantes ont lieu :
- Il appelle la version GOT (via un pointeur) qui pointe initialement vers un code de configuration dans le PLT.
- Ce code de configuration charge la bibliothèque partagée pertinente si ce n'est pas encore fait, puis modifie le pointeur GOT de sorte que les appels ultérieurs se fassent directement vers le vrai
printf
(à l'adresse virtuelle spécifique au processus) plutôt que le code de configuration du PLT.
- Ensuite, il appelle le code de
printf
chargé à cette adresse.
Lors de appels ultérieurs, parce que le pointeur GOT a été modifié, l'approche à plusieurs étapes est simplifiée :
- Il appelle la version GOT (via pointeur), qui pointe maintenant vers le vrai
printf
.
Un bon article peut être trouvé ici, détaillant comment glibc
est chargé à l'exécution.