Writing an OS in Rust

Philipp Oppermann's blog

A Freestanding Rust Binary

Contenu traduit : Ceci est une traduction communautaire de l'article A Freestanding Rust Binary. Il peut être incomplet, obsolète ou contenir des erreurs. Veuillez signaler les quelconques problèmes !

Traduit par : @Alekzus.

La première étape pour créer notre propre noyau de système d'exploitation est de créer un exécutable Rust qui ne relie pas la bibliothèque standard. Cela rend possible l'exécution du code Rust sur la "bare machine" sans système d'exploitation sous-jacent.

Ce blog est développé sur GitHub. Si vous avez un problème ou une question, veuillez ouvrir une issue. Vous pouvez aussi laisser un commentaire en bas de page. Le code source complet de cet article est disponible sur la branche post-01.

Table des matières

🔗Introduction

Pour écrire un noyau de système d'exploitation, nous avons besoin d'un code qui ne dépend pas de fonctionnalités de système d'exploitation. Cela signifie que nous ne pouvons pas utiliser les fils d'exécution, les fichiers, la mémoire sur le tas, le réseau, les nombres aléatoires, la sortie standard ou tout autre fonctionnalité nécessitant une abstraction du système d'exploitation ou un matériel spécifique. Cela a du sens, étant donné que nous essayons d'écrire notre propre OS et nos propres pilotes. Cela signifie que nous ne pouvons pas utiliser la majeure partie de la bibliothèque standard de Rust. Il y a néanmoins beaucoup de fonctionnalités de Rust que nous pouvons utiliser. Par exemple, nous pouvons utiliser les iterators, les closures, le pattern matching, l'option et le result, le string formatting, et bien-sûr l'ownership system. Ces fonctionnalités permettent l'écriture d'un noyeau d'une façon expressive et haut-niveau sans se soucier des [comportements indéfinis] ou de la sécurité de la mémoire.

Pour créer un noyau d'OS en Rust, nous devons créer un exécutable qui peut tourner sans système d'exploitation sous-jacent. Un tel exécutable est appelé “freestanding” (autoporté) ou “bare-metal”. Cet article décrit les étapes nécessaires pour créer un exécutable Rust autoporté et explique pourquoi ces étapes sont importantes. Si vous n'êtes intéressé que par un exemple minimal, vous pouvez aller au résumé.

🔗Désactiver la Bibliothèque Standard

Par défaut, tous les crates Rust relient la bibliothèque standard, qui dépend du système d'exploitation pour les fonctionnalités telles que les fils d'exécution, les fichiers ou le réseau. Elle dépend aussi de la bibliothèque standard de C libc, qui intéragit de près avec les services de l'OS. Comme notre plan est d'écrire un système d'exploitation, nous ne pouvons pas utiliser des bibliothèques dépendant de l'OS. Nous devons donc désactiver l'inclusion automatique de la bibliothèque standard en utilisant l'attribut no std.

Nous commencons par créer un nouveau projet d'application cargo. La manière la plus simple de faire est avec la ligne de commande :

cargo new blog_os --bin --edition 2018

J'ai nommé le projet blog_os, mais vous pouvez bien-sûr choisir le nom qu'il vous convient. Le flag --bin indique que nous voulons créer un exécutable (contrairement à une bibliothèque) et le flag --edition 2018 indique que nous voulons utiliser l'édition 2018 de Rust pour notre crate. Quand nous lançons la commande, cargo crée la structure de répertoire suivante pour nous :

blog_os
├── Cargo.toml
└── src
    └── main.rs

Le fichier Cargo.toml contient la configuration de la crate, par exemple le nom de la crate, l'auteur, le numéro de versionnage sémantique et les dépendances. Le fichier src/main.rs contient le module racine de notre crate et notre fonction main. Vous pouvez compiler votre crate avec cargo build et ensuite exécuter l'exécutable compilé blog_os dans le sous-dossier target/debug.

🔗L'Attribut no_std

Pour l'instant, notre crate relie la bilbiothèque standard implicitement. Désactivons cela en ajoutant l'attribut no std :

// main.rs

#![no_std]

fn main() {
    println!("Hello, world!");
}

