+++ title = "Un noyau Rust minimal" weight = 2 path = "fr/minimal-rust-kernel" date = 2018-02-10 [extra] chapter = "Bare Bones" # Please update this when updating the translation translation_based_on_commit = "c689ecf810f8e93f6b2fb3c4e1e8b89b8a0998eb" # GitHub usernames of the people that translated this post translators = ["TheMimiCodes", "maximevaillancourt"] +++ Dans cet article, nous créons un noyau Rust minimal 64-bit pour l'architecture x86. Nous continuons le travail fait dans l'article précédent [freestanding Rust binary] pour créer une image de disque amorçable qui imprime quelque chose à l'écran. [freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.md Cet article est développé ouvertement sur [GitHub]. Si vous avez des problèmes ou des questions, veuillez ouvrir une Issue sur GitHub. Vous pouvez aussi laisser un commentaire [au bas de la page]. Le code source complet pour cet article peut être trouvé dans la branche [`post-02`][post branch]. [GitHub]: https://github.com/phil-opp/blog_os [au bas de la page]: #comments [post branch]: https://github.com/phil-opp/blog_os/tree/post-02 ## The Boot Process QUand vous ouvrez un ordinateur, il commence à exécuter le code du micrologiciel qui est enregistré dans la carte maîtresse[ROM]. Ce code performe un [power-on self-test], détecte la mémoire volatile disponible, et pré-initialise le CPU et le matériel. Par la suite, il recherche un disque amorçable et commence le processus d'amorçage du noyau du système d'exploitation. [ROM]: https://en.wikipedia.org/wiki/Read-only_memory [power-on self-test]: https://en.wikipedia.org/wiki/Power-on_self-test Sur x86, il y a deux standards pour les micrologiciels: le “Basic Input/Output System“ (**[BIOS]**) et le nouvel “Unified Extensible Firmware Interface” (**[UEFI]**). Le BIOS standard est vieux et dépassé, mais il est simple et bien suporté sur toutes les machines x86 depuis les années 1980. Au contraire, l'UEFI, est plutôt moderne et il offre davantage de fonctionnalités, cependant, il est plus complexe à installer (du moins, selon moi). [BIOS]: https://en.wikipedia.org/wiki/BIOS [UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface Actuellement, nous offrons seulement un support BIOS, mais nous planifions aussi du support pour l'UEFI. Si vous aimeriez nous aider avec cela, consultez [Github issue](https://github.com/phil-opp/blog_os/issues/349). ### BIOS Boot Presque tous les systèmes x86 ont des support pour amorcer le BIOS, incluant les récentes machine UEFI-based qui utilisent un BIOS émulé. C'est bien étant donné que vous pouvez utiliser la même logique d'armorçage sur toutes les machines du dernier siècle. De plus, cette grande compatibilité est à la fois le plus grand désavantage de l'amorçage BIOS. En effet, cela signifie que le CPU est mis dans un mode de compatibilité 16-bit appelé [real mode] avant l'amorçage afin que les bootloaders archaïques des années 1980 puissent encore fonctionner. Commençons par le commencement: Quand vous ouvrez votre ordinateur, il charge le BIOS provenant d'un emplacement de mémoire flash spéciale localisée sur la carte maîtresse. Le BIOS exécute des tests d'auto-diagnostic et es routines d'initialisation du matériel, puis il cherche des disques amorçables. S'il en trouve un, le contrôle est transféré à its _bootloader_, qui est une portion 512-byte du code exécutable enregistré au début du disque. La pluplart des bootloaders sont plus gros que 512 bytes, alors les bootloaders sont communément séparés en deux étapes. La première étape, est de 512 bytes, et la seconde étape, est chargé subséquemment à la première étapge. Le bootloader doit déterminé la localisation de l'image de noyau sur le disque et de la télécharger dans sa mémoire. Il doit aussi transformer le CPU 16-bit [real mode] en un 32-bit [protected mode], puis en un 64-bit [long mode], où les registres 64-bit et la mémoire maîtresse complète sont disponibles. Sa troisième tâche est de récupérer certaines informations (telle que les associations mémoires) du BIOS et les passer au noyau du système d'exploitation. [real mode]: https://en.wikipedia.org/wiki/Real_mode [protected mode]: https://en.wikipedia.org/wiki/Protected_mode [long mode]: https://en.wikipedia.org/wiki/Long_mode [memory segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation Implémenter un bootloader est délicat puisque cela requiert l'écriture de code assembleur et plusieurs autres étapes particulières comme “write this magic value to this processor register”. Par conséquent, nous ne couvrons pas la création d'un bootoader dans cet article et nous fournissons plutôt un outil appelé [bootimage] qui ajoute automatiquement un bootloader à votre noyau. [bootimage]: https://github.com/rust-osdev/bootimage Si vous êtes intéressé à créer votre propre booloader : Gardez l'oeil ouvert, plusieurs articles sur ce sujet sont déjà prévus! #### The Multiboot Standard Pour éviter que chaque système d'opération implémente son propre bootloader, qui est seulement compatible avec un seul système d'exploitation, le [Free Software Foundation] à créé un bootloader public standard appelé [Multiboot] en 1995. Le standard défini une interface entre le bootloader et le système d'opération afin que n'importe quel Multiboot-compliant bootloader puisse charger n'importe quel système d'opérations Multiboot-compliant. La référence d'implementation est [GNU GRUB], qui est le bootloader le plus populaire pour les systèmes Linux. [Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation [Multiboot]: https://wiki.osdev.org/Multiboot [GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB Pour faire un noyau conforme à la spécification Multiboot, il faut seulement inséré un [Multiboot header] au début du fichier du noyau. Cela fait en sorte que c'est très simple to boot un système d'exploitation depuis GRUB. Cependant, le GRUB et le Multiboot standard présentent aussi des problèmes : [Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format - Ils supportent seulement le mode de protection 32-bit. Cela signifie que si vous devez encore faire la configuration du CPU pour changer au 64-bit long mode. - Ils sont désignés pour faire le bootloader simple plutôt que le noyau. Par exemple, le noyau doit être lié avec un [adjusted default page size], étant donné que le GRUB ne peut pas trouver les entêtes Multiboot autrement. Un autre exemple est que le [boot information], qui est passé au noyau, contient plusieurs structures spécifiques à l'architecture plutôt que de fournir des abstractions pures. - GRUB et le standard Multiboot sont peu documentés. - GRUB doit être installé sur un système hôte pour créer une image de disque amorçable depuis le fichier du noyau. Cela rend le développement sur Windows ou sur Mac plus difficile. [adjusted default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2 [boot information]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format En raison de ces désavantages, nous avons décidé de ne pas utiliser GRUB ou le standard Multiboot. Cependant, nous avons planifié d'ajouter un support Multiboot à notre outil [bootimage], afin qu'il soit aussi possible de charger votre noyau sur un système GRUB. Si vous êtes interessé à écrire un noyau Multiboot conforme, consultez la [first edition] de cette série d'articles. [first edition]: @/edition-1/_index.md ### UEFI (Nous ne fournissons pas le support UEFI à l'heure actuelle, mais nous aimerions bien! Si vous êtes intéressé à aider, dites-le nous dans le [Github issue](https://github.com/phil-opp/blog_os/issues/349).) ## A Minimal Kernel Maintenant que nous savons à peu près comment un ordinateur démarre, c'est le temps de créer notre propre noyau minimal. Notre objectif est de créé une image de disque qui imprime “Hello World!” à l'écran lorsqu'il démarre. Nous faisons ceci en améliorant le [freestanding Rust binary][binaire Rust autonome] du dernier article. Comme vous vous en rappelez peut-être, nous avons bâti un binaire autonome grâce à `cargo`, mais selon le système d'opérations, nous avions besoin de différents points d'entrée et d'options de compilation. Ceci est dû au fait que `cargo` construit pour le the _host system_ by default, i.e., le système que vous utilisez. Ce n'est pas quelque chose que nous voulons pour notre noyau, puisqu'un noyau exécute par dessus, e.g., Windows, ce qui ne fait pas de sens. À la place, nous voulons compiler un système cible bien défini _target system_. ### Installing Rust Nightly Rust a trois canaux de distribution : _stable_, _beta_, et _nightly_. The Rust Book explique bien les différences entre ces canaux, alors prenez une minute et [check it out](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). Pour construire un système d'opérations, nous aurons besoin de fonctionalités expérimentales qui sont disponibles uniquement sur le canal de distribution nocturne, alors nous devons installer une version nocturne de Rust. Pour gérer l'installation de Rust, je recommande fortement [rustup]. Cela vous permet d'installer la version nocturne, beta and stable compilers côte-à-côte et facilite leur mise à jour. Avec rustup, vous pouvez utiliser un canal de distribution nocturne pour les directives actuelles en excéutant `rustup override set nightly`. Par ailleurs, vous pouvez ajouter un fichier appelé `rust-toolchain` avec le contenu `nightly` au dossier racine du projet. Vous pouvez vérifier que vous avez une version nocturne installée en exécutant `rustc --version`: Le numéro de la version devrait comprendre `-nightly` à la fin. [rustup]: https://www.rustup.rs/ La version nocturne du compilateur nous permet d'activer certaines fonctionnalités expérimentales en utilisant certains _drapeaux de fonctionalité_ dans le haut de notre fichier. Par exemple, nous pourrions activer [`asm!` macro][macro expérimentale `asm!`] pour écrire du code assembleur intégré en ajoutant `#![feature(asm)]` au haut de notre `main.rs`. Noter que ces fonctionnalités expérimentales sont tout à fait instables, ce qui veut dire que des versions futures de Rust pourraient les changer ou les retirer sans préavis. Pour cette raison, nous les utiliserons seulement lorsque strictement nécessaire. [`asm!` macro]: https://doc.rust-lang.org/stable/reference/inline-assembly.html ### Spécification de cible Cargo supporte différent systèmes cibles avec le paramètre `--target`. La cible est définie par un soi-disant _[target triple][triplet de cible]_, qui décrit l'architecteur du processeur, le fabricant, le système d'exploitation, et l'interface binaire d'application ([ABI]). Par exemple, le triplet `x86_64-unknown-linux-gnu` décrit un système avec un processeur `x86_64`, pas de fabricant défini, et un système d'exploitation Linux avec l'interface binaire d'application GNU. Rust supporte [plusieurs différents triplets de cible][platform-support], incluant `arm-linux-androideabi` pour Android ou [`wasm32-unknown-unknown` pour WebAssembly](https://www.hellorust.com/setup/wasm-target/). [target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple [ABI]: https://stackoverflow.com/a/2456882 [platform-support]: https://forge.rust-lang.org/release/platform-support.html [custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html Pour notre système cible toutefois, nous avons besoin de paramètres de configuration spéciaux (par exemple, pas de système d'explotation sous-jacent), donc aucun des [triplets de cible existants][platform-support] ne convient. Heureusement, Rust nous permet de définir [notre propre cible][custom-targets] par l'entremise d'un fichier JSON. Par exemple, un fichier JSON qui décrit une cible `x86_64-unknown-linux-gnu` ressemble à ceci: ```json { "llvm-target": "x86_64-unknown-linux-gnu", "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", "arch": "x86_64", "target-endian": "little", "target-pointer-width": "64", "target-c-int-width": "32", "os": "linux", "executables": true, "linker-flavor": "gcc", "pre-link-args": ["-m64"], "morestack": false } ``` La plupart des champs sont requis par LLVM pour générer le code pour cette plateforme. Par exemple, le champ [`data-layout`] définit la taille de divers types d'entiers, de nombres à virgule flottante, et de pointeurs. Puis, il y a des champs que Rust utilise pour de la compilation conditionelle, comme `target-pointer-width`. Le troisième type de champ définit comme une caisse doit être construite. Par exemple, le champ `pre-link-args` spécifie les arguments fournis au [linker][lieur]. [`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout [linker]: https://en.wikipedia.org/wiki/Linker_(computing) Nous pouvons aussi cibler les systèmes `x86_64` avec notre noyau, donc notre spécification de cible ressemblera beaucoup à celle plus haut. Commençons par créer un fichier `x86_64-blog_os.json` (utilisez le nom de votre choix) avec ce contenu commun: ```json { "llvm-target": "x86_64-unknown-none", "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", "arch": "x86_64", "target-endian": "little", "target-pointer-width": "64", "target-c-int-width": "32", "os": "none", "executables": true } ``` Noter que nous avons changé le système d'exploitation dans le champs `llvm-target` et `os` pour `none`, puisque nous ferons l'exécution sur du "bare metal" (pas de système d'exploitation sous-jacent). Nous ajoutons ensuite les champs suivants reliés à la construction: ```json "linker-flavor": "ld.lld", "linker": "rust-lld", ``` Plutôt que d'utiliser le lieur par défaut de la plateforme (qui pourrait ne pas supporter les cibles Linux), nous utilisons le lieur multi-plateforme [LLD] qui est inclut avec Rust pour lier notre noyau. [LLD]: https://lld.llvm.org/ ```json "panic-strategy": "abort", ``` Ce paramètre spécifie que la cible ne permet pas le [stack unwinding][déroulement de la pile] lorsque le noyau panique, alors le système devrait plutôt s'arrêter directement. Ceci mène au même résultat que l'option `panic = "abort"` dans notre Cargo.toml, alors nous pouvons la retirer de ce fichier. (Noter que, contrairement à l'option Cargo.toml, cette option de cible s'applique aussi quand nous recompilerons la bibliothèque `core` plus loin dans cet article. Ainsi, même si vous préférez garder l'option Cargo.toml, gardez cette option.) [stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php ```json "disable-redzone": true, ``` Nous écrivons un noyau, donc nous devrons éventuellement gérer les interruptions. Pour ce faire en toute sécurité, nous devons désactiver une optimisation de pointeur de pile nommée la _“zone rouge", puisqu'elle causerait une corruption de la pile autrement. Pour plus d'informations, lire notre article séparé à propos de la [disabling the red zone][désactivation de la zone rouge]. [disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md ```json "features": "-mmx,-sse,+soft-float", ``` Le champ `features` active/désactive des fonctionalités de la cible. Nous désactivons les fonctionalités `mmx` et `sse` en les précédant d'un signe "moins" et activons la fonctionnalité `soft-float` en la précédant d'un signe "plus". Noter qu'il ne doit pas y avoir d'espace entre les différentes fonctionnalités, sinon LLVM n'arrive pas à analyser la chaîne de caractères des fonctionnalités. Les fonctionnalités `mmx` et `sse` déterminent le support les instructions [Single Instruction Multiple Data (SIMD)], qui peuvent souvent significativement accélérer les programmes. Toutefois, utiliser les grands registres SIMD dans les noyaux des systèmes d'exploitation mène à des problèmes de performance. Ceci arrive puisque le noyau a besoin de restaurer tous les registres à leur état original avant de continuer un programme interrompu. Cela signifie que le noyau doit enregistrer l'état SIMD complet dans la mémoire principale à chaque appel système ou interruption matérielle. Puisque l'état SIMD est très grand (512–1600 octets) et que les interruptions peuvent survenir très fréquemment, ces opérations d'enregistrement/restauration additionnelles nuisent considérablement à la performance. Pour prévenir cela, nous désactivons SIMD pour notre noyau (pas pour les applications qui s'exécutent dessus!). [Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD Un problème avec la désactivation de SIMD est que les opérations sur les nombres à virgule flottante sur `x86_64` nécessitent les registres SIMD par défaut. Pour résoudre ce problème, nous ajoutons la fonctionnalité `soft-float`, qui émule toutes les opérations à virgule flottante avec des fonctions logicielles utilisant des entiers normaux. Pour plus d'informations, voir notre article sur la [désactivation de SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md). #### Putting it Together Our target specification file now looks like this: ```json { "llvm-target": "x86_64-unknown-none", "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", "arch": "x86_64", "target-endian": "little", "target-pointer-width": "64", "target-c-int-width": "32", "os": "none", "executables": true, "linker-flavor": "ld.lld", "linker": "rust-lld", "panic-strategy": "abort", "disable-redzone": true, "features": "-mmx,-sse,+soft-float" } ``` ### Construction de notre noyau Compiler pour notre nouvelle cible utilisera les conventions Linux (je ne suis pas trop certain pourquoi; j'assume que c'est simplement le comportement par défaut de LLVM). Cela signifie que nos avons besoin d'un point d'entrée nommé `_start` comme décrit dans le [previous post][dernier article]: [previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.md ```rust // src/main.rs #![no_std] // ne pas lier la bibliothèque standard Rust #![no_main] // désactiver tous les points d'entrée Rust use core::panic::PanicInfo; /// Cette fonction est invoquée lorsque le système panique #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } #[no_mangle] // ne pas massacrer le nom de cette fonction pub extern "C" fn _start() -> ! { // cette fonction est le point d'entrée, puisque le lieur cherche une fonction // nommée `_start` par défaut loop {} } ``` Noter que le point d'entrée doit être appelé `_start` indépendamment du système d'exploitation hôte. Nous pouvons maintenant construire le noyau pour notre nouvelle cible en fournissant le nom du fichier JSON comme `--target`: ``` > cargo build --target x86_64-blog_os.json error[E0463]: can't find crate for `core` ``` Cela échoue! L'erreur nous dit que le compilateur ne trouve plus la [`core` library][bibliothèque `core`]. Cette bibliothèque contient les types de base Rust comme `Result`, `Option`, les itérateurs, et est implicitement liée à toutes les caisses `no_std`. [`core` library]: https://doc.rust-lang.org/nightly/core/index.html Le problème est que la bibliothèque essentielle est distribuée avec le Rust compilateur comme biliothèque _precompilée_. Donc, elle est seulement valide pour les triplets d'hôtes supportés (par exemple, `x86_64-unknown-linux-gnu`) mais pas pour notre cible personnalisée. Si nous voulons compiler du code pour d'autres cibles, nous devons d'abord recompiler `core` pour ces cibles. #### L'option `build-std` C'est ici que la [`build-std` feature][fonctionnalité `build-std`] de cargo entre en jeu. Elle permet de recompiler `core` et d'autres caisses de la bibliothèque standard sur demande, plutôt que d'utiliser des versions précompilées incluses avec l'installation de Rust. Cette fonctionnalité est très récente et n'est pas encore complète, donc elle est définie comme instable et est seulement disponible avec les [nightly Rust compilers][versions nocturnes du compilateur Rust]. [`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std [nightly Rust compilers]: #installing-rust-nightly Pour utiliser cette fonctionnalité, nous devons créer un fichier de [cargo configuration][configuration cargo] dans `.cargo/config.toml` avec le contenu suivant: ```toml # dans .cargo/config.toml [unstable] build-std = ["core", "compiler_builtins"] ``` Ceci indique à cargo qu'il doit recompiler les bibliothèques `core` et `compiler_builtins`. Celle-ci est nécessaire pour qu'elle est une dépendance de `core`. Afin de recompiler ces bibliothèques, cargo doit avoir accès au code source de Rust, que nous pouvons installer avec `rustup component add rust-src`.