.text, .data, .got, .plt : la carto
d'un ELF
Lecture des segments et sections d'un ELF (Executable and Linkable Format) chargé en mémoire : PT_LOAD, .text, .plt, .got, .data, stack, heap, et résolution dynamique d'un appel via la PLT (Procedure Linkage Table) et la GOT (Global Offset Table).
Maxime Jérôme··6 min de lecture
Prérequis
- Compréhension basique de Linux
- Shell
- Maniement de GCC
- Différentes adresses (virtuelles, logiques, physiques)
- Introduction ELF
Hello ! o/
Dans cet article, vous allez comprendre comment la mémoire est segmentée et sectionnée lorsqu'un programme ELF (Executable and Linkable Format) est chargé. Chouette, non ? Cela vous permettra de mieux comprendre l'organisation de votre programme en mémoire et à quoi sert chaque partie.
Segments
Les segments sont des regroupements de sections qui ont leurs propres flags, adresses de base, etc. Pour obtenir les segments d'un programme, vous pouvez utiliser la commande suivante :
$ readelf -l ./programElf file type is EXEC (Executable file)
Entry point 0x401050
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000040 0x0000000000400040 0x0000000000400040 0x0002d8 0x0002d8 R 0x8
INTERP 0x000318 0x0000000000400318 0x0000000000400318 0x00001c 0x00001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x000508 0x000508 R 0x1000
LOAD 0x001000 0x0000000000401000 0x0000000000401000 0x0001b5 0x0001b5 R E 0x1000
LOAD 0x002000 0x0000000000402000 0x0000000000402000 0x000120 0x000120 R 0x1000
LOAD 0x002e10 0x0000000000403e10 0x0000000000403e10 0x000220 0x000228 RW 0x1000
DYNAMIC 0x002e20 0x0000000000403e20 0x0000000000403e20 0x0001d0 0x0001d0 RW 0x8
NOTE 0x000338 0x0000000000400338 0x0000000000400338 0x000020 0x000020 R 0x8
NOTE 0x000358 0x0000000000400358 0x0000000000400358 0x000044 0x000044 R 0x4
GNU_PROPERTY 0x000338 0x0000000000400338 0x0000000000400338 0x000020 0x000020 R 0x8
GNU_EH_FRAME 0x002004 0x0000000000402004 0x0000000000402004 0x000044 0x000044 R 0x4
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
GNU_RELRO 0x002e10 0x0000000000403e10 0x0000000000403e10 0x0001f0 0x0001f0 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash
.dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .gotVoici les différents segments (liste complète ici) :
- PT_PHDR : Contient l'adresse et la taille de la table des segments.
- PT_INTERP : Contient le chemin vers
ld(ex./lib64/ld-linux-x86-64.so.2). - PT_LOAD : Segment pouvant être chargé en mémoire ; il est normal d'en avoir plusieurs, triés par adresse virtuelle.
- PT_DYNAMIC : Informations sur le linkage dynamique.
- PT_NOTE : Contient des informations auxiliaires comme :
- OS/ABI requis.
- Version minimale du kernel.
- GNU_EH_FRAME : Gestionnaire des exceptions.
- GNU_STACK : Contient la pile d'exécution (stack).
- GNU_RELRO : Sections à mettre en RO (Read-Only) après les relocations dynamiques. RELRO = RELocation Read-Only.
Sections
Les sections sont les composantes des segments. Voici un aperçu des sections principales, triées des adresses basses (0x0) vers les adresses hautes (0xf) :
| Adresses | Segment | Section | Description |
|---|---|---|---|
| 0x0 | LOAD #2 | .plt | PLT (Procedure Linkage Table), utilisée pour appeler les fonctions/procédures externes, comme printf@plt. |
.text | Contient les opcodes (Operations Codes) de l'architecture CPU, c'est-a-dire le code source compilé du programme. | ||
| LOAD #3 | .rodata | Constantes en RO (Read-Only). | |
| LOAD #4 | .ctors / init_array | Liste des constructeurs (pointeurs sur fonction), appelés avant l'appel de main (définis avec __attribute__((constructor))). | |
.dtors / fini_array | Liste des destructeurs (pointeurs sur fonction), appelés après l'appel de main, comme atexit(3) (définis avec __attribute__((destructor))). | ||
.got | GOT (Global Offset Table), utilisée pour pointer sur des variables globales importées de bibliotheques (ex. EXIT_SUCCESS de stdlib.h). | ||
.got.plt | GOT de la PLT, utilisée pour les fonctions/procedures externes, comme printf@libc. | ||
.data | Comme .rodata, mais en RW (Read-Write). | ||
.bss | Comme .data, mais pour les variables globales non initialisées (initialisées a 0). | ||
| N/A | [heap] | Sur le DS (Data Segment), espace mémoire réservé aux allocations dynamiques. Elle grandit vers les adresses hautes et est utilisée par malloc(3), brk(2), etc. | |
| N/A | [libs] | Apres la heap, les bibliotheques (libc, OpenCL, GTK, ...) sont chargées. Elles ont leurs propres segments/sections et sont des programmes a part entiere. | |
| GNU_STACK | [stack] | Allocation statique, grandit vers les adresses basses. Contient les variables locales, les retours d'adresses et les sauvegardes des registres de stack comme RBP (EBP en Intel x86). | |
| 0xf | N/A | [kernel] | Le kernel est chargé dans les adresses les plus hautes (non chargé lors du lancement d'un programme utilisateur). |
Appel d'une Fonction
Imaginons que nous soyons dans .text, et que EIP (le pointeur sur l'instruction courante) pointe vers une instruction call sur printf@plt. Nous savons qu'en résultat, printf(3) sera appelée. Voici ce qu'il se passe réellement :
printf@pltappelleprintf@got.plt.printf@got.pltappelleprintf@libc.
.text
┌─────────────────────────────────────────────────────────┐
│ ... │
│ call printf@plt ; appel depuis le code │
└───────────────┬─────────────────────────────────────────┘
│
▼
.plt (Procedure Linkage Table)
┌─────────────────────────────────────────────────────────┐
│ printf@plt: │
│ jmp *printf@got.plt ; saut indirect via GOT │
│ push <reloc_index> ; 1er appel : lazy binding │
│ jmp PLT[0] ; résolution via ld.so │
└───────────────┬─────────────────────────────────────────┘
│ (apres résolution : adresse réelle)
▼
.got.plt (Global Offset Table pour la PLT)
┌─────────────────────────────────────────────────────────┐
│ printf@got.plt: 0x00007f... → printf@libc │
└───────────────┬─────────────────────────────────────────┘
│
▼
libc.so chargée en mémoire
┌─────────────────────────────────────────────────────────┐
│ printf: implémentation réelle dans la libc │
└─────────────────────────────────────────────────────────┘
Note : la résolution est faite une seule fois (lazy binding).
Les appels suivants passent directement de .plt vers libc.Lazy binding et RELRO
printf@libc n'est écrite dans la GOT qu'au premier appel. La protection RELRO (RELocation Read-Only) en mode Full RELRO résout toutes les fonctions au démarrage et passe la GOT en lecture seule, bloquant ainsi les attaques de type GOT overwrite.Dans le cas où l'on compile avec le flag -static, voici un exemple d'appel sur printf :
Compilation dynamique (par défaut) Compilation statique (-static)
────────────────────────────────── ─────────────────────────────────────
.text .text
│ call printf@plt │ call printf ; appel direct
│ │ ; printf est
.plt │ printf: ; dans .text !
│ jmp *printf@got.plt │ ...implémentation...
│ │ ret
.got.plt
│ → printf@libc (résolue au runtime)
binaire léger, libc partagée binaire lourd, libc embarquée
dépend de la libc installée 100% autonome, pas de dépendance
Taille typique : ~16 Ko Taille typique : ~800 Ko+Article suivant
Le dynamic linker : ce qui tourne avant ton main