- Une analyse technique qui explore le processus par lequel le noyau crée et initialise un processus via l’appel système
execve, avant même l’exécution d’un programme
- Cet appel transmet le chemin du fichier exécutable, les arguments et les variables d’environnement, puis le noyau s’en sert pour charger un exécutable au format ELF
- Un fichier ELF contient le code, les données, les symboles et les informations de liaison dynamique ; le noyau l’interprète pour effectuer le mapping mémoire et l’initialisation de la pile
- Le noyau transfère ensuite le contrôle au point d’entrée
_start, et ce n’est qu’après l’initialisation du runtime propre au langage que la fonction main définie par l’utilisateur est appelée
- Ce processus montre la structure de coopération entre le système d’exploitation, le compilateur et le runtime, et il est essentiel pour comprendre comment l’exécution d’un programme fonctionne au niveau système
Le point de départ de l’exécution d’un programme : l’appel à execve
- Sous Linux, l’exécution d’un programme commence par l’appel système
execve
- Sous la forme
execve(const char *filename, char *const argv[], char *const envp[]), il transmet le nom du fichier exécutable, la liste des arguments et la liste des variables d’environnement
- Le noyau détermine ainsi quel programme exécuter et dans quel environnement
- Dans les langages de haut niveau, cet appel est encapsulé par l’API d’exécution de processus de la bibliothèque standard
- Exemple :
std::process::Command de Rust appelle execve en interne
- Il effectue un processus semblable à la recherche dans le PATH d’un shell, en convertissant le nom de la commande en chemin complet
- Dans le cas d’un script contenant un shebang (
#!), le noyau exécute le programme à l’aide de l’interpréteur indiqué
- Exemple :
#!/usr/bin/python3 → exécution via l’interpréteur Python
ELF : la structure du fichier exécutable
- Sous Linux, les fichiers exécutables utilisent le format ELF (Executable and Linkable Format)
- ELF est un format standard de fichier exécutable qui contient le code, les données, les symboles et les informations de relocalisation
- D’autres systèmes d’exploitation utilisent des formats distincts, comme Mach-O (macOS) ou PE (Windows)
- L’en-tête ELF contient des informations sur la structure du fichier et son placement en mémoire
- Exemples de champs :
ELF Magic, Class, Entry point address, Program headers, Section headers
Entry point address est l’adresse de la première instruction exécutée par le programme
- Dans l’exemple d’en-tête ELF, il s’agit d’un fichier exécutable ELF32 pour l’architecture RISC-V, dont le point d’entrée est défini à l’adresse
0x10358
Les composants internes d’ELF
- Un fichier ELF est composé de plusieurs sections
.text : code exécutable
.data : variables globales initialisées
.bss : variables globales non initialisées
.plt : table utilisée pour les appels aux bibliothèques partagées
.symtab, .strtab : table des symboles et table des chaînes
- La PLT (Procedure Linkage Table) prend en charge les appels vers des fonctions de bibliothèques partagées
- Exemple :
printf, malloc de libc
- La section
PT_INTERP d’ELF indique le linker dynamique (interpreter)
- Le noyau lit l’ELF, place en mémoire les sections chargeables et, si nécessaire, applique des mécanismes de sécurité comme ASLR et le bit NX
Table des symboles et liaison au runtime
- La table des symboles (
symtab) d’un ELF contient les informations d’adresse des fonctions et des variables
- Exemples d’entrées :
_start, main, __libc_start_main
- Même un simple programme « Hello, World! » peut contenir plus de 2 300 symboles
- Cela provient en grande partie de la bibliothèque standard et du code d’initialisation du runtime
- Parce qu’une implémentation de
libc comme musl ou glibc y est liée
- Après avoir chargé chaque section de l’ELF, le noyau transfère le contrôle à l’interpréteur (linker dynamique)
- L’interpréteur gère la relocalisation, l’aléatorisation des adresses (ASLR), les droits d’exécution (bit NX), etc.
Le processus d’initialisation de la pile
- Avant d’exécuter le programme, le noyau doit construire directement la pile (
stack)
- La pile sert aux variables locales, aux frames d’appel de fonctions et au passage des arguments
- Les
argv, envp transmis lors de l’appel à execve sont stockés dans la pile
- Le programme peut ainsi accéder aux arguments de ligne de commande et aux variables d’environnement
- Le noyau ajoute également à la pile le vecteur auxiliaire ELF (
auxv)
- Il contient une trentaine d’éléments, comme la taille de page, les métadonnées ELF et des informations système
- Exemple :
AT_PAGESZ indique la taille des pages mémoire (par exemple 4 KiB)
- Dans l’exemple d’émulateur RISC-V, le pointeur de pile (
sp) démarre à une adresse élevée, puis les arguments, variables d’environnement et vecteur auxiliaire sont empilés en ordre inverse
Le point d’entrée et la fonction _start
- Le point d’entrée d’un ELF est défini comme l’adresse de la fonction
_start
_start est le tout premier code en espace utilisateur auquel le noyau transfère le contrôle
- Dans la plupart des langages,
_start effectue l’initialisation du runtime avant d’appeler main
- Exemple :
std::rt::lang_start en Rust, __libc_start_main en C
- Dans l’exemple Rust, les attributs
#![no_std] et #![no_main] permettent de définir directement _start sans runtime
- Dans
_start, on lit argc, argv, envp depuis la pile puis on appelle le pointeur vers main
- Le runtime propre au langage effectue des initialisations spécifiques au langage, comme les constructeurs globaux, le stockage local aux threads ou la gestion des exceptions
Le déroulement complet jusqu’à l’appel de main()
- L’ensemble du processus peut être résumé ainsi
- Appel à
execve → le noyau charge le fichier ELF
- Interprétation de l’ELF → mapping des sections de code/données, désignation de l’interpréteur
- Construction de la pile → stockage des arguments, variables d’environnement et vecteur auxiliaire
- Exécution du point d’entrée
_start
- Initialisation du runtime, puis appel à
main()
- Cette série d’étapes montre la structure de coopération entre le noyau du système d’exploitation, le format ELF et le runtime du langage
- Le noyau Linux réel inclut aussi une logique interne supplémentaire liée à l’espace d’adressage, à la table des processus, à la gestion des groupes, etc., mais cet article se concentre sur le flux essentiel qui précède ces aspects
Conclusion et correction
- Le processus d’exécution avant
main() est une combinaison d’initialisation au niveau noyau et de configuration du runtime
- Même un simple programme « Hello, World! » s’exécute après être passé par une structure ELF complexe et une initialisation du runtime
- Dans une version initiale du texte, une partie de la logique de chargement des sections était attribuée au noyau, mais il a été corrigé que ce rôle relève en réalité de l’interpréteur ELF
- Cette analyse constitue une base utile pour comprendre la programmation système, les compilateurs et l’architecture des systèmes d’exploitation
Aucun commentaire pour le moment.