Invitation à écrire un OS microkernel atomic en C++

Pour ce que cela intéresse, j'ai repris l'apprentissage et le perfectionnement en C++. J'avais laissé de côté surtout avec mes problèmes cognitifs. Mais pour autant j'aimerai approfondir le topic de la sécurité des systèmes d'exploitations.

Et donc j'aimerai utiliser des features du C++23.

Pour cela je vais étudier le L4 Microkernel qui est réputé comme une famille de microkernels utilisés pour des systèmes critiques, notamment en sécurité. Il a été écrit en C++. l'architecture que je vise: RISCV en 32 bits < 4GB RAM pour étudier le mode protégé pour les systèmes embarqués.

L'avantage c'est que l'on peut trouver sur le web, des cartes de prototypages électroniques en RISCV pour pas cher tel que MangoPi MQ Pro à 30€ avec 1GB de RAM.

Pourquoi un micro-OS?

Les micro-OS représentent une approche minimaliste et modulaire du développement des systèmes d'exploitation, offrant des avantages en termes de sécurité, de fiabilité, et de flexibilité. Ils sont particulièrement adaptés aux environnements où la légèreté et la robustesse sont primordiales, comme dans les systèmes embarqués ou les projets de recherche. Bien que les micro-OS soient plus simples par leur taille, leur conception modulaire peut rendre le développement plus complexe, car chaque composant doit être conçu pour interagir correctement avec les autres.

Par ou commencer?

Déja faut étudier le C++ et un peu d'assembleur. Connaitre les features pour optimiser le code. Analyser la codebase d'ancien projet. Pour le projet on utilisera clang++/LLVM comme compilateur.


1. Développer un Bootloader :

Le bootloader est le premier programme qui s'exécute lorsque vous allumez un ordinateur. Il charge le noyau de l'OS en mémoire et lui transfère le contrôle. Voici les étapes clés pour développer un bootloader :

  • Choisir un mode de démarrage :

    • Mode Protégé (Protected Mode) : Mode 32-bit permettant l'accès à plus de 1 Mo de RAM et aux fonctionnalités avancées.
  • Créer un secteur de démarrage :

    • Écrire un code en assembleur (ASM) qui tient dans 512 octets pour démarrer l'OS.
    • Le secteur de démarrage peut charger un deuxième stage plus complexe écrit en C++.
  • Initialisation de l'environnement :

    • Configurer les segments de mémoire, initialiser le GDT (Global Descriptor Table) et passer en mode protégé si vous optez pour un OS 32-bit.
  • Charger le microkernel :

    • Le bootloader doit localiser le noyau sur le disque, le charger en mémoire et lui transférer le contrôle.

2. Écrire un Microkernel en C++ :

Le microkernel sera le cœur de votre OS, responsable des fonctionnalités de base comme la gestion des processus, de la mémoire, et de la communication inter-processus (IPC).

  • Démarrage minimal :

    • Le microkernel doit démarrer et configurer l'environnement matériel minimal, comme le gestionnaire d'interruptions, la pile du processeur, et les pilotes de base.
  • Gestion de la mémoire :

    • Implémentez une gestion basique de la mémoire, avec un gestionnaire de pages pour contrôler l'accès à la mémoire physique et virtuelle.
  • Gestion des processus :

    • Implémentez un planificateur simple pour gérer plusieurs tâches. Au début, un planificateur à tourniquet (round-robin) peut être suffisant.
  • Communication inter-processus (IPC) :

    • La communication entre les processus est essentielle dans un microkernel. Commencez par un modèle de message simple que les processus peuvent utiliser pour échanger des données.
  • Sécurité et isolation :

    • Puisque votre objectif est de faire de la recherche en sécurité, implémentez des mécanismes pour isoler les processus (sandboxing) et surveiller leur comportement.

Il y a des sites pour étudier la création d'OS from Scratch. Il y a OSDev.org, mais aussi des projet sur Github:

On va surtout étudier la sécurité, c'est un exo pour le fun pour implémenter des méthodes de sandboxing (jail system) et surveillance de processus, ainsi que les logs pour contrôler le bon fonctionnement. L'intérêt serait d'étudier un mode atomic sur le microkernel. Et transposer des concepts de serverless comme paradigme. Développement de l'isolation accrue: Chercher à développer un Jail System (chroot) combinant les namespaces, cgroups. On va créer un fork amélioré de Jail(chroot).

Quelques pistes

Utiliser les fonctionnalités du C++23 pour développer un microkernel avec des aspects immuables (atomic) est une approche innovante qui peut tirer parti des avancées du langage pour améliorer la sécurité et la fiabilité. Voici comment certaines des nouvelles fonctionnalités du C++23 peuvent être utiles :