Quand nous essayons maintenant de compiler (avec cargo build), l'erreur suivante se produit :

error: cannot find macro `println!` in this scope
 --> src/main.rs:4:5
  |
4 |     println!("Hello, world!");
  |     ^^^^^^^

La raison est que la macro println fait partie de la bibliothèque standard, que nous ne pouvons plus utiliser. Nous ne pouvons donc plus afficher de texte avec. Cela est logique, car println écrit dans la sortie standard, qui est un descripteur de fichier spécial fourni par le système d'eploitation.

Supprimons l'affichage et essayons à nouveau avec une fonction main vide :

// main.rs

#![no_std]

fn main() {}
> cargo build
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`

Maintenant le compilateur a besoin d'une fonction #[panic_handler] et d'un objet de langage.

🔗Implémentation de Panic

L'attribut panic_handler définit la fonction que le compilateur doit appeler lorsqu'un panic arrive. La bibliothèque standard fournit sa propre fonction de gestion de panic mais dans un environnement no_std, nous avons besoin de le définir nous-mêmes :

// dans main.rs

use core::panic::PanicInfo;

/// Cette fonction est appelée à chaque panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

Le paramètre PanicInfo contient le fichier et la ligne où le panic a eu lieu et le message optionnel de panic. La fonction ne devrait jamais retourner quoi que ce soit, elle est donc marquée comme fonction divergente en retournant le type “never” !. Nous ne pouvons pas faire grand chose dans cette fonction pour le moment, nous bouclons donc indéfiniment.

🔗L'Objet de Langage eh_personality

Les objets de langage sont des fonctions et des types spéciaux qui sont requis par le compilateur de manière interne. Par exemple, le trait Copy est un objet de langage qui indique au compilateur quels types possèdent la sémantique copy. Quand nous regardons l'implémentation du code, nous pouvons voir qu'il possède l'attribut spécial #[lang = copy] qui le définit comme étant un objet de langage.

Bien qu'il soit possible de fournir des implémentations personnalisées des objets de langage, cela ne devrait être fait qu'en dernier recours. La raison est que les objets de langages sont des détails d'implémentation très instables et qui ne sont même pas vérifiés au niveau de leur type (donc le compilateur ne vérifie même pas qu'une fonction possède les bons types d'arguments). Heureusement, il y a une manière plus robuste de corriger l'erreur d'objet de langage ci-dessus.

L'objet de langage eh_personality marque une fonction qui est utilisée pour l'implémentation du déroulement de pile. Par défaut, Rust utilise le déroulement de pile pour exécuter les destructeurs de chaque variable vivante sur la pile en cas de panic. Cela assure que toute la mémoire utilisée est libérée et permet au fil d'exécution parent d'attraper la panic et de continuer l'exécution. Le déroulement toutefois est un processus compliqué et nécessite des bibliothèques spécifiques à l'OS (libunwind pour Linux ou gestion structurée des erreurs pour Windows), nous ne voulons donc pas l'utiliser pour notre système d'exploitation.

🔗Désactiver le Déroulement

Il y a d'autres cas d'utilisation pour lesquels le déroulement n'est pas souhaité. Rust offre donc une option pour interrompre après un panic. Cela désactive la génération de symboles de déroulement et ainsi réduit considérablement la taille de l'exécutable. Il y a de multiples endroit où nous pouvons désactiver le déroulement. Le plus simple est d'ajouter les lignes suivantes dans notre Cargo.toml :

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

Cela configure la stratégie de panic à abort pour le profil dev (utilisé pour cargo build) et le profil release (utilisé pour cargo build --release). Maintenant l'objet de langage eh_personality ne devrait plus être requis.

Nous avons dorénavant corrigé les deux erreurs ci-dessus. Toutefois, si nous essayons de compiler, une autre erreur apparaît :

> cargo build
error: requires `start` lang_item

L'objet de langage start manque à notre programme. Il définit le point d'entrée.

🔗L'attribut start

On pourrait penser que la fonction main est la première fonction appelée lorsqu'un programme est exécuté. Toutefois, la plupart des langages ont un environnement d'exécution qui est responsable des tâches telles que le ramassage des miettes (ex: dans Java) ou les fils d'exécution logiciel (ex: les goroutines dans Go). Cet environnement doit être appelé avant main puisqu'il a besoin de s'initialiser.

Dans un exécutable Rust classique qui relie la bibliothèque standard, l'exécution commence dans une bibliothèque d'environnement d'exécution C appelé crt0 (“C runtime zero”). Elle configure l'environnement pour une application C. Cela comprend la création d'une pile et le placement des arguments dans les bons registres. L'environnement d'exécution C appelle ensuite le point d'entrée de l'environnement d'exécution de Rust, qui est marqué par l'objet de langage start. Rust possède un environnement d'exécution très minime, qui se charge de petites tâches telles que la configuration des guardes de dépassement de pile ou l'affichage de la trace d'appels lors d'un panic. L'environnement d'exécution finit par appeler la fonction main.

Notre exécutable autoporté n'a pas accès à l'environnement d'exécution de Rust ni à crt0. Nous avons donc besion de définir notre propre point d'entrée. Implémenter l'objet de langage start n'aiderait pas car nous aurions toujours besoin de crt0. Nous avons plutôt besoin de réécrire le point d'entrée de crt0 directement.

🔗Réécrire le Point d'Entrée

Pour indiquer au compilateur que nous ne voulons pas utiliser la chaîne de point d'entrée normale, nous ajoutons l'attribut #![no_main].

#![no_std]
#![no_main]

use core::panic::PanicInfo;

/// Cette fonction est appelée à chaque panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

Vous remarquerez peut-être que nous avons retiré la fonction main. La raison est que la présence de cette fonction n'a pas de sens sans un environnement d'exécution sous-jacent qui l'appelle. À la place, nous réécrivons le point d'entrée du système d'exploitation avec notre propre fonction _start :

#[no_mangle]
pub extern "C" fn _start() -> ! {
    loop {}
}

En utilisant l'attribut #[no_mangle], nous désactivons la décoration de nom pour assurer que le compilateur Rust crée une fonction avec le nom _start. Sans cet attribut, le compilateur génèrerait un symbol obscure _ZN3blog_os4_start7hb173fedf945531caE pour donner un nom unique à chaque fonction. L'attribut est nécessaire car nous avons besoin d'indiquer le nom de la fonction de point d'entrée à l'éditeur de lien (linker) dans l'étape suivante.

Nous devons aussi marquer la fonction avec extern C pour indiquer au compilateur qu'il devrait utiliser la convention de nommage de C pour cette fonction (au lieu de la convention de nommage de Rust non-spécifiée). Cette fonction se nomme _start car c'est le nom par défaut des points d'entrée pour la plupart des systèmes.

Le type de retour ! signifie que la fonction est divergente, c-à-d qu'elle n'a pas le droit de retourner quoi que ce soit. Cela est nécessaire car le point d'entrée n'est pas appelé par une fonction, mais invoqué directement par le système d'exploitation ou par le chargeur d'amorçage. Donc au lieu de retourner une valeur, le point d'entrée doit invoquer l'appel système exit du système d'exploitation. Dans notre cas, arrêter la machine pourrait être une action convenable, puisqu'il ne reste rien d'autre à faire si un exécutable autoporté s'arrête. Pour l'instant, nous remplissons la condition en bouclant indéfiniement.

Quand nous lançons cargo build, nous obtenons une erreur de linker.

🔗Erreurs de Linker

Le linker est un programme qui va transformer le code généré en exécutable. Comme le format de l'exécutable differt entre Linux, Windows et macOS, chaque système possède son propre linker qui lève une erreur différente. La cause fondamentale de cette erreur est la même : la configuration par défaut du linker part du principe que notre programme dépend de l'environnement d'exécution de C, ce qui n'est pas le cas.

Pour résoudre les erreurs, nous devons indiquer au linker qu'il ne doit pas inclure l'environnement d'exécution de C. Nous pouvons faire cela soit en passant un ensemble précis d'arguments, soit en compilant pour une cible bare metal.

🔗Compiler pour une Cible Bare Metal

Par défaut Rust essaie de compiler un exécutable qui est compatible avec l'environnment du système actuel. Par exemple, si vous utilisez Windows avec x86_64, Rust essaie de compiler un exécutable Windows .exe qui utilises des instructions x86_64. Cet environnement est appelé système "hôte".

Pour décrire plusieurs environnements, Rust utilise une chaîne de caractères appelée triplé cible. Vous pouvez voir le triplé cible de votre système hôte en lançant la commande rustc --version --verbose :

rustc 1.35.0-nightly (474e7a648 2019-04-07)
binary: rustc
commit-hash: 474e7a6486758ea6fc761893b1a49cd9076fb0ab
commit-date: 2019-04-07
host: x86_64-unknown-linux-gnu
release: 1.35.0-nightly
LLVM version: 8.0

La sortie ci-dessus provient d'un système Linux x86_64. Nous pouvons voir que le triplé host est x86_64-unknown-linux-gnu, qui inclut l'architecture du CPU (x86_64), le vendeur (unknown), le système d'exploitation (linux) et l'ABI (gnu).

En compilant pour notre triplé hôte, le compilateur Rust ainsi que le linker supposent qu'il y a un système d'exploitation sous-jacent comme Linux ou Windows qui utilise l'environnement d'exécution C par défaut, ce qui cause les erreurs de linker. Donc pour éviter ces erreurs, nous pouvons compiler pour un environnement différent sans système d'exploitation sous-jacent.

Un exemple d'un tel envrironnement est le triplé cible thumbv7em-none-eabihf, qui décrit un système ARM embarqué. Les détails ne sont pas importants, tout ce qui compte est que le triplé cible n'a pas de système d'exploitation sous-jacent, ce qui est indiqué par le none dans le triplé cible. Pour pouvoir compilé pour cette cible, nous avons besoin de l'ajouter dans rustup :

rustup target add thumbv7em-none-eabihf

Cela télécharge une copie de la bibliothèque standard (et core) pour le système. Maintenant nous pouvons compiler notre exécutable autoporté pour cette cible :

cargo build --target thumbv7em-none-eabihf

En donnant un argument --target, nous effectuons une [compilation croisée][cross_compile] de notre exécutable pour un système bare metal. Comme le système cible n'a pas de système d'exploitation, le linker n'essaie pas de lier l'environnement d'exécution C et notre compilation réussit sans erreur de linker.

C'est l'approche que nous allons utiliser pour construire notre noyau d'OS. Plutôt que thumbv7em-none-eabihf, nous allons utiliser une cible personnalisée qui décrit un environnement bare metal x86_64. Les détails seront expliqués dans le prochain article.

🔗Arguments du Linker

Au lieu de compiler pour un système bare metal, il est aussi possible de résoudre les erreurs de linker en passant un ensemble précis d'arguments au linker. Ce n'est pas l'approche que nous allons utiliser pour notre noyau. Cette section est donc optionnelle et fournis uniquement à titre de complétude. Cliquez sur "Arguments du Linker" ci-dessous pour montrer le contenu optionel.

Arguments du Linker

Dans cette section nous allons parler des erreurs de linker qui se produisent sur Linux, Windows et macOS. Nous allons aussi apprendre à résoudre ces erreurs en passant des arguments complémentaires au linker. À noter que le format de l'exécutable et le linker diffèrent entre les systèmes d'exploitation. Il faut donc un ensemble d'arguments différent pour chaque système.

🔗Linux

Sur Linux, voici l'erreur de linker qui se produit (raccourcie) :

error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" […]
  = note: /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
          (.text+0x12): undefined reference to `__libc_csu_fini'
          /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
          (.text+0x19): undefined reference to `__libc_csu_init'
          /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
          (.text+0x25): undefined reference to `__libc_start_main'
          collect2: error: ld returned 1 exit status

