Un exécutable n’est pas que du code machine. C’est un format de fichier : un contrat entre l’éditeur de liens qui l’écrit et le système d’exploitation qui le charge. Le fichier indique à l’OS pour quel CPU il est fait, quels octets placer où en mémoire et avec quelles permissions, de quelles bibliothèques il a besoin, et où se trouve la première instruction. Le chapitre sur l’assembleur, l’éditeur de liens et le chargeur suivait un programme à travers cette chaîne d’outils ; celui-ci ouvre le fichier lui-même.
Trois formats couvrent presque tous les ordinateurs d’aujourd’hui :
| Format | Utilisé par | Octets magiques |
|---|---|---|
| ELF (Executable and Linkable Format) | Linux, les BSD, Android, la plupart des systèmes embarqués | 7f 45 4c 46 — \x7fELF |
| PE (Portable Executable) | Windows, les firmwares UEFI | 4d 5a — MZ |
| Mach-O | macOS, iOS | cf fa ed fe (64 bits) |
La commande file les reconnaît à ces octets magiques. Les trois partagent la même anatomie : un en-tête, une description de la façon de placer le fichier en mémoire, le code et les données, et des tables supplémentaires pour les symboles, les relocations et la liaison dynamique.
ELF, octet par octet
Voici un minuscule programme Linux x86-64 — il écrit hello et se termine — lié en un exécutable statique de 1 288 octets. Il fonctionne vraiment : hello, code de sortie 0. Voici ses 64 premiers octets, l’en-tête ELF :
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0200 3e00 0100 0000 6011 2000 0000 0000 ..>.....`. .....
00000020: 4000 0000 0000 0000 c802 0000 0000 0000 @...............
00000030: 0000 0000 4000 3800 0500 4000 0900 0700 ....@.8...@.....
| Octets | Champ | Valeur |
|---|---|---|
7f 45 4c 46 | magique | \x7fELF |
02 | classe | 64 bits |
01 | données | petit-boutiste |
02 00 | type | ET_EXEC, un exécutable à adresse fixe |
3e 00 | machine | 0x3e, x86-64 |
60 11 20 00 … | point d’entrée | 0x201160, l’adresse de _start |
40 00 … | position des en-têtes de programme | juste après l’en-tête |
05 00 | nombre d’en-têtes de programme | 5 |
09 00 | nombre d’en-têtes de section | 9 |
Chaque champ de plusieurs octets est en petit-boutiste. Le point d’entrée est l’endroit où le noyau envoie le CPU une fois le programme chargé.
Les segments : comment le charger
Les en-têtes de programme décrivent des segments : des plages du fichier à placer en mémoire, avec des permissions. C’est ce que lit le noyau :
Type Offset VirtAddr FileSiz MemSiz Flags
PHDR 0x000040 0x200040 0x000118 0x000118 r--
LOAD 0x000000 0x200000 0x00015e 0x00015e r-- en-têtes + .rodata
LOAD 0x000160 0x201160 0x000021 0x000021 r-x .text
LOAD 0x000181 0x202181 0x000004 0x001004 rw- .data + .bss
STACK 0 0 0 0 rw-
- Le segment de code est lisible et exécutable, mais pas modifiable. Le segment de données est modifiable, mais pas exécutable. Cette séparation, appelée W^X (écriture ou exécution, jamais les deux), empêche d’exécuter simplement des données injectées comme du code.
- Le dernier
LOADfait 4 octets dans le fichier (.data) mais0x1004en mémoire. Les 4 Ko supplémentaires sont.bss: le noyau les fournit sous forme de mémoire à zéro, et ils n’occupent aucune place dans le fichier. STACKavec les drapeauxrw-demande une pile non exécutable.
Les sections : la vue de l’éditeur de liens
Les sections — .text, .rodata, .data, .bss, .symtab… — sont la vue plus fine qu’utilisent éditeurs de liens, débogueurs et désassembleurs. Plusieurs sections sont regroupées dans chaque segment selon leurs permissions. Le noyau n’a pas du tout besoin de la table des sections : strip peut retirer les symboles et le programme fonctionne toujours. C’est pourquoi les binaires « strippés » sont plus difficiles à analyser — les noms ont disparu — mais se chargent exactement de la même façon.
ELF dynamique : PIE, interpréteur, bibliothèques
La plupart des programmes Linux ne sont pas statiques. Voici un programme C compilé avec les réglages par défaut de gcc (sur une machine Linux ARM64 ; la structure est identique sur x86-64). Son en-tête indique Type: DYN (Position-Independent Executable file), et ses en-têtes de programme comptent plus d’entrées :
Type Offset VirtAddr FileSiz MemSiz Flg
PHDR 0x000040 0x000040 0x0001f8 0x0001f8 R
INTERP 0x000238 0x000238 0x00001b 0x00001b R
[Requesting program interpreter: /lib/ld-linux-aarch64.so.1]
LOAD 0x000000 0x000000 0x00088c 0x00088c R E
LOAD 0x00fdc8 0x01fdc8 0x000274 0x000278 RW
DYNAMIC 0x00fdd8 0x01fdd8 0x0001e0 0x0001e0 RW
GNU_STACK 0 0 0 0 RW
GNU_RELRO 0x00fdc8 0x01fdc8 0x000238 0x000238 R
- PIE : les adresses commencent à 0. Tout le programme est chargé à une base aléatoire choisie à l’exécution (ASLR) : son point d’entrée,
0x640, n’est qu’un décalage. INTERPnomme l’éditeur de liens dynamique à lancer en premier. Sous Linux x86-64, c’est/lib64/ld-linux-x86-64.so.2.DYNAMICpointe vers la section dynamique, dont la première entrée est iciNEEDED libc.so.6: les bibliothèques à charger. Elle contient aussi les tables de relocation etFLAGS_1: PIE.GNU_RELROmarque les données qui doivent passer en lecture seule une fois les relocations appliquées par l’éditeur de liens dynamique, comme la GOT en full RELRO.
Pour voir le résultat en mémoire, lisez /proc/<pid>/maps. Pour un cat en cours d’exécution, on voit chaque segment du programme, de la libc et de l’éditeur de liens dynamique à leurs adresses aléatoires, avec les permissions demandées par les en-têtes de programme :
aaaae4b10000-aaaae4b19000 r-xp /usr/bin/cat
aaaae4b2f000-aaaae4b30000 r--p /usr/bin/cat
aaaae4b30000-aaaae4b31000 rw-p /usr/bin/cat
aaaaf18be000-aaaaf18df000 rw-p [heap]
ffffbbb90000-ffffbbd1b000 r-xp …/libc.so.6
ffffbbd47000-ffffbbd6e000 r-xp …/ld-linux-aarch64.so.1
ffffbbd83000-ffffbbd85000 r-xp [vdso]
fffff8b3a000-fffff8b5b000 rw-p [stack]
Le [vdso] est une petite bibliothèque que le noyau place dans chaque processus, pour que des appels comme gettimeofday n’aient pas besoin d’un vrai appel système.
PE : le format de Windows
Un fichier PE commence par une relique des années 1980. Voici les 96 premiers octets d’un exécutable Windows 64 bits minimal :
00000000: 4d5a 7800 0100 0000 0400 0000 0000 0000 MZx.............
00000010: 0000 0000 0000 0000 4000 0000 0000 0000 ........@.......
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 7800 0000 ............x...
00000040: 0e1f ba0e 00b4 09cd 21b8 014c cd21 5468 ........!..L.!Th
00000050: 6973 2070 726f 6772 616d 2063 616e 6e6f is program canno
MZ: tout fichier PE commence par un en-tête MS-DOS et un minuscule programme DOS, le stub, qui affiche This program cannot be run in DOS mode.- À l’offset
0x3C,e_lfanew(ici0x78) pointe vers le vrai en-tête : la signaturePE\0\0, puis l’en-tête COFF (machine0x8664= x86-64, nombre de sections…) et l’en-tête optionnel, qui n’a rien d’optionnel.
Quelques champs de l’en-tête optionnel de ce fichier :
| Champ | Valeur | Signification |
|---|---|---|
| Magic | 0x20B | PE32+, une image 64 bits |
| ImageBase | 0x140000000 | adresse de chargement préférée (la valeur par défaut des programmes 64 bits) |
| AddressOfEntryPoint | 0x1000 | point d’entrée, en RVA |
| SectionAlignment / FileAlignment | 0x1000 / 0x200 | sections espacées de 4 Ko en mémoire, de 512 o dans le fichier |
| Subsystem | 3 | un programme console |
| DllCharacteristics | 0x8160 | ASLR (DYNAMIC_BASE), ASLR 64 bits (HIGH_ENTROPY_VA), NX_COMPAT, compatible terminal server |
Les adresses PE sont surtout des RVA — adresses virtuelles relatives, des décalages par rapport à l’endroit où l’image est chargée. L’en-tête optionnel se termine par les répertoires de données : des pointeurs vers la table d’importation (les DLL et fonctions à résoudre dans l’IAT), la table d’exportation (pour les DLL), les relocations de base (.reloc, nécessaires quand l’ASLR déplace l’image), les ressources, le TLS et les informations de débogage.
Mach-O : le format d’Apple
Un fichier Mach-O est un en-tête suivi d’une liste de commandes de chargement (load commands). Un petit programme C compilé sur un Mac :
magic cputype filetype ncmds flags
MH_MAGIC_64 ARM64 EXECUTE 17 NOUNDEFS DYLDLINK TWOLEVEL PIE
- Les commandes
LC_SEGMENT_64définissent les segments :__PAGEZERO,__TEXT(code et constantes),__DATAet__LINKEDIT(symboles et informations de liaison).__PAGEZEROcouvre les 4 premiers Go sans aucun accès en 64 bits : tout pointeur nul ou tronqué à 32 bits provoque immédiatement un plantage. LC_MAINdonne le point d’entrée,LC_LOAD_DYLINKERnomme l’éditeur de liens dynamique (/usr/lib/dyld), etLC_LOAD_DYLIBliste les bibliothèques — tout programme chargelibSystem.B.dylib.LC_CODE_SIGNATURE: sur Apple silicon, tout exécutable doit porter une signature, même ad hoc, sinon le noyau refuse de le lancer.
Mach-O a aussi des binaires universels (ou fat) : plusieurs fichiers Mach-O complets, un par architecture, derrière un petit en-tête. Sur ce Mac, /usr/bin/ssh contient une tranche x86_64 et une tranche arm64e.
Côte à côte
| ELF | PE | Mach-O | |
|---|---|---|---|
| Point d’entrée | e_entry dans l’en-tête | AddressOfEntryPoint (RVA) | LC_MAIN |
| Description du chargement | en-têtes de programme (segments) | table des sections + alignement | commandes LC_SEGMENT_64 |
| Bibliothèques nécessaires | DT_NEEDED | répertoire d’importation | LC_LOAD_DYLIB |
| Pointeurs vers les fonctions importées | GOT (+ stubs PLT) | IAT | pointeurs de symboles, paresseux ou non |
| Éditeur de liens dynamique | PT_INTERP (ld-linux…) | intégré au chargeur de l’OS | LC_LOAD_DYLINKER (dyld) |
| Indépendance de la position | PIE (ET_DYN) | DYNAMIC_BASE + .reloc | drapeau PIE |
En lire un soi-même
| Tâche | Linux / ELF | Windows / PE | macOS / Mach-O |
|---|---|---|---|
| quel est ce fichier ? | file | file, PE-bear | file, lipo -archs |
| en-tête | readelf -h | dumpbin /headers | otool -hv |
| segments / sections | readelf -lW, readelf -SW | dumpbin /headers | otool -l |
| bibliothèques nécessaires | readelf -d | dumpbin /imports | otool -L |
| désassembler | objdump -d -M intel | dumpbin /disasm | otool -tV, objdump -d |
Un avertissement : ldd liste les bibliothèques d’un programme, mais sur certains systèmes il le fait en exécutant l’éditeur de liens dynamique du programme. Ne l’utilisez pas sur un fichier dont vous doutez ; readelf -d ou objdump -p lisent la même information sans rien exécuter.
À retenir
- Un exécutable est un format partagé par l’éditeur de liens et le chargeur de l’OS : ELF sous Linux, PE sous Windows, Mach-O sur les systèmes Apple.
- L’en-tête donne l’architecture et le point d’entrée. Les segments (en-têtes de programme ELF, sections PE,
LC_SEGMENT_64Mach-O) disent quoi placer où, avec quelles permissions. - Les sections sont la vue plus fine de l’éditeur de liens et du débogueur. Le chargeur n’en a pas besoin, d’où le fonctionnement de
strip. - Les programmes dynamiques nomment un interpréteur ou éditeur de liens dynamique et leurs bibliothèques nécessaires, et portent les tables que remplit le chargeur : GOT, IAT, pointeurs de symboles.
- Les en-têtes déclarent aussi des propriétés de sécurité — PIE/ASLR, pile non exécutable, RELRO,
NX_COMPAT, signatures de code — c’est pourquoi on les lit en premier quand on analyse un binaire.