Comment un binaire devient un processus en cours d'exécution

Comment un binaire devient un processus en cours d’exécution

Table des matières

Vous êtes-vous déjà demandé ce qui se passe réellement lorsque vous lancez un programme ?
Pas simplement cliquer sur « Run » ou exécuter une commande dans le terminal, mais tout ce qui se passe en coulisses — du fichier exécutable stocké sur le disque jusqu’à un processus pleinement actif en mémoire.

Dans cet article, nous allons voir comment les processus sont créés, comment leur mémoire est organisée, et comment cela fonctionne aussi bien sous Windows que sous les systèmes de type Unix.

Que vous écriviez du code en C, Python ou Rust, comprendre ce mécanisme vous rendra plus efficace et plus pertinent en tant que développeur.

Fichiers exécutables : le point de départ d’un processus

Au cœur de chaque processus en cours d’exécution se trouve un fichier exécutable — un fichier binaire contenant des instructions que le CPU peut comprendre et exécuter directement.

Si vous développez avec des langages compilés comme C, C++, Rust ou Go, votre code est traduit par un compilateur en un exécutable binaire. C’est ce binaire que le système d’exploitation charge et exécute.

À l’inverse, pour les langages interprétés ou basés sur une machine virtuelle comme Python, JavaScript ou Java, votre code s’exécute via un interpréteur ou une machine virtuelle.

Ces interpréteurs sont eux-mêmes des binaires compilés. Lorsque vous lancez un script, c’est donc l’interpréteur qui devient le processus.

📖 Analyse détaillée :
Pour comprendre en détail comment ces différents langages transforment un code écrit en langage humain en instructions binaires, consultez mon guide : Du code source au code machine : les deux voies vers un programme exécutable.

Chaque système d’exploitation utilise un format spécifique pour les fichiers exécutables :

Format de fichier exécutable binaire
Format de fichier exécutable binaire

Les fichiers exécutables contiennent des sections structurées, chacune ayant un rôle précis. Même si les noms varient légèrement selon les formats — ELF (Linux), PE (Windows) et Mach-O (macOS) — les concepts restent globalement identiques :

  • Code et données

    • .text → instructions machine
      • PE : .text, Mach-O : __TEXT,__text
    • .data → variables initialisées
      • PE : .data, Mach-O : __DATA,__data
    • .bss → variables non initialisées
      • PE : .bss, Mach-O : __DATA,__bss
    • .rodata → données en lecture seule (ex. chaînes de caractères)
      • PE : .rdata, Mach-O : __TEXT,__const
  • Tables des symboles et des chaînes

    • .symtab → table des symboles
      • PE : symboles COFF ou fichier .pdb externe, Mach-O : via LC_SYMTAB
    • .strtab → table des chaînes (noms des symboles)
      • PE : partie de COFF, Mach-O : également via LC_SYMTAB
  • Informations de relocalisation

    • .reloc → utilisé pour l’édition de liens dynamique et l’ajustement des adresses
      • PE : .reloc, Mach-O : entrées de relocalisation spécifiques aux sections

…et bien d’autres, selon la plateforme et les options de compilation.

Certaines sections sont en lecture seule (comme .rodata et .text). D’autres sont lisibles et modifiables (comme .bss et .data).

Le système d’exploitation s’appuie sur cette structure pour mapper correctement l’exécutable en mémoire.

Dans cet exemple :

 1const int G_ARGV_INDEX = 1;
 2char g_element;
 3int g_index = 0;
 4int main(int argc, char *argv[]) {
 5    char *match = "Hello";
 6    if (argc < G_ARGV_INDEX) {
 7        return 1;
 8    }
 9    // ... 
10    return 0;
11}

La section .text contient le code machine compilé de la fonction main() et des autres fonctions.

La section .data contient les variables globales ou statiques initialisées comme g_index, tandis que g_element se trouve dans .bss car elle n’est pas initialisée.

La section .rodata contient la chaîne "Hello" ainsi que la constante G_ARGV_INDEX.

