CrĂ©er un système d’exploitation n’est pas rĂ©servĂ© aux laboratoires de recherche. C’est aussi un formidable terrain de jeu pour comprendre en profondeur comment un ordinateur dĂ©marre, organise sa gestion de la mĂ©moire, dialogue avec les interfaces matĂ©rielles et exĂ©cute plusieurs programmes en parallèle. Derrière chaque clic sur une icĂ´ne se cache une mĂ©canique Ă©tonnamment fine, que l’on peut apprendre Ă reconstruire pièce par pièce.
Au moment de se lancer, beaucoup imaginent un “gros monolithe” ingérable. En réalité, un OS se décompose en briques bien identifiées : un chargeur de démarrage, un noyau, des drivers, un système de fichiers, puis tout ce qui touche à la planification des processus et à la sécurité. Lors d’un atelier avec des développeurs, l’un d’eux a passé une soirée entière à déboguer un simple secteur de boot de 512 octets ; le lendemain, il comprenait enfin ce qu’il se passait entre l’appui sur le bouton Power et l’apparition du logo. C’est ce type de déclic qui transforme la curiosité en véritable projet.
Ce guide suit la progression naturelle d’un mini-OS moderne : on part d’un chargeur de démarrage minimal en assembleur, on ajoute un noyau en C, puis on construit la gestion de la mémoire, la planification et enfin le système de fichiers. À chaque étape, l’objectif est double : comprendre les enjeux techniques, et garder une vision marketing / produit claire de ce que vous construisez réellement. Car un OS maison peut devenir un outil pédagogique, un démonstrateur pour votre entreprise, voire la base d’une plateforme embarquée ou d’une sandbox de cybersécurité.
Comprendre l’architecture d’un système d’exploitation avant d’écrire la première ligne de code
Avant de coder quoi que ce soit, il s’agit de clarifier ce que doit faire, très concrètement, votre futur système d’exploitation. Les systèmes modernes (Windows, Linux, Android, iOS) partagent une mĂŞme ossature : un noyau qui orchestre le matĂ©riel et un ensemble de services qui standardisent les usages pour les applications. Comprendre ce “squelette” Ă©vite de se perdre dans les dĂ©tails de la programmation système.
On peut considérer qu’un OS standard gère quatre responsabilités majeures : la gestion de la mémoire, la planification des processus (quels programmes tournent, quand, avec quelles priorités), la gestion des interfaces matérielles (périphériques, interruptions, timers, clavier, disque…) et l’accès aux données via un système de fichiers. Tout le reste, du réseau à l’interface graphique, se construit au-dessus de ces briques fondamentales.
Un exemple parlant : la petite distribution expérimentale “Pepin OS” est née d’un simple tutoriel de noyau. Au départ, le système se contentait d’afficher “Hello world” depuis un secteur de boot. En ajoutant progressivement gestion de la mémoire, interruptions, scheduling puis système de fichiers Ext2, il est devenu une plateforme d’enseignement pour des étudiants en informatique et pour une équipe R&D qui testait de nouveaux algorithmes de chiffrement bas niveau.
Dans un contexte professionnel, une bonne conception logicielle d’OS commence par des questions stratégiques : votre système cible-t-il l’embarqué, le poste de travail, le cloud, l’IoT industriel ? Doit-il être temps réel, prioriser la sécurité, ou servir de laboratoire de recherche ? Ces choix influencent tout : format de binaire (ELF, PE), architecture (x86, ARM, RISC-V), structure du noyau (monolithique ou micro-noyau) et même style de drivers.
- Définir la plateforme cible : PC, carte ARM, VM cloud, carte de développement.
- Choisir l’architecture CPU supportée : x86, x86_64, ARM, RISC-V.
- Décider du style de noyau : monolithique simple ou micro-noyau modulaire.
- Fixer l’objectif : pédagogique, R&D, produit embarqué, démonstrateur sécurité.
Dans les faits, la plupart des projets amateurs ou expérimentaux démarrent avec un noyau monolithique 32 bits pour simplifier la gestion de la mémoire, puis migrent éventuellement vers du 64 bits. L’essentiel est de garder un périmètre réaliste : un OS qui boote, exécute un programme utilisateur, offre quelques appels système et sait lire un fichier sur disque est déjà un projet très abouti.
| Composant clé | Rôle principal | Compétence dominante |
|---|---|---|
| Chargeur de démarrage | Charger le noyau en mémoire et lui passer la main | Assembleur, BIOS/UEFI |
| Noyau | Gérer CPU, mémoire, processus, interruptions | Programmation système en C |
| Gestion de la mémoire | Isolation, pagination, allocation dynamique | Architecture CPU, MMU, data structures |
| Planification des processus | Répartir le CPU entre les tâches | Algorithmes, temps réel, synchronisation |
| Drivers & interfaces matérielles | Clavier, disque, réseau, horloge, écran | Documentation matériel, interruptions, IO |
| Système de fichiers | Organisation des données persistantes | Conception de formats, gestion bloc/inode |
Poser cette architecture d’ensemble permet ensuite de dérouler un plan clair : démarrage, noyau minimal, gestion mémoire, multitâche, périphériques, puis données persistantes.
Définir les prérequis techniques pour créer un OS
La rĂ©alitĂ© est simple : sans une solide base en C et en assembleur, il sera très difficile d’aller au bout. Un système d’exploitation ne peut pas reposer uniquement sur des langages de haut niveau. Il doit manipuler des registres, des segments, des adresses physiques, gĂ©rer des interruptions Ă la microseconde près.
Concrètement, un·e développeur·se qui se prépare à ce type de projet doit maîtriser au minimum : les pointeurs en C, la gestion manuelle de la mémoire, la compilation et l’édition de liens avec GCC et LD, la lecture d’un hexdump, l’utilisation d’un émulateur (Bochs, QEMU) et l’assembleur x86 ou ARM pour le démarrage.
- Commencer par revoir C “système” : pointeurs, structures, bitfields, gestion d’erreurs.
- Pratiquer l’assembleur sur de petits exercices (afficher un caractère, addition simple).
- S’habituer à compiler en ligne de commande et à lire des messages de linker.
- Installer QEMU ou Bochs et apprendre Ă lancer un binaire brut.
Ce socle évite un écueil classique : passer plus de temps à se battre avec l’outil de compilation qu’avec la logique du noyau. Une équipe qui a commencé son OS pendant un hackathon a perdu un week-end entier sur une erreur “undefined reference to __stack_chk_fail” simplement parce qu’elle ignorait l’option -fno-stack-protector nécessaire en contexte bare metal.
Du bootloader au noyau minimal : le premier démarrage de votre OS
Le premier jalon concret, c’est un secteur de boot de 512 octets qui affiche un message. Cela peut sembler dérisoire, mais c’est la preuve que votre code s’exécute réellement dès la mise sous tension, sans OS existant. On travaille ici en mode réel 16 bits, avec un code en assembleur extrêmement compact, assemblé avec nasm au format binaire brut.
Au démarrage, le BIOS (ou l’UEFI en mode compatibilité) réalise le POST, puis charge le premier secteur du périphérique de boot à l’adresse 0x7C00 et lui transfère l’exécution. Ce secteur contient à la fois du code et la signature 0xAA55 en fin de bloc, indispensable pour être reconnu comme valide. Votre premier “Hello world” passe souvent par l’interruption vidéo 0x10 du BIOS, qui affiche les caractères un à un.
Un exemple pédagogique : la “startup story” de beaucoup d’OS hobbyistes commence par un bootloader affichant “Chargement du kernel…”. Les équipes constatent très vite que la moindre faute d’adresse seg:offset bloque complètement la machine. C’est un apprentissage radical mais extrêmement formateur sur la façon dont le processeur adresse la mémoire en mode réel.
- Écrire un fichier bootsect.asm en assembleur 16 bits.
- Compiler avec nasm -f bin pour obtenir un binaire de 512 octets.
- Tester avec QEMU ou Bochs via une image de disquette ou de disque virtuel.
- Vérifier que la signature 0xAA55 est bien en fin de secteur.
| Étape | Objectif | Résultat attendu |
|---|---|---|
| Secteur de boot simple | Afficher un message à l’écran | Preuve que le bootloader s’exécute |
| Bootloader étendu | Lire des secteurs de disque via int 0x13 | Chargement brut du noyau en mémoire |
| Passage en mode protégé | Activer le 32 bits et une GDT minimale | Possibilité d’exécuter du C et plus d’1 Mo de RAM |
Une fois ce jalon atteint, la suite logique est un bootloader légèrement plus sophistiqué qui charge un binaire plus conséquent (votre futur noyau), puis bascule le processeur en mode protégé pour profiter d’un adressage 32 bits et préparer la gestion de la mémoire.
Passer en mode protégé et exécuter un noyau en C
Pour que votre noyau soit écrit principalement en C, il doit tourner en mode protégé 32 bits. Cela implique une étape cruciale : initialiser une GDT (Global Descriptor Table) avec au moins trois segments (code, données, pile), charger cette table via l’instruction lgdt, puis activer le bit PE dans le registre CR0.
Le bootloader lit alors plusieurs secteurs de disque grâce à l’interruption 0x13, les copie à une adresse physique connue (par exemple 0x1000 ou 0x100000), active le mode protégé, remplace les segments 16 bits par les sélecteurs 32 bits définis dans la GDT, pointe la pile vers une zone sûre, puis effectue un saut lointain vers l’entrée de votre noyau en C (souvent nommée _start ou kmain).
- Définir une GDT minimale : NULL, segment code 32 bits, segment données 32 bits.
- Charger la GDT en mémoire et l’enregistrer dans GDTR.
- Activer le bit PE de CR0 et recharger les sélecteurs de segments.
- Sauter vers l’adresse du noyau en C, linké à la bonne adresse.
Dans votre noyau C, les toutes premières fonctions touchent au “confort de développement” : affichage texte via écriture directe dans la mémoire vidéo 0xB8000, petites fonctions printk ou print, puis un mini-système de debug (curseur, scroll). C’est cette console qui vous accompagnera pour toutes les étapes suivantes, avant même d’avoir un shell utilisateur.
Une fois ce pipeline maîtrisé, vous disposez d’un environnement dans lequel la programmation système en C peut s’exprimer : il devient alors possible d’implémenter la gestion de la mémoire, la planification des processus et les interfaces matérielles sans repasser systématiquement par du code assembleur complexe.
Maîtriser la gestion de la mémoire : segmentation, pagination et allocation
La gestion de la mémoire est l’un des points qui différencient un OS “jouet” d’un système réellement exploitable. Sans elle, aucune isolation, aucun multitâche robuste, aucune protection contre les erreurs n’est possible. Sur les architectures x86, deux mécanismes se combinent : la segmentation (déjà utilisée pour le mode protégé) et la pagination (page tables) qui introduit la notion de mémoire virtuelle.
Un design courant pour un OS expérimental consiste à utiliser une segmentation simplifiée (segments couvrant tout l’espace 0–4 Go) et à déléguer la vraie isolation des tâches à la pagination. Le noyau crée un “répertoire de pages” (page directory) et une ou plusieurs “tables de pages” qui mappent les adresses virtuelles vers la mémoire physique réelle, typiquement par blocs de 4 Ko.
Lors d’un projet pédagogique, une équipe a commencé par ce qu’on appelle l’identity mapping : faire coïncider les 4 premiers Mo de mémoire virtuelle avec les 4 premiers Mo de mémoire physique. Cela leur a permis d’activer la pagination sans se perdre dans les translations d’adresses, puis, une fois ce socle stable, ils ont introduit un espace utilisateur séparé, mappé par exemple à partir de 0x40000000.
- Initialiser un répertoire de pages en marquant comme présents les blocs utiles.
- Créer une première table de pages couvrant les 4 premiers Mo.
- Écrire une fonction init_mm() qui remplit ces structures et active le bit PG de CR0.
- Prévoir un TLB flush (via CR3 ou invlpg) à chaque modification de mappage.
| Concept mémoire | Description | Impact sur l’OS |
|---|---|---|
| Segmentation | Découpage logique en segments code/données/pile | Base historique, simplifiée dans les OS modernes |
| Pagination 4 Ko | Mapping de pages virtuelles → pages physiques | Isolation des tâches, protection mémoire |
| Bitmap de pages | Tableau de bits indiquant pages libres/occupées | Allocation de cadres de page pour le noyau et les tâches |
| Heap noyau (kmalloc) | Zone pour l’allocation dynamique côté noyau | Permet des structures flexibles et des listes chaînées |
Au-dessus de ces mécanismes bruts, votre conception logicielle doit prévoir des API claires : get_page_frame(), release_page_frame(), kmalloc/kfree pour le noyau, et plus tard un malloc utilisateur appuyé sur un appel système sbrk-like. Cette séparation rend le code testable (d’abord en userland Linux, puis intégré dans le noyau) et limite les effets de bord.
Isoler les tâches utilisateur avec la mémoire virtuelle
Dès que vous introduisez un mode utilisateur et plusieurs processus, la gestion de la mémoire doit assurer une isolation stricte. L’idée est simple : chaque tâche voit un espace d’adressage virtuel cohérent pour elle, même si physiquement ses pages sont éparpillées en RAM. Les appels système, eux, s’exécutent en mode noyau et accèdent à une partie commune de la mémoire virtuelle (typiquement les 1ers Go).
Pour y parvenir, le noyau crée un répertoire de pages par tâche. Les premières entrées du répertoire pointent vers les pages noyau partagées, les dernières vers des tables de pages dédiées à l’espace utilisateur. Lors du contexte switch, le scheduler change simplement CR3 vers le répertoire de pages de la nouvelle tâche, et le processeur se charge d’utiliser les bons mappings.
- Allouer un nouveau répertoire de pages pour chaque processus.
- Copier les entrées noyau (communes) dans la partie basse.
- Mapper le code et les données de la tâche à une adresse virtuelle fixe (par ex. 0x40000000).
- Mettre en place une pile utilisateur dédiée, mappée et protégée.
C’est cette architecture qui permet ensuite d’implémenter proprement des appels système comme read, write, open, ou plus tard un vrai système de fichiers complet. En marketing produit, on pourrait dire que vous posez ici la brique “sécurité par conception” de votre futur OS : aucun processus ne peut lire ou écraser les données d’un autre sans passer par des API contrôlées.
Une fois cette étape franchie, votre projet quitte définitivement le stade du “Hello world kernel” pour entrer dans celui d’un OS capable de faire tourner des applications isolées, condition indispensable à tout cas d’usage sérieux.
Planification des processus et gestion des interruptions : donner l’illusion du multitâche
La planification des processus (scheduling) est ce qui donne l’illusion qu’un ordinateur exécute plusieurs programmes simultanément, alors qu’un seul cœur ne traite qu’un flux d’instructions à la fois. Un OS maison n’échappe pas à la règle : il lui faut un scheduler, même simple, d’autant plus qu’il doit dialoguer avec les interruptions matérielles générées par l’horloge, le clavier, le disque ou le réseau.
Sur x86, deux composants sont au cœur de ce mécanisme : le contrôleur d’interruptions programmable (PIC 8259A ou APIC moderne) et la table des descripteurs d’interruptions (IDT). Le noyau configure les vecteurs d’interruptions pour pointer vers des routines de service (ISR) écrites en assembleur, mais qui, la plupart du temps, appellent ensuite du C pour le traitement métier.
Un cas typique : l’IRQ0 déclenchée à chaque tic d’horloge. La routine associée incrémente un compteur et appelle à intervalles réguliers la fonction schedule(), qui sauvegarde le contexte du processus courant, choisit le suivant selon une politique simple (round-robin, priorité fixe…) et restaure son contexte. En quelques millisecondes, le processeur bascule de tâche en tâche, donnant l’impression de parallélisme.
- Initialiser l’IDT avec une ISR par défaut et des handlers spécifiques (timer, clavier, etc.).
- Configurer le PIC pour rediriger les IRQ vers des vecteurs non réservés (ex. à partir de 0x20).
- Sauvegarder et restaurer soigneusement tous les registres lors d’une interruption.
- Mettre en place un scheduler simple, puis l’améliorer (timeslice, priorités, états de processus).
| Élément | Fonction | Impact sur la planification |
|---|---|---|
| IRQ0 (timer) | Tic régulier déclenchant schedule() | Découpe du temps CPU entre tâches |
| Struct process | Stockage du contexte registre/mémoire | Permet de reprendre une tâche où elle s’est arrêtée |
| État de processus | Ready, running, blocked, zombie, etc. | Décisions du scheduler et appels système wait/exit |
| Syscalls | Interface entre code utilisateur et noyau | Changement de mode utilisateur noyau |
Dans un contexte business ou R&D, cette couche de multitâche est la clé pour expérimenter des politiques de scheduling spécifiques : par exemple, donner la priorité à des tâches de calcul IA, limiter l’impact de processus non fiables, ou simuler des charges de production. Votre OS devient un laboratoire pour concevoir et tester des stratégies d’allocation de ressources.
Gérer les interruptions du clavier et des périphériques pour rendre l’OS interactif
Un système d’exploitation sans interaction reste abstrait. Les interfaces matĂ©rielles – Ă commencer par le clavier et l’écran texte – transforment un noyau technique en plateforme utilisable. Sur PC, le contrĂ´leur clavier 8042 envoie des “scan codes” via le port 0x60 dès qu’une touche est pressĂ©e ou relâchĂ©e. Le noyau doit lire ces codes dans une ISR, les dĂ©coder, puis les placer dans un buffer partagĂ© avec un futur shell.
La routine d’interruption du clavier illustre une bonne conception logicielle de driver : une fine couche d’assembleur pour gérer iret et envoyer l’EOI au PIC, et la logique métier (gestion du Shift, Alt, Ctrl, mappage azerty/qwerty, curseur) écrite en C pour rester lisible et testable. C’est ce modèle que vous pourrez répliquer pour les autres drivers : disque IDE, contrôleur réseau, etc.
- Lire le statut du contrôleur clavier sur le port 0x64, puis les données sur 0x60.
- Décoder make et break codes, mettre à jour l’état des touches spéciales.
- Écrire dans un buffer circulaire partagé avec le processus qui lit la console.
- Mettre Ă jour le curseur via le contrĂ´leur VGA (ports 0x3D4/0x3D5).
Avec ces briques, vous pouvez lancer un premier shell maison, exécutant des commandes simples (affichage de texte, test de la mémoire, lecture de secteurs disque). Pour une équipe marketing ou produit, c’est un jalon fort : vous pouvez enfin “montrer” votre OS, même s’il se limite à un terminal texte. Cette démonstration est souvent suffisante pour susciter l’intérêt de partenaires techniques ou d’un comité d’investissement interne.
Ce lien entre interruptions matérielles, drivers bien conçus et expérience utilisateur est exactement ce qu’on retrouve dans les OS de production, à une autre échelle. Votre prototype n’est plus seulement un exercice de style, il devient une plateforme interactive.
Persistance des données et lancement d’applications : système de fichiers et format ELF
Un OS sans stockage persistant redémarre dans un état vierge à chaque boot. Pour dépasser ce stade, il faut gérer un système de fichiers. Beaucoup de projets s’appuient sur Ext2, un format documenté, largement utilisé et relativement simple comparé à ses successeurs journallés (Ext3/Ext4). L’OS lit alors directement la structure du disque : superbloc, descripteurs de groupes, inodes, blocs de données.
Le noyau commence par lire le superbloc pour connaître la taille des blocs, le nombre d’inodes, la répartition en groupes. Ensuite, il charge les descripteurs de groupes pour localiser les bitmaps de blocs et d’inodes ainsi que la table des inodes. À partir du numéro d’inode, il sait retrouver un fichier, puis le lire bloc par bloc en suivant la liste des blocs pointés par l’inode (directs, indirects, doublement indirects…).
- Implémenter une fonction disk_read bas niveau (PIO sur ports IDE).
- Lire le superbloc Ext2 et en dériver blocksize, inodes par groupe, etc.
- Récupérer une inode à partir de son numéro (calcul du groupe, index, offset).
- Lire le contenu d’un fichier dans un buffer mémoire noyau.
| Structure Ext2 | Contenu principal | Utilité pour l’OS |
|---|---|---|
| Superbloc | Taille des blocs, nombre d’inodes, etc. | Paramétrage global du système de fichiers |
| Group descriptor | Emplacement des bitmaps et de la table d’inodes | Localisation rapide des métadonnées |
| Inode | Droits, taille, pointeurs de blocs | Représentation d’un fichier ou répertoire |
| Directory entry | Nom du fichier → numéro d’inode | Navigation par chemins (« / », « /bin/app ») |
Sur cette base, vous pouvez implémenter des appels système de haut niveau (open, read, close) et un cache minimal d’inodes. On passe d’un OS qui ne fait que tourner en mémoire à une plateforme capable de charger dynamiquement de nouveaux programmes, de stocker des logs, de lire des configurations, bref, d’héberger un véritable écosystème logiciel.
Charger et exécuter des applications ELF depuis le système de fichiers
Pour qu’un OS devienne une plateforme, il doit pouvoir charger des binaires générés par un compilateur standard. Le format ELF (Executable and Linkable Format) est très répandu sur Linux et dans l’écosystème open source. Il décrit, via un en-tête et une table de segments (Program Header Table), quelles parties du fichier charger en mémoire, à quelles adresses virtuelles et avec quels droits.
L’algorithme du chargeur ELF (loader) reste relativement compact : lire l’en-tête, vérifier la signature “ELF”, parcourir la Program Header Table, pour chaque segment de type PT_LOAD allouer/mappper les pages nécessaires, copier les données depuis le fichier dans la mémoire de la tâche, puis initialiser le registre eip à l’adresse d’entrée indiquée dans l’en-tête.
- Compiler une application utilisateur statique avec gcc -static -Ttext=0x40000000.
- Lire le fichier ELF via vos appels système de système de fichiers.
- Créer un nouveau processus, répertoire de pages, pile utilisateur.
- Effectuer un contexte switch vers ce processus via iret vers le mode utilisateur.
Un cas d’usage très parlant pour une équipe marketing digitale : démontrer un mini “store d’applications” interne à l’OS, où chaque binaire ELF est une app isolée, instrumentée pour du test de charge, du benchmarking ou de la cybersécurité. L’OS maison devient une “sandbox” parfaitement contrôlée, idéale pour présenter des POC à des clients B2B.
Ă€ ce stade, votre système d’exploitation sait booter, isoler des tâches, planifier leur exĂ©cution, gĂ©rer les interfaces matĂ©rielles de base et charger des programmes persistants depuis un système de fichiers. C’est dĂ©jĂ un terrain de jeu extrĂŞmement puissant pour tester des idĂ©es, former des Ă©quipes et explorer l’avenir du numĂ©rique… depuis les fondations.
Quelles compĂ©tences sont indispensables pour crĂ©er un système d’exploitation maison ?
Il est nécessaire de maîtriser le langage C, un assembleur (x86 ou ARM), la compilation en ligne de commande, ainsi que les bases de l’architecture processeur (modes réel/protégé, registres, interruptions). Une bonne compréhension des structures de données et des algorithmes est également essentielle pour la gestion de la mémoire et la planification des processus.
Peut-on dĂ©velopper un système d’exploitation en travaillant uniquement en C sans assembleur ?
Non, un minimum d’assembleur est incontournable pour le bootloader, le passage en mode protégé, la gestion des interruptions et certaines opérations sur les registres. En revanche, une fois ces briques écrites, l’essentiel du noyau peut être développé en C.
Combien de temps faut-il pour obtenir un OS qui démarre et lance un programme utilisateur ?
Pour un développeur déjà à l’aise avec le C et l’assembleur, compter plusieurs semaines de travail concentré pour atteindre un noyau qui boote, gère la mémoire, planifie une tâche et exécute un premier binaire ELF simple. Pour un projet mené à temps partiel, cela s’étale facilement sur plusieurs mois.
Pourquoi utiliser un système de fichiers existant comme Ext2 plutôt qu’en inventer un ?
S’appuyer sur un format documenté comme Ext2 évite de réinventer des mécanismes complexes (inodes, blocs, bitmaps) et permet de créer ou modifier l’image disque directement depuis Linux. Cela accélère fortement le développement et améliore la fiabilité des accès disque.
Un OS personnel peut-il avoir une utilité en entreprise ?
Oui, un OS minimal peut servir de plateforme d’enseignement interne, de banc d’essai pour des algorithmes bas niveau, de sandbox de cybersécurité ou de base pour un système embarqué spécialisé. Il permet aussi de monter en compétence des équipes techniques sur la programmation système et l’architecture.