Le problème est que le linker inclut par défaut la routine de démarrage de l'environnement d'exécution de C, qui est aussi appelée _start. Elle requiert des symboles de la bibliothèque standard de C libc que nous n'incluons pas à cause de l'attribut no_std. Le linker ne peut donc pas résoudre ces références. Pour résoudre cela, nous pouvons indiquer au linker qu'il ne devrait pas lier la routine de démarrage de C en passant l'argument -nostartfiles.

Une façon de passer des attributs au linker via cargo est la commande cargo rustc. Cette commande se comporte exactement comme cargo build, mais permet aussi de donner des options à rustc, le compilateur Rust sous-jacent. rustc possède le flag -C link-arg, qui donne un argument au linker. Combinés, notre nouvelle commande ressemble à ceci :

cargo rustc -- -C link-arg=-nostartfiles

Dorénavant notre crate compile en tant qu'exécutable Linux autoporté !

Nous n'avions pas besoin de spécifier le nom de notre point d'entrée de façon explicite car le linker cherche par défaut une fonction nommée _start.

🔗Windows

Sur Windows, une erreur de linker différente se produit (raccourcie) :

error: linking with `link.exe` failed: exit code: 1561
  |
  = note: "C:\\Program Files (x86)\\…\\link.exe" […]
  = note: LINK : fatal error LNK1561: entry point must be defined