Les sections .symtab et .strtab incluent les noms des symboles et leurs adresses, permettant à l’éditeur de liens de résoudre les références entre les différentes parties du programme.

Note

Contrairement au code source où les variables sont représentées par leurs noms, dans le fichier exécutable elles sont représentées par leurs adresses. Le chargeur du système d’exploitation utilise ces informations pour mapper correctement les sections en mémoire.

Qu’est-ce qu’un processus ?

Lorsqu’un fichier exécutable est lancé, le système d’exploitation le transforme en processus. Un processus ne se limite pas à du code. C’est une instance en cours d’exécution, avec sa propre mémoire et ses structures de contrôle.

Chaque processus comporte deux éléments clés.

1. Espace mémoire

C’est là que le contenu du fichier exécutable est chargé en RAM. On y retrouve les mêmes sections — .text, .data, .bss, etc.

Le chargeur du système d’exploitation se charge de les copier et de les mapper en mémoire.

Espace mémoire d’un processus
Espace mémoire d’un processus

À la fin de cet espace mémoire, le système stocke également :

  • Les variables d’environnement (par exemple MY_ENV=hello avec $ MY_ENV=hello python hello.py)
  • Les arguments du programme (comme hello.py avec $ python hello.py)

Entre les deux, on trouve des zones essentielles à l’exécution du programme : la pile, le tas et les mappages mémoire.

  • Pile (stack) : stocke les variables locales et les informations d’appel de fonctions. Elle grandit et rétrécit selon les appels et retours de fonctions.
  • Tas (heap) : utilisé pour l’allocation dynamique de mémoire. Il évolue au fil des allocations et libérations.
  • Mappages mémoire : emplacement des bibliothèques partagées et des ressources chargées dynamiquement dans l’espace d’adressage du processus.

Le système d’exploitation utilise un système de mémoire virtuelle, permettant l’isolation des processus et empêchant toute interférence entre eux.

2. Process Control Block (PCB)

Le PCB est une structure de données maintenue par le système d’exploitation. Elle contient toutes les métadonnées liées au processus.

Cela inclut :

  • Identifiant du processus (PID)
  • Pointeur vers le PCB du processus parent
  • Code de retour
  • État du processus
  • Descripteurs de fichiers ouverts
  • Registres CPU
  • Compteur ordinal
  • Gestionnaires de signaux
  • Priorité du processus
  • Répertoire de travail courant
  • …et bien plus encore

Sous Linux, le PCB est représenté par une structure C appelée task_struct, définie dans le code source du noyau.

Cette structure contient tous les champs nécessaires à la gestion et à l’ordonnancement du processus par le noyau.

Elle est stockée dans l’espace noyau, et non dans l’espace utilisateur, garantissant que seul le système d’exploitation peut y accéder.

L’ensemble des PCB est conservé dans une structure globale appelée table des processus, qui suit tous les processus en cours d’exécution.

Encore plus intéressant : chaque processus est généralement créé par un autre processus. C’est une chaîne continue de créations.

Par exemple, voici un arbre de processus simplifié sur un système Linux :