1. Types Consteval et Constinit

  • Immuabilité à la Compilation : Les nouvelles fonctionnalités consteval et constinit permettent de calculer des expressions à la compilation. Cela peut être utilisé pour initialiser des constantes globales ou des configurations du kernel qui ne doivent pas être modifiées après le démarrage. Ces types garantissent que certaines valeurs restent immuables dès la compilation, renforçant ainsi la sécurité du kernel.
  • Exemple d'Utilisation : Vous pouvez définir des configurations critiques du microkernel avec constinit pour empêcher toute modification après l'initialisation, ce qui s'aligne avec l'idée d'un kernel atomique.

2. Modules

  • Isolation et Encapsulation : Les modules en C++23 permettent de mieux encapsuler le code et de réduire les dépendances entre les différentes parties du système. Dans le contexte d'un microkernel, cela signifie que vous pouvez isoler les composants critiques (comme la gestion de la mémoire ou des interruptions) dans des modules distincts qui ne peuvent pas être modifiés ou contaminés par d'autres parties du système.
  • Sécurité Renforcée : En isolant les fonctionnalités critiques dans des modules, vous créez des barrières naturelles qui protègent ces composants contre les accès non autorisés ou malveillants, alignant ainsi l'architecture sur l'idée d'un microkernel immuable.

3. Coroutines

  • Gestion Efficiente des Concurrences : Les coroutines, introduites en C++20 et améliorées en C++23, permettent de gérer des tâches concurrentes de manière plus efficace et contrôlée. Pour un microkernel, cela signifie que vous pouvez gérer les processus utilisateur et les interruptions de manière plus fluide tout en minimisant les états mutables et les interactions non sécurisées entre les tâches.
  • Immuabilité et Concurrence : En combinant coroutines avec des données immuables, vous pouvez réduire les risques liés aux conditions de course (race conditions), car les coroutines peuvent fonctionner de manière isolée sans avoir besoin de modifier des états globaux.

4. Constexpr Dynamique

  • Évaluation à la Compilation Améliorée : Le constexpr en C++23 a été étendu pour supporter des expressions plus complexes. Cela permet de vérifier à la compilation des propriétés du microkernel, telles que les invariants critiques qui ne devraient jamais changer pendant l'exécution.
  • Systèmes Invariants : Par exemple, vous pouvez vérifier que certaines configurations du microkernel respectent des conditions spécifiques dès la compilation, renforçant ainsi l'idée d'un système sécurisé et immuable.

5. Concepts et Contraintes (Concepts)

  • Renforcement des Interfaces : Les concepts permettent de définir des contraintes sur les types de données utilisées dans les templates. Pour un microkernel, cela permet de garantir que seules des structures de données spécifiques et sécurisées sont utilisées, évitant ainsi l'introduction de comportements indésirables.
  • Sécurité du Code : Vous pouvez définir des concepts qui assurent que certaines opérations sur des données sensibles respectent toujours des contraintes strictes, rendant le kernel plus robuste et moins susceptible aux erreurs ou attaques.

6. Pointeurs Intelligents (Smart Pointers)

  • std::unique_ptr et std::shared_ptr : Ces pointeurs intelligents sont essentiels pour gérer la mémoire de manière sûre. std::unique_ptr garantit qu'un seul objet pointe vers une ressource donnée, évitant ainsi les fuites de mémoire et les accès concurrentiels dangereux. std::shared_ptr, avec std::weak_ptr, peut être utilisé pour gérer des objets partagés, tout en évitant les cycles de référence.
  • Immuabilité et Atomicité : Vous pouvez associer ces pointeurs à des objets immuables pour garantir que les ressources qu'ils gèrent ne soient pas modifiées après leur création. Cela contribue à la stabilité du microkernel.

7. std::atomic et Opérations Atomiques

  • Pointeurs Atomiques : En utilisant std::atomic avec des pointeurs, vous pouvez gérer la concurrence de manière sûre. Cela est particulièrement important pour les systèmes multithread où plusieurs processus peuvent accéder ou modifier des ressources partagées.
  • Synchronisation Sans Mutex : Les opérations atomiques permettent de synchroniser l'accès aux ressources sans les coûts et les complications associés aux mutex traditionnels. Cela améliore la performance et réduit les risques de deadlocks.

8. std::span et std::array

  • Accès Sécurisé à la Mémoire : std::span et std::array sont des alternatives sécurisées aux pointeurs bruts pour accéder à des blocs de mémoire contigus. Ils fournissent des vérifications automatiques des limites, évitant les dépassements de mémoire, ce qui est essentiel dans un contexte de kernel.
  • Gestion de la Mémoire avec Sécurité : Ces structures peuvent être combinées avec constexpr pour définir des blocs de mémoire immuables, renforçant ainsi la stabilité et la sécurité du microkernel.