Cette erreur signifie que le linker ne peut pas trouver le point d'entrée. Sur Windows, le nom par défaut du point d'entrée dépend du sous-système utilisé. Pour le sous-système CONSOLE, le linker cherche une fonction nommée mainCRTStartup et pour le sous-système WINDOWS, il cherche une fonction nomée WinMainCRTStartup. Pour réécrire la valeur par défaut et indiquer au linker de chercher notre fonction _start à la place, nous pouvons donner l'argument /ENTRY au linker :

cargo rustc -- -C link-arg=/ENTRY:_start

Vu le format d'argument différent nous pouvons clairement voir que le linker Windows est un programme totalement différent du linker Linux.

Maintenant une erreur de linker différente se produit :

error: linking with `link.exe` failed: exit code: 1221
  |
  = note: "C:\\Program Files (x86)\\…\\link.exe" […]
  = note: LINK : fatal error LNK1221: a subsystem can't be inferred and must be
          defined

Cette erreur se produit car les exécutables Windows peuvent utiliser différents sous-systèmes. Pour les programmes normaux, ils sont inférés en fonction du nom du point d'entrée : s'il est nommé main, le sous-système CONSOLE est utilisé. Si le point d'entrée est nommé WinMain, alors le sous-sytème WINDOWS est utilisé. Comme notre fonction _start possède un nom différent, nous devons préciser le sous-système explicitement :

cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"

Ici nous utilisons le sous-système CONSOLE, mais le sous-système WINDOWS pourrait fonctionner aussi. Au lieu de donner -C link-arg plusieurs fois, nous utilisons -C link-args qui utilise des arguments séparés par des espaces.

Avec cette commande, notre exécutable devrait compiler avec succès sous Windows.

🔗macOS

Sur macOS, voici l'erreur de linker qui se produit (raccourcie) :

error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" […]
  = note: ld: entry point (_main) undefined. for architecture x86_64
          clang: error: linker command failed with exit code 1 […]

Cette erreur nous indique que le linker ne peut pas trouver une fonction de point d'entrée avec le nom par défaut main (pour une quelconque raison, toutes les fonctions sur macOS sont précédées de _). Pour configurer le point d'entrée sur notre fonction _start, nous donnons l'argument -e au linker :

cargo rustc -- -C link-args="-e __start"

L'argument -e spécifie le nom de la fonction de point d'entrée. Comme toutes les fonctions ont un préfixe supplémentaire _ sur macOS, nous devons configurer le point d'entrée comme étant __start au lieu de _start.

Maintenant l'erreur de linker suivante se produit :

error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" […]
  = note: ld: dynamic main executables must link with libSystem.dylib
          for architecture x86_64
          clang: error: linker command failed with exit code 1 […]

macOS ne supporte pas officiellement les bibliothèques liées de façon statique et necéessite que les programmes lient la bibliothèque libSystem par défaut. Pour réécrire ceci et lier une bibliothèque statique, nous donnons l'argument -static au linker :

cargo rustc -- -C link-args="-e __start -static"

Cela ne suffit toujours pas, une troisième erreur de linker se produit :

error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" […]
  = note: ld: library not found for -lcrt0.o
          clang: error: linker command failed with exit code 1 […]

Cette erreur se produit car les programmes sous macOS lient crt0 (“C runtime zero”) par défaut. Ceci est similaire à l'erreur que nous avions eu sous Linux et peut aussi être résolue en ajoutant l'argument -nostartfiles au linker :

cargo rustc -- -C link-args="-e __start -static -nostartfiles"

Maintenant notre programme compile avec succès sous macOS.

🔗Unifier les Commandes de Compilation

À cet instant nous avons différentes commandes de compilation en fonction de la plateforme hôte, ce qui n'est pas idéal. Pour éviter cela, nous pouvons créer un ficher nommé .cargo/config.toml qui contient les arguments spécifiques aux plateformes :

# dans .cargo/config.toml

[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-arg=-nostartfiles"]

[target.'cfg(target_os = "windows")']
rustflags = ["-C", "link-args=/ENTRY:_start /SUBSYSTEM:console"]

[target.'cfg(target_os = "macos")']
rustflags = ["-C", "link-args=-e __start -static -nostartfiles"]

La clé rustflags contient des arguments qui sont automatiquement ajoutés à chaque appel de rustc. Pour plus d'informations sur le fichier .cargo/config.toml, allez voir la documentation officielle

Maintenant notre programme devrait être compilable sur les trois plateformes avec un simple cargo build.

🔗Devriez-vous Faire Ça ?

Bien qu'il soit possible de compiler un exécutable autoporté pour Linux, Windows et macOS, ce n'est probablement pas une bonne idée. La raison est que notre exécutable s'attend toujours à trouver certaines choses, par exemple une pile initialisée lorsque la fonction _start est appelée. Sans l'environnement d'exécution C, certains de ces conditions peuvent ne pas être remplies, ce qui pourrait faire planter notre programme, avec par exemple une erreur de segmentation.

Si vous voulez créer un exécutable minimal qui tourne sur un système d'exploitation existant, include libc et mettre l'attribut #[start] come décrit ici semble être une meilleure idée.

🔗Résumé

Un exécutable Rust autoporté minimal ressemble à ceci :

src/main.rs:

#![no_std] // ne pas lier la bibliothèque standard Rust
#![no_main] // désactiver tous les points d'entrée au niveau de Rust

use core::panic::PanicInfo;

#[no_mangle] // ne pas décorer le nom de cette fonction
pub extern "C" fn _start() -> ! {
    // cette fonction est le point d'entrée, comme le linker cherche une fonction
    // nomée `_start` par défaut
    loop {}
}

/// Cette fonction est appelée à chaque panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

Cargo.toml:

[package]
name = "crate_name"
version = "0.1.0"
authors = ["Author Name <author@example.com>"]

# le profile utilisé pour `cargo build`
[profile.dev]
panic = "abort" # désactive le déroulement de la pile lors d'un panic

# le profile utilisé pour `cargo build --release`
[profile.release]
panic = "abort" # désactive le déroulement de la pile lors d'un panic

Pour compiler cet exécutable, nous devons compiler pour une cible bare metal telle que thumbv7em-none-eabihf :

cargo build --target thumbv7em-none-eabihf

Sinon, nous pouvons aussi compiler pour le système hôte en donnant des arguments supplémentaires pour le linker :

# Linux
cargo rustc -- -C link-arg=-nostartfiles
# Windows
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
# macOS
cargo rustc -- -C link-args="-e __start -static -nostartfiles"

À noter que ceci est juste un exemple minimal d'un exécutable Rust autoporté. Cet exécutable s'attend à de nombreuses choses, comme par exemple le fait qu'une pile soit initialisée lorsque la fonction _start est appelée. Donc pour une réelle utilisation d'un tel exécutable, davantages d'étapes sont requises.

🔗Et ensuite ?

Le poste suivant explique les étapes nécessaires pour transformer notre exécutable autoporté minimal en noyau de système d'opération. Cela comprend la création d'une cible personnalisée, l'intégration de notre exécutable avec un chargeur d'amorçage et l'apprentissage de comment imprimer quelque chose sur l'écran.



Commentaires

Do you have a problem, want to share feedback, or discuss further ideas? Feel free to leave a comment here! Please stick to English and follow Rust's code of conduct. This comment thread directly maps to a discussion on GitHub, so you can also comment there if you prefer.

Instead of authenticating the giscus application, you can also comment directly on GitHub.

Veuillez commenter en Anglais si possible.