1user@host:~$ pstree -as 16980
2init(Ubuntu-20.
3  └─SessionLeader
4      └─Relay(11773)
5          └─bash
6              └─python

(Vous pouvez exécuter pstree vous-même pour explorer les liens entre les processus sur votre machine.)

Lorsque vous lancez l’interpréteur Python, il devient un processus enfant du shell (bash, zsh, etc.).

Le shell est lui-même un descendant du processus init (ou systemd sur les systèmes Linux modernes), qui est le tout premier processus lancé au démarrage du système.

Cette hiérarchie apparaît naturellement lors de la création et de la gestion des processus par le système d’exploitation.

Comprendre cette relation parent-enfant est essentiel. Mais comment un nouveau processus est-il réellement créé ? Voyons cela étape par étape.

Création d’un processus : étape par étape

Comment le système d’exploitation passe-t-il d’un fichier exécutable à un nouveau processus actif ?

Voici les principales étapes.

  1. Duplication du parent Un nouveau processus est créé en copiant l’espace mémoire et le PCB d’un processus existant (le parent).

    Certains champs, comme le PID et le parent, sont mis à jour pour refléter l’identité du processus enfant.

    D’autres champs, comme le compteur ordinal ou les descripteurs de fichiers ouverts, sont généralement identiques à ceux du parent.

  2. Chargement de l’exécutable Le nouveau processus remplace ensuite son espace mémoire par le contenu du fichier exécutable. Les sections sont mappées exactement comme défini par le format du fichier.

La copie d’espaces mémoire volumineux étant coûteuse, les systèmes utilisent souvent le Copy-On-Write (COW). Le parent et l’enfant partagent initialement les mêmes pages mémoire.

Si l’un des deux modifie une page, le système crée alors une copie distincte pour le processus concerné. Cela économise du temps et de la mémoire.

Comment un parent surveille ses processus enfants

Lorsqu’un processus parent crée un processus enfant (par exemple via fork()), il doit souvent surveiller son état.

En général, le parent :

  • Attend la fin de l’exécution du processus enfant.
  • Récupère son code de retour.

Sur les systèmes de type Unix, les appels système comme wait() et waitpid() sont utilisés.

Ils permettent au parent de bloquer son exécution jusqu’à la terminaison de l’enfant, puis d’analyser la manière dont il s’est terminé (succès, échec ou signal).

Sous Windows, des fonctions équivalentes existent, comme WaitForSingleObject(), qui attend la fin d’un processus, et GetExitCodeProcess(), qui récupère son code de retour.

Cette surveillance est essentielle pour la gestion des ressources, le traitement des erreurs et pour éviter les processus zombies ou orphelins.

Créer des processus dans le code

Sous Unix / Linux / macOS (systèmes POSIX)

Vous pouvez créer un nouveau processus avec l’appel système fork(). Il duplique le processus appelant.

Exemple simple en C :

 1#include <stdio.h>
 2#include <unistd.h>
 3int main() {
 4    pid_t pid = fork();
 5    if (pid < 0) {
 6        // Fork a échoué
 7        fprintf(stderr, "Fork failed\n");
 8        return 1;
 9    } 
10    printf("The value of pid is %d.\n", pid);
11    return 0;
12}

La sortie sera :

The value of pid is 2623. 
The value of pid is 0.

Le parent et l’enfant continuent à exécuter le même code, mais ils peuvent se distinguer grâce à la valeur de retour de fork() :

  • L’enfant reçoit 0
  • Le parent reçoit le PID de l’enfant

Pour charger un autre exécutable dans le processus enfant, on utilise une fonction de la famille exec().

Ces fonctions remplacent l’image du processus courant par celle d’un nouvel exécutable.

Par exemple, utiliser l’utilitaire printenv pour afficher une variable d’environnement ENV_1 :

 1#include <stdio.h>
 2#include <unistd.h>
 3#include <sys/wait.h>
 4int main() {
 5    pid_t pid = fork();
 6    if (pid < 0) {
 7        fprintf(stderr, "Fork failed\n");
 8        return 1;
 9    }
10    if (pid == 0) {
11        char *args[] = {"printenv", "ENV_1", NULL};
12        char *envp[] = {"ENV_1=Child: env var 1", "ENV_2=2", NULL}; 
13        execve("/usr/bin/printenv", args, envp);
14    } else {
15        printf("Parent: Hello (child pid is %d).\n", pid);
16        wait(NULL);
17    }
18    return 0;
19}

La sortie affiche la valeur de ENV_1 définie dans le processus enfant, tandis que le parent affiche son propre message :

Parent: Hello (child pid is 7703). 
Child: env var 1.

Il existe plusieurs variantes de exec() selon les besoins (execl, execv, execvp, etc.).

Autres mécanismes de création de processus

Même si la combinaison fork() + exec() est la méthode classique sur les systèmes Unix, d’autres appels système existent pour des usages plus spécifiques :

  • clone() : principalement disponible sous Linux, clone() est une version plus flexible de fork(). Il permet de contrôler précisément le partage des ressources entre le parent et l’enfant, comme la mémoire, les descripteurs de fichiers ou les espaces de noms.

    Contrairement à fork(), qui duplique tout le processus, clone() permet de choisir ce qui est partagé ou isolé.
    Il est à la base des bibliothèques de threads comme pthread_create() et des technologies de conteneurs.

  • posix_spawn() : fait partie du standard POSIX. Cette fonction combine fork() et exec() en un seul appel.
    Elle est particulièrement utile dans les environnements sensibles aux performances (macOS, systèmes embarqués), où le coût de fork() peut être trop élevé.

Ces alternatives sont utilisées dans des contextes spécifiques, mais elles illustrent bien la flexibilité de la gestion des processus sous Unix.

Sous Windows

Sous Windows, la création de processus se fait via la fonction CreateProcessA().

Elle :

  • Crée un nouveau processus
  • Lance son thread principal
  • Alloue un nouvel espace mémoire
  • Initialise un nouveau PCB
 1#include <windows.h>
 2int main() {
 3    STARTUPINFOA si = { sizeof(si) };
 4    PROCESS_INFORMATION pi;
 5    CreateProcessA("C:\\Windows\\System32\\notepad.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
 6    WaitForSingleObject(pi.hProcess, INFINITE);
 7    CloseHandle(pi.hProcess);
 8    CloseHandle(pi.hThread);
 9    return 0;
10}

Vous fournissez le chemin vers l’exécutable et pouvez éventuellement passer des arguments et des variables d’environnement.

La fonction nécessite de nombreux paramètres, mais elle offre un contrôle très précis sur le processus créé.

Il existe plusieurs variantes de CreateProcess selon le niveau de contrôle requis.

Conclusion

Comprendre la différence entre l’espace mémoire d’un processus et son PCB est fondamental pour la programmation système, le débogage et l’écriture d’applications efficaces.

Ce n’est pas une simple théorie. Cela apporte une vraie confiance quand on travaille près du système.

Savoir comment les langages compilés et interprétés interagissent avec l’OS aide aussi à mieux comprendre les performances et le comportement des programmes.

Que vous exploriez les appels système, le fonctionnement interne d’un OS ou que vous cherchiez simplement à écrire du meilleur code, cette connaissance est un atout solide.

La prochaine fois que vous cliquerez sur « Run » ou que vous créerez un processus par code, vous saurez exactement ce qui se passe sous la surface.

Lectures et ressources complémentaires

Pour aller plus loin sur la création de processus, la gestion mémoire et les formats exécutables, consultez ces ressources de qualité (en Anglais):

Articles Connexes

Mise en Place et Utilisation de Rust Hors Ligne pour un Développement Sans Faille : Un Tutoriel Étape par Étape

Mise en Place et Utilisation de Rust Hors Ligne pour un Développement Sans Faille : Un Tutoriel Étape par Étape

[Dernière mise à jour: 23 août 2025]

C’est un processus simple de mettre en place Rust lorsque vous avez accès à Internet, mais que se passe-t-il si vous êtes hors ligne? Rust est …

Lire la suite
Qu'est-ce qu'un Programme Informatique et un Langage de Programmation

Qu’est-ce qu’un Programme Informatique et un Langage de Programmation

À quoi peut-on comparer les programmes informatiques? Pour moi, ils sont comme des manuels d’instruction. D’un point de vue fonctionnel, …

Lire la suite
Synchroniser l'utilisateur DevContainer avec l'hôte — La bonne méthode (UID/GID + Nom d'utilisateur)

Synchroniser l’utilisateur DevContainer avec l’hôte — La bonne méthode (UID/GID + Nom d’utilisateur)

Vous avez déjà vu apparaître des erreurs de permissions en travaillant sur des fichiers dans un conteneur Docker ? Par exemple quand un fichier créé …

Lire la suite