Le dynamic linker : ce qui tourne avant
ton main
Static vs dynamic linking sous ELF : structures Elf32_Rel et Elf64_Sym, sections dynamiques (DT_STRTAB, DT_SYMTAB, DT_JMPREL), entrées PLT/GOT.PLT et resolution paresseuse via _dl_runtime_resolve.
Maxime Jérôme··7 min de lecture
Prérequis
- Compréhension basique de Linux
- Shell
- Maniement de GCC
- Assembleur Intel x86
- Différentes adresses : virtuelles, logiques, physiques
- Introduction ELF
- Segments et Sections ELF
Hello ! o/
Dans cet article, on va explorer comment ELF lie les différentes bibliothèques qu'un programme importe. Pour commencer, il est essentiel de différencier deux types de linkage : statique et dynamique. Le linkage statique se fait avec le flag GCC -static, tandis que le dynamique est le comportement par défaut.
La différence entre ces deux méthodes réside dans la taille de l'exécutable. En linkage statique, vous importez le code source de toutes les fonctions et constantes externes directement dans votre exécutable. En linkage dynamique, vous n'avez qu'à établir une passerelle avec la PLT (Procedure Linkage Table) et la GOT (Global Offset Table) pour accéder à ces fonctions et constantes.
Cela influence aussi les performances. Comme plusieurs programmes accèdent à la même page mémoire de bibliothèque, les instructions seront probablement déjà en cache. À l'inverse, une bibliothèque chargée statiquement à des adresses différentes sera plus lente à charger au premier accès.
Static Linkage
Pas grand-chose à dire sur le linkage statique : tout est mis dans la section .text. Dans "tout", j'entends les constantes globales et les fonctions/procédures externes utilisées.
Dynamic Linkage
Le linkage dynamique est un peu plus complexe. Voici quelques structures de données issues de elf.h :
typedef uint32_t Elf32_Addr;
typedef uint32_t Elf32_Word;
typedef struct elf32_rel {
Elf32_Addr r_offset; /* Adresse */
Elf32_Word r_info; /* Type de relocation et index du symbole */
} Elf32_Rel;
typedef struct elf64_rel {
Elf64_Addr r_offset; /* Adresse */
Elf64_Xword r_info; /* Type de relocation et index du symbole */
} Elf64_Rel;
/* Comment extraire et insérer des informations dans le champ r_info. */
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
typedef struct elf32_sym {
Elf32_Word st_name; /* Nom du symbole (Index dans DT_STRTAB) */
Elf32_Addr st_value; /* Valeur du symbole */
Elf32_Word st_size; /* Taille du symbole */
unsigned char st_info; /* Binding et type du symbole */
unsigned char st_other; /* Visibilité du symbole sous glibc >= 2.2 */
Elf32_Section st_shndx; /* Index de la section */
} Elf32_Sym;
typedef struct elf64_sym {
Elf64_Word st_name; /* Nom du symbole (Index dans DT_STRTAB) */
unsigned char st_info; /* Binding et type du symbole */
unsigned char st_other; /* Aucune définition, 0 */
Elf64_Half st_shndx; /* Index de la section */
Elf64_Addr st_value; /* Valeur du symbole */
Elf64_Xword st_size; /* Taille du symbole */
} Elf64_Sym;Sections Dynamiques
Quelques sections dynamiques à connaitre sur ELF :
| Tag de Section Dynamique | Description |
|---|---|
| DT_STRTAB | Pointe sur la table des strings. Cette table contient simplement des strings séparées par un null byte ('\x00'). |
| DT_SYMTAB | Pointe sur la table des symboles. Cette table contient les informations pour louer et relouer les définitions et références symboliques du programme, définies par les structures Elf32_Sym et Elf64_Sym. |
| DT_JMPREL | Pointe sur les entrées de relocation associées à la PLT. |
| DT_VERSYM | Pointe sur l'adresse de la section .gnu.version qui contient les indices des versions des symboles. |
Entrée PLT et GOT.PLT
Une entrée sur la PLT (Procedure Linkage Table) peut ressembler à ceci (x86) :
0x804xxx4 <some_func>: jmp *some_func_dyn_reloc_entry ; Entrée de la relocation
0x804xxxa <some_func+6>: push $reloc_offset ; Offset de relocation
0x804xxxf <some_func+11>: jmp beginning_of_.plt_section ; Début de la PLTExemple pour puts@plt :
0x0804xxx0 <+0>: jmp DWORD PTR ds:0x804xxxc ; puts@got.plt
0x0804xxx6 <+6>: push 0x0 ; Offset
0x0804xxxb <+11>: jmp 0x804xxx0 ; Début de la PLTGénériquement, on peut définir cet algorithme :
.plt:
push .got.plt[link_map] ; == .got.plt[1]
jmp .got.plt[_dl_runtime_resolve] ; == .got.plt[2]
<func>@plt:
jmp .got.plt[x + n]
push n
jmp .pltCe qui signifie en bon français :
- Jump sur la section
.got.pltpour y trouverfunc@got.plt. - Si
.got.pltn'a pas encore loué l'adresse sur la bibliothèque :- L'adresse à
.got.plt[x + n]renvoie surfunc@plt. - Mettre l'offset en argument.
- Mettre
link_mapde.got.plten argument. - Appeler
_dl_runtime_resolve()pour louer l'adresse de la fonction.
- L'adresse à
- Sinon, appeler la fonction sur la lib.
Premier appel à puts()
┌──────────────────────────────────────────────────────┐
│ │
│ call puts@plt │
│ │ │
│ ▼ │
│ puts@plt : │
│ jmp [puts@got.plt] ──► adresse non résolue │
│ │ (pointe sur puts@plt+6)
│ ▼ │
│ push offset_puts │ │
│ jmp .plt │ │
│ │ │ │
│ ▼ │ │
│ .plt (stub 0) : │ │
│ push link_map │ │
│ jmp _dl_runtime_resolve ◄─┘ │
│ │ │
│ ▼ │
│ _dl_runtime_resolve() : │
│ 1. trouve le nom "puts" dans DT_SYMTAB/DT_STRTAB │
│ 2. cherche l'adresse dans les libs chargées │
│ 3. écrit l'adresse réelle dans puts@got.plt │
│ 4. jump sur puts() dans libc │
│ │
│ Appels suivants : │
│ jmp [puts@got.plt] ──► adresse réelle libc::puts │
│ (résolution directe) │
└──────────────────────────────────────────────────────┘_dl_*()
_dl_runtime_resolve() est une fonction écrite en assembleur.
Dump of assembler code for function _dl_runtime_resolve_xsave:
0x00007ffff7fea430 <+0>: push rbx
...
0x00007ffff7fea4ee <+190>: mov rbx,QWORD PTR [rsp]
0x00007ffff7fea4f2 <+194>: add rsp,0x18
0x00007ffff7fea4f6 <+198>: bnd jmp r11Tout ce qu'il faut comprendre : elle sauvegarde les registres, appelle _dl_fixup() et jump ensuite sur sa valeur de retour.
Au plus simple, _dl_fixup() fait :
- Trouver le nom de la fonction dans la section dynamique adéquate.
- Chercher dans toutes les libs chargées et trouver son adresse.
- Écrire son adresse dans la
GOT.PLT(Global Offset Table, section.got.plt).
Lazy binding
BIND_NOW cherchent à remplacer pour des raisons de sécurité.Conclusion
Pour le linkage dynamique, la procédure complète est la suivante :
- On charge le programme sur le CPU avec ses segments et sections en mémoire.
- Lors d'un appel de fonction :
- Si c'est la premiere fois :
- On appelle
_dl_runtime_resolve(). - On trouve le nom de la fonction dans notre programme.
- On cherche dans les libs chargées la fonction.
- On réécrit la valeur pour les prochaines fois.
- On appelle la fonction.
- On appelle
- Sinon : la valeur sur laquelle on est redirigé pointe directement sur la fonction dans la lib.
- Si c'est la premiere fois :
Programme ELF en mémoire
┌──────────────────────────────────────────────────────────────┐
│ .text │
│ call puts@plt ────────────────┐ │
│ │ │
│ .plt ▼ │
│ puts@plt: │
│ jmp [.got.plt + offset] ─────┐ │
│ │ │
│ .got.plt ▼ │
│ [puts entry] (non résolu) ──► stub PLT → _dl_runtime_resolve
│ [puts entry] (résolu) ──► adresse réelle libc::puts │
│ │
│ ld.so (link editor / dynamic linker) │
│ _dl_runtime_resolve() │
│ └─► _dl_fixup() │
│ ├─ lit DT_SYMTAB + DT_STRTAB → nom "puts" │
│ ├─ parcourt les libs chargées (DT_NEEDED) │
│ └─ écrit l'adresse dans .got.plt │
└──────────────────────────────────────────────────────────────┘Merci pour votre lecture :)