mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 22:37:49 +00:00
Compare commits
49 Commits
68d0c946f4
...
edition-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd3550ea87 | ||
|
|
ecb60ec326 | ||
|
|
8a1267477a | ||
|
|
ce01059620 | ||
|
|
4d0c3ac188 | ||
|
|
d565cd125b | ||
|
|
ca86085360 | ||
|
|
5f3d38884c | ||
|
|
f557d1c698 | ||
|
|
0c248d027e | ||
|
|
2cf0675a2d | ||
|
|
916ad36e78 | ||
|
|
3c2e91fa4e | ||
|
|
c9683a2cd9 | ||
|
|
514736b1d8 | ||
|
|
647b509971 | ||
|
|
1118350b16 | ||
|
|
fb096a7484 | ||
|
|
8a41fd65bf | ||
|
|
50802c8332 | ||
|
|
ba410f40ba | ||
|
|
a119d36cc9 | ||
|
|
9080e69a09 | ||
|
|
2676d69c31 | ||
|
|
c31f3c2728 | ||
|
|
5799263124 | ||
|
|
ded60de8d0 | ||
|
|
171956adc8 | ||
|
|
e2a4464835 | ||
|
|
f0fe3929ab | ||
|
|
c728cf8225 | ||
|
|
f38c11ae8e | ||
|
|
a1b195ede0 | ||
|
|
cfd31a977d | ||
|
|
06dd5edb3f | ||
|
|
685f55dd31 | ||
|
|
0b6b053c54 | ||
|
|
e0464fbd44 | ||
|
|
5e88b86d1e | ||
|
|
e6b507e6d2 | ||
|
|
5681d3f0f7 | ||
|
|
5baf50a5b4 | ||
|
|
5d63300512 | ||
|
|
739d9e1a3c | ||
|
|
8a1063df5f | ||
|
|
d2c6b281c8 | ||
|
|
3900ddeb2c | ||
|
|
a7f4647e73 | ||
|
|
a7cfc562e9 |
@@ -51,6 +51,9 @@ translation_contributors = "With contributions from"
|
||||
word_separator = "and"
|
||||
|
||||
# Chinese (simplified)
|
||||
[languages.zh-CN]
|
||||
title = "Writing an OS in Rust"
|
||||
description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code."
|
||||
[languages.zh-CN.translations]
|
||||
lang_name = "Chinese (simplified)"
|
||||
toc = "目录"
|
||||
@@ -66,6 +69,9 @@ translation_contributors = "With contributions from"
|
||||
word_separator = "和"
|
||||
|
||||
# Chinese (traditional)
|
||||
[languages.zh-TW]
|
||||
title = "Writing an OS in Rust"
|
||||
description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code."
|
||||
[languages.zh-TW.translations]
|
||||
lang_name = "Chinese (traditional)"
|
||||
toc = "目錄"
|
||||
@@ -81,6 +87,9 @@ translation_contributors = "With contributions from"
|
||||
word_separator = "和"
|
||||
|
||||
# Japanese
|
||||
[languages.ja]
|
||||
title = "Writing an OS in Rust"
|
||||
description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code."
|
||||
[languages.ja.translations]
|
||||
lang_name = "Japanese"
|
||||
toc = "目次"
|
||||
@@ -96,6 +105,9 @@ translation_contributors = "With contributions from"
|
||||
word_separator = "及び"
|
||||
|
||||
# Persian
|
||||
[languages.fa]
|
||||
title = "Writing an OS in Rust"
|
||||
description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code."
|
||||
[languages.fa.translations]
|
||||
lang_name = "Persian"
|
||||
toc = "فهرست مطالب"
|
||||
@@ -111,6 +123,9 @@ translation_contributors = "With contributions from"
|
||||
word_separator = "و"
|
||||
|
||||
# Russian
|
||||
[languages.ru]
|
||||
title = "Writing an OS in Rust"
|
||||
description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code."
|
||||
[languages.ru.translations]
|
||||
lang_name = "Russian"
|
||||
toc = "Содержание"
|
||||
@@ -126,6 +141,9 @@ translation_contributors = "With contributions from"
|
||||
word_separator = "и"
|
||||
|
||||
# French
|
||||
[languages.fr]
|
||||
title = "Writing an OS in Rust"
|
||||
description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code."
|
||||
[languages.fr.translations]
|
||||
lang_name = "French"
|
||||
toc = "Table des matières"
|
||||
@@ -141,6 +159,9 @@ translation_contributors = "With contributions from"
|
||||
word_separator = "et"
|
||||
|
||||
# Korean
|
||||
[languages.ko]
|
||||
title = "Writing an OS in Rust"
|
||||
description = "This blog series creates a small operating system in the Rust programming language. Each post is a small tutorial and includes all needed code."
|
||||
[languages.ko.translations]
|
||||
lang_name = "Korean"
|
||||
toc = "목차"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
+++
|
||||
title = "Un binaire Rust autonome"
|
||||
title = "Un binaire Rust autoporté"
|
||||
weight = 1
|
||||
path = "fr/freestanding-rust-binary"
|
||||
date = 2018-02-10
|
||||
@@ -29,7 +29,7 @@ Ce blog est développé sur [GitHub]. Si vous avez un problème ou une question,
|
||||
|
||||
## 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 noyau d'une façon expressive et haut-niveau sans se soucier des [comportements indéfinis] ou de la [sécurité de la mémoire].
|
||||
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 noyau d'une façon expressive et haut-niveau sans se soucier des [comportements indéfinis] ou de la [sécurité de la mémoire].
|
||||
|
||||
[option]: https://doc.rust-lang.org/core/option/
|
||||
[result]:https://doc.rust-lang.org/core/result/
|
||||
@@ -47,18 +47,18 @@ Cet article décrit les étapes nécessaires pour créer un exécutable Rust aut
|
||||
|
||||
## 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`].
|
||||
Par défaut, toutes les crates Rust sont liées à la bibliothèque standard, qui repose sur les fonctionnalités du système d’exploitation telles que les fils d'exécution, les fichiers et la connectivité réseau. Elle est également liée à la bibliothèque standard C `libc`, qui interagit étroitement avec les services fournis par 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`].
|
||||
|
||||
[bibliothèque standard]: https://doc.rust-lang.org/std/
|
||||
[attribut `no std`]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html
|
||||
|
||||
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 :
|
||||
Nous commençons 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 :
|
||||
J'ai nommé le projet `blog_os`, mais vous pouvez évidemment 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 :
|
||||
|
||||
[édition 2018]: https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html
|
||||
|
||||
@@ -75,7 +75,7 @@ Le fichier `Cargo.toml` contient la configuration de la crate, par exemple le no
|
||||
|
||||
### L'Attribut `no_std`
|
||||
|
||||
Pour l'instant, notre crate relie la bilbiothèque standard implicitement. Désactivons cela en ajoutant l'[attribut `no std`] :
|
||||
Pour l'instant, notre crate relie la bibliothèque standard implicitement. Désactivons cela en ajoutant l'[attribut `no std`] :
|
||||
|
||||
```rust
|
||||
// main.rs
|
||||
|
||||
@@ -383,7 +383,7 @@ error: linking with `cc` failed: exit code: 1
|
||||
cargo rustc -- -C link-args="-e __start"
|
||||
```
|
||||
|
||||
`-e` 表示肉口點的函式名稱,然後由於 macOS 上所有的函式都會加上前綴 `_`,我們需要設置入口點為 `__start` 而不是 `_start`。
|
||||
`-e` 表示入口點的函式名稱,然後由於 macOS 上所有的函式都會加上前綴 `_`,我們需要設置入口點為 `__start` 而不是 `_start`。
|
||||
|
||||
接下來會出現另一個連結器錯誤:
|
||||
|
||||
|
||||
@@ -323,7 +323,7 @@ build-std-features = ["compiler-builtins-mem"]
|
||||
در پشت صحنه، این پرچم [ویژگی `mem`] از کریت `compiler_builtins` را فعال میکند. اثرش این است که صفت `[no_mangle]#` بر روی [پیادهسازی `memcpy` و بقیه موارد] از کریت اعمال میشود، که آنها در دسترس لینکر قرار میدهد. شایان ذکر است که این توابع در حال حاضر [بهینه نشدهاند]، بنابراین ممکن است عملکرد آنها در بهترین حالت نباشد، اما حداقل صحیح هستند. برای `x86_64` ، یک pull request باز برای [بهینه سازی این توابع با استفاده از دستورالعملهای خاص اسمبلی][memcpy rep movsb] وجود دارد.
|
||||
|
||||
[ویژگی `mem`]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L51-L52
|
||||
[پیادهسازی `memcpy` و بقیه موارد]: (https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69)
|
||||
[پیادهسازی `memcpy` و بقیه موارد]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
|
||||
[بهینه نشدهاند]: https://github.com/rust-lang/compiler-builtins/issues/339
|
||||
[memcpy rep movsb]: https://github.com/rust-lang/compiler-builtins/pull/365
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ build-std-features = ["compiler-builtins-mem"]
|
||||
このとき、裏で`compiler_builtins`クレートの[`mem`機能][`mem` feature]が有効化されています。これにより、このクレートの[`memcpy`などの実装][`memcpy` etc. implementations]に`#[no_mangle]`アトリビュートが適用され、リンカがこれらを利用できるようになっています。
|
||||
|
||||
[`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L51-L52
|
||||
[`memcpy` etc. implementations]: (https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69)
|
||||
[`memcpy` etc. implementations]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
|
||||
|
||||
この変更をもって、私達のカーネルはコンパイラに必要とされているすべての関数の有効な実装を手に入れたので、コードがもっと複雑になっても変わらずコンパイルできるでしょう。
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ UEFI 표준으로 동작하는 최신 기기들도 가상 BIOS를 지원하기
|
||||
지난 포스트에서 우리는 `cargo`를 통해 freestanding 실행파일을 만들었었는데, 호스트 시스템의 운영체제에 따라 프로그램 실행 시작 지점의 이름 및 컴파일 인자들을 다르게 설정해야 했습니다. 이것은 `cargo`가 기본적으로 _호스트 시스템_ (여러 분이 실행 중인 컴퓨터 시스템) 을 목표로 빌드하기 때문이었습니다. 우리의 커널은 다른 운영체제 (예를 들어 Windows) 위에서 실행될 것이 아니기에, 호스트 시스템에 설정 값을 맞추는 대신에 우리가 명확히 정의한 _목표 시스템 (target system)_ 을 목표로 컴파일할 것입니다.
|
||||
|
||||
### Rust Nightly 설치하기 {#installing-rust-nightly}
|
||||
Rust는 _stable_, _beta_ 그리고 _nightly_ 이렇게 세 가지의 채널을 통해 배포됩니다. Rust Book에 [세 채널들 간의 차이에 대해 잘 정리한 챕터]((https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains))가 있습니다. 운영체제를 빌드하기 위해서는 _nightly_ 채널에서만 제공하는 실험적인 기능들을 이용해야 하기에 _nightly_ 버전의 Rust를 설치하셔야 합니다.
|
||||
Rust는 _stable_, _beta_ 그리고 _nightly_ 이렇게 세 가지의 채널을 통해 배포됩니다. Rust Book에 [세 채널들 간의 차이에 대해 잘 정리한 챕터](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains)가 있습니다. 운영체제를 빌드하기 위해서는 _nightly_ 채널에서만 제공하는 실험적인 기능들을 이용해야 하기에 _nightly_ 버전의 Rust를 설치하셔야 합니다.
|
||||
|
||||
여러 버전의 Rust 언어 설치 파일들을 관리할 때 [rustup]을 사용하는 것을 강력 추천합니다. rustup을 통해 nightly, beta 그리고 stable 컴파일러들을 모두 설치하고 업데이트할 수 있습니다. `rustup override set nightly` 명령어를 통해 현재 디렉토리에서 항상 nightly 버전의 Rust를 사용하도록 설정할 수 있습니다.
|
||||
`rust-toolchain`이라는 파일을 프로젝트 루트 디렉토리에 만들고 이 파일에 `nightly`라는 텍스트를 적어 놓아도 같은 효과를 볼 수 있습니다. `rustc --version` 명령어를 통해 현재 nightly 버전이 설치되어 있는지 확인할 수 있습니다 (출력되는 버전 넘버가 `-nightly`라는 텍스트로 끝나야 합니다).
|
||||
|
||||
@@ -49,7 +49,7 @@ pub extern "C" fn _start() -> ! {
|
||||
|
||||
// trigger a page fault
|
||||
unsafe {
|
||||
*(0xdeadbeef as *mut u64) = 42;
|
||||
*(0xdeadbeef as *mut u8) = 42;
|
||||
};
|
||||
|
||||
// as before
|
||||
|
||||
@@ -45,7 +45,7 @@ pub extern "C" fn _start() -> ! {
|
||||
|
||||
// ページフォルトを起こす
|
||||
unsafe {
|
||||
*(0xdeadbeef as *mut u64) = 42;
|
||||
*(0xdeadbeef as *mut u8) = 42;
|
||||
};
|
||||
|
||||
// 前回同様
|
||||
|
||||
@@ -48,7 +48,7 @@ pub extern "C" fn _start() -> ! {
|
||||
|
||||
// 페이지 폴트 일으키기
|
||||
unsafe {
|
||||
*(0xdeadbeef as *mut u64) = 42;
|
||||
*(0xdeadbeef as *mut u8) = 42;
|
||||
};
|
||||
|
||||
// 이전과 동일
|
||||
|
||||
@@ -42,7 +42,7 @@ pub extern "C" fn _start() -> ! {
|
||||
|
||||
// trigger a page fault
|
||||
unsafe {
|
||||
*(0xdeadbeef as *mut u64) = 42;
|
||||
*(0xdeadbeef as *mut u8) = 42;
|
||||
};
|
||||
|
||||
// as before
|
||||
@@ -374,9 +374,9 @@ pub fn init() {
|
||||
}
|
||||
```
|
||||
|
||||
We reload the code segment register using [`set_cs`] and load the TSS using [`load_tss`]. The functions are marked as `unsafe`, so we need an `unsafe` block to invoke them. The reason is that it might be possible to break memory safety by loading invalid selectors.
|
||||
We reload the code segment register using [`CS::set_reg`] and load the TSS using [`load_tss`]. The functions are marked as `unsafe`, so we need an `unsafe` block to invoke them. The reason is that it might be possible to break memory safety by loading invalid selectors.
|
||||
|
||||
[`set_cs`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/segmentation/fn.set_cs.html
|
||||
[`CS::set_reg`]: https://docs.rs/x86_64/0.14.5/x86_64/instructions/segmentation/struct.CS.html#method.set_reg
|
||||
[`load_tss`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/tables/fn.load_tss.html
|
||||
|
||||
Now that we have loaded a valid TSS and interrupt stack table, we can set the stack index for our double fault handler in the IDT:
|
||||
|
||||
@@ -47,7 +47,7 @@ pub extern "C" fn _start() -> ! {
|
||||
|
||||
// trigger a page fault
|
||||
unsafe {
|
||||
*(0xdeadbeef as *mut u64) = 42;
|
||||
*(0xdeadbeef as *mut u8) = 42;
|
||||
};
|
||||
|
||||
// as before
|
||||
|
||||
@@ -322,7 +322,7 @@ pub extern "C" fn _start() -> ! {
|
||||
blog_os::init();
|
||||
|
||||
// new
|
||||
let ptr = 0xdeadbeaf as *mut u32;
|
||||
let ptr = 0xdeadbeaf as *mut u8;
|
||||
unsafe { *ptr = 42; }
|
||||
|
||||
// as before
|
||||
@@ -347,7 +347,7 @@ pub extern "C" fn _start() -> ! {
|
||||
```rust
|
||||
// Note: The actual address might be different for you. Use the address that
|
||||
// your page fault handler reports.
|
||||
let ptr = 0x2031b2 as *mut u32;
|
||||
let ptr = 0x2031b2 as *mut u8;
|
||||
|
||||
// read from a code page
|
||||
unsafe { let x = *ptr; }
|
||||
|
||||
@@ -329,7 +329,7 @@ pub extern "C" fn _start() -> ! {
|
||||
blog_os::init();
|
||||
|
||||
// ここを追加
|
||||
let ptr = 0xdeadbeaf as *mut u32;
|
||||
let ptr = 0xdeadbeaf as *mut u8;
|
||||
unsafe { *ptr = 42; }
|
||||
|
||||
// ここはこれまでと同じ
|
||||
@@ -354,7 +354,7 @@ pub extern "C" fn _start() -> ! {
|
||||
```rust
|
||||
// 注意:実際のアドレスは個々人で違うかもしれません。
|
||||
// あなたのページフォルトハンドラが報告した値を使ってください。
|
||||
let ptr = 0x2031b2 as *mut u32;
|
||||
let ptr = 0x2031b2 as *mut u8;
|
||||
|
||||
// コードページから読み込む
|
||||
unsafe { let x = *ptr; }
|
||||
|
||||
@@ -316,7 +316,7 @@ pub extern "C" fn _start() -> ! {
|
||||
blog_os::init();
|
||||
|
||||
// new
|
||||
let ptr = 0xdeadbeaf as *mut u32;
|
||||
let ptr = 0xdeadbeaf as *mut u8;
|
||||
unsafe { *ptr = 42; }
|
||||
|
||||
// as before
|
||||
@@ -341,7 +341,7 @@ We see that the current instruction pointer is `0x2031b2`, so we know that this
|
||||
```rust
|
||||
// Note: The actual address might be different for you. Use the address that
|
||||
// your page fault handler reports.
|
||||
let ptr = 0x2031b2 as *mut u32;
|
||||
let ptr = 0x2031b2 as *mut u8;
|
||||
|
||||
// read from a code page
|
||||
unsafe { let x = *ptr; }
|
||||
|
||||
@@ -325,7 +325,7 @@ pub extern "C" fn _start() -> ! {
|
||||
blog_os::init();
|
||||
|
||||
// new
|
||||
let ptr = 0xdeadbeaf as *mut u32;
|
||||
let ptr = 0xdeadbeaf as *mut u8;
|
||||
unsafe { *ptr = 42; }
|
||||
|
||||
// as before
|
||||
@@ -350,7 +350,7 @@ pub extern "C" fn _start() -> ! {
|
||||
```rust
|
||||
// Note: The actual address might be different for you. Use the address that
|
||||
// your page fault handler reports.
|
||||
let ptr = 0x2031b2 as *mut u32;
|
||||
let ptr = 0x2031b2 as *mut u8;
|
||||
|
||||
// read from a code page
|
||||
unsafe { let x = *ptr; }
|
||||
|
||||
@@ -214,7 +214,7 @@ Since we are compiling for a custom target, we can't use the precompiled version
|
||||
|
||||
[unstable]
|
||||
build-std = ["core", "compiler_builtins", "alloc"]
|
||||
````
|
||||
```
|
||||
|
||||
Now the compiler will recompile and include the `alloc` crate in our kernel.
|
||||
|
||||
@@ -223,18 +223,12 @@ The reason that the `alloc` crate is disabled by default in `#[no_std]` crates i
|
||||
```
|
||||
error: no global memory allocator found but one is required; link to std or add
|
||||
#[global_allocator] to a static item that implements the GlobalAlloc trait.
|
||||
|
||||
error: `#[alloc_error_handler]` function required, but not found
|
||||
```
|
||||
|
||||
The first error occurs because the `alloc` crate requires a heap allocator, which is an object that provides the `allocate` and `deallocate` functions. In Rust, heap allocators are described by the [`GlobalAlloc`] trait, which is mentioned in the error message. To set the heap allocator for the crate, the `#[global_allocator]` attribute must be applied to a `static` variable that implements the `GlobalAlloc` trait.
|
||||
|
||||
The second error occurs because calls to `allocate` can fail, most commonly when there is no more memory available. Our program must be able to react to this case, which is what the `#[alloc_error_handler]` function is for.
|
||||
The error occurs because the `alloc` crate requires a heap allocator, which is an object that provides the `allocate` and `deallocate` functions. In Rust, heap allocators are described by the [`GlobalAlloc`] trait, which is mentioned in the error message. To set the heap allocator for the crate, the `#[global_allocator]` attribute must be applied to a `static` variable that implements the `GlobalAlloc` trait.
|
||||
|
||||
[`GlobalAlloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html
|
||||
|
||||
We will describe these traits and attributes in detail in the following sections.
|
||||
|
||||
### The `GlobalAlloc` Trait
|
||||
|
||||
The [`GlobalAlloc`] trait defines the functions that a heap allocator must provide. The trait is special because it is almost never used directly by the programmer. Instead, the compiler will automatically insert the appropriate calls to the trait methods when using the allocation and collection types of `alloc`.
|
||||
@@ -329,32 +323,7 @@ static ALLOCATOR: Dummy = Dummy;
|
||||
|
||||
Since the `Dummy` allocator is a [zero-sized type], we don't need to specify any fields in the initialization expression.
|
||||
|
||||
When we now try to compile it, the first error should be gone. Let's fix the remaining second error:
|
||||
|
||||
```
|
||||
error: `#[alloc_error_handler]` function required, but not found
|
||||
```
|
||||
|
||||
### The `#[alloc_error_handler]` Attribute
|
||||
|
||||
As we learned when discussing the `GlobalAlloc` trait, the `alloc` function can signal an allocation error by returning a null pointer. The question is: how should the Rust runtime react to such an allocation failure? This is where the `#[alloc_error_handler]` attribute comes in. It specifies a function that is called when an allocation error occurs, similar to how our panic handler is called when a panic occurs.
|
||||
|
||||
Let's add such a function to fix the compilation error:
|
||||
|
||||
```rust
|
||||
// in src/lib.rs
|
||||
|
||||
#![feature(alloc_error_handler)] // at the top of the file
|
||||
|
||||
#[alloc_error_handler]
|
||||
fn alloc_error_handler(layout: alloc::alloc::Layout) -> ! {
|
||||
panic!("allocation error: {:?}", layout)
|
||||
}
|
||||
```
|
||||
|
||||
The `alloc_error_handler` function is still unstable, so we need a feature gate to enable it. The function receives a single argument: the `Layout` instance that was passed to `alloc` when the allocation failure occurred. There's nothing we can do to resolve the failure, so we just panic with a message that contains the `Layout` instance.
|
||||
|
||||
With this function, the compilation errors should be fixed. Now we can use the allocation and collection types of `alloc`. For example, we can use a [`Box`] to allocate a value on the heap:
|
||||
With this static, the compilation errors should be fixed. Now we can use the allocation and collection types of `alloc`. For example, we can use a [`Box`] to allocate a value on the heap:
|
||||
|
||||
[`Box`]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html
|
||||
|
||||
@@ -380,11 +349,11 @@ fn kernel_main(boot_info: &'static BootInfo) -> ! {
|
||||
|
||||
Note that we need to specify the `extern crate alloc` statement in our `main.rs` too. This is required because the `lib.rs` and `main.rs` parts are treated as separate crates. However, we don't need to create another `#[global_allocator]` static because the global allocator applies to all crates in the project. In fact, specifying an additional allocator in another crate would be an error.
|
||||
|
||||
When we run the above code, we see that our `alloc_error_handler` function is called:
|
||||
When we run the above code, we see that a panic occurs:
|
||||
|
||||

|
||||
|
||||
The error handler is called because the `Box::new` function implicitly calls the `alloc` function of the global allocator. Our dummy allocator always returns a null pointer, so every allocation fails. To fix this, we need to create an allocator that actually returns usable memory.
|
||||
The panic occurs because the `Box::new` function implicitly calls the `alloc` function of the global allocator. Our dummy allocator always returns a null pointer, so every allocation fails. To fix this, we need to create an allocator that actually returns usable memory.
|
||||
|
||||
## Creating a Kernel Heap
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ translation_contributors = ["asami-kawasaki", "Foo-x"]
|
||||
[コールスタック]: https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%BC%E3%83%AB%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF
|
||||
[コンテキスト・スイッチ (context switch)]: https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%B3%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88%E3%82%B9%E3%82%A4%E3%83%83%E3%83%81
|
||||
|
||||
コールスタックは非常に大きくなる可能性があるため、OSは通常、各タスクのスイッチでコールスタックの内容をバックアップする代わりに、各タスクに個別のコールスタックを設定します。このような独立したスタックを持つタスクは、[_thread of execution_](略して**スレッド**)と呼ばれます。タスクごとに独立したスタックを使用することで、コンテキスト・スイッチの際に保存する必要があるのはレジスタの内容だけになります(プログラム・カウンタとスタック・ポインタを含む)。この方法を取ることで、コンテキスト・スイッチの性能上のオーバーヘッドが最小限になります。これは、コンテキスト・スイッチが1秒間に100回も行われることがあるため、非常に重要なことです。
|
||||
コールスタックは非常に大きくなる可能性があるため、OSは通常、各タスクのスイッチでコールスタックの内容をバックアップする代わりに、各タスクに個別のコールスタックを設定します。このような独立したスタックを持つタスクは、[略して**スレッド**][_thread of execution_]と呼ばれます。タスクごとに独立したスタックを使用することで、コンテキスト・スイッチの際に保存する必要があるのはレジスタの内容だけになります(プログラム・カウンタとスタック・ポインタを含む)。この方法を取ることで、コンテキスト・スイッチの性能上のオーバーヘッドが最小限になります。これは、コンテキスト・スイッチが1秒間に100回も行われることがあるため、非常に重要なことです。
|
||||
|
||||
[_thread of execution_]: https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%AC%E3%83%83%E3%83%89_(%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF)
|
||||
|
||||
|
||||
@@ -445,7 +445,7 @@ Afterwards, you can run the tools through `rust-nm`, `rust-objdump`, and `rust-s
|
||||
|
||||
[`cargo-binutils`]: https://github.com/rust-embedded/cargo-binutils
|
||||
|
||||
### `nm`
|
||||
### List Symbols using `nm`
|
||||
|
||||
We defined a `_start` function as the entry point of our kernel.
|
||||
To verify that it is properly exposed in the executable, we can run `nm` to list all the symbols defined in the executable:
|
||||
@@ -463,7 +463,7 @@ If we comment out the `_start` function or if we remove the `#[no_mangle]` attri
|
||||
|
||||
This way we can ensure that we set the `_start` function correctly.
|
||||
|
||||
### `objdump`
|
||||
### Inspect ELF File using `objdump`
|
||||
|
||||
The `objdump` tool can inspect different parts of executables that use the [ELF file format]. This is the file format that the `x86_64-unknown-none` target uses, so we can use `objdump` to inspect our kernel executable.
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ UEFI, in contrast, is more modern and has much more features, but also more comp
|
||||
### BIOS
|
||||
|
||||
Almost all x86 systems have support for BIOS booting, including most UEFI-based machines that support an emulated BIOS.
|
||||
This is great, because you can use the same boot logic across all machines from the last centuries.
|
||||
This is great, because you can use the same boot logic across all machines from the last century.
|
||||
The drawback is that the standard is very old, for example the CPU is put into a 16-bit compatibility mode called [real mode] before booting so that archaic bootloaders from the 1980s would still work.
|
||||
Also, BIOS-compatibility will be slowly removed on newer UEFI machines over the next years (see below).
|
||||
|
||||
@@ -75,7 +75,7 @@ This structure has the following general format:
|
||||
| 446 | partition entry 1 | 16 |
|
||||
| 462 | partition entry 2 | 16 |
|
||||
| 478 | partition entry 3 | 16 |
|
||||
| 444 | partition entry 4 | 16 |
|
||||
| 494 | partition entry 4 | 16 |
|
||||
| 510 | boot signature | 2 |
|
||||
|
||||
The bootstrap code is commonly called the _bootloader_ and responsible for loading and starting the operating system kernel.
|
||||
@@ -275,7 +275,7 @@ We will take a closer look at the `entry_point` macro and the different configur
|
||||
bootloader_api::entry_point!(kernel_main);
|
||||
|
||||
// ↓ this replaces the `_start` function ↓
|
||||
fn kernel_main(_bootinfo: &'static mut bootloader_api::BootInfo) -> ! {
|
||||
fn kernel_main(_boot_info: &'static mut bootloader_api::BootInfo) -> ! {
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
@@ -292,7 +292,7 @@ There are a few notable things:
|
||||
|
||||
To verify that the `entry_point` macro worked as expected, we can use the `objdump` tool as [described in the previous post][objdump-prev]. First, we recompile using `cargo build --target x86_64-unknown-none`, then we inspect the section headers using `objdump` or `rust-objdump`:
|
||||
|
||||
[objdump-prev]: @/edition-3/posts/01-minimal-kernel/index.md#objdump
|
||||
[objdump-prev]: @/edition-3/posts/01-minimal-kernel/index.md#inspect-elf-file-using-objdump
|
||||
|
||||
```bash,hl_lines=8
|
||||
❯ rust-objdump -h target/x86_64-unknown-none/debug/kernel
|
||||
@@ -343,7 +343,7 @@ In the next sections, we will implement following steps to achieve this:
|
||||
|
||||
- Create a [`cargo` workspace] with an empty root package.
|
||||
- Add an [_artifact dependency_] to include the compiled kernel binary in the root package.
|
||||
- Invoke the `bootloader::DiskImageBuilder` in our new root package.
|
||||
- Invoke the `bootloader::DiskImageBuilder` in the root package.
|
||||
|
||||
[`cargo` workspace]: https://doc.rust-lang.org/cargo/reference/workspaces.html
|
||||
[_artifact dependency_]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies
|
||||
@@ -491,7 +491,7 @@ profile = "default"
|
||||
targets = ["x86_64-unknown-none"]
|
||||
```
|
||||
|
||||
The `channel` field specifies which [toolchain`] to use.
|
||||
The `channel` field specifies which [`toolchain`] to use.
|
||||
In our case, we want to use the latest nightly compiler.
|
||||
We could also specify a specific nightly here, e.g. `nightly-2023-04-30`, which can be useful when there is some breakage in the newest nightly.
|
||||
In the `targets` list, we can specify additional targets that we want to compile to.
|
||||
@@ -548,7 +548,7 @@ bindeps = true
|
||||
After switching to nightly Rust and enabling the unstable `bindeps` feature, we can finally add an artifact dependency on our compiled kernel.
|
||||
For this, we update the `dependency` table of our `blog_os` crate:
|
||||
|
||||
```toml ,hl_lines=9-10
|
||||
```toml ,hl_lines=11-12
|
||||
[package]
|
||||
name = "blog_os"
|
||||
version = "0.1.0"
|
||||
@@ -558,13 +558,18 @@ edition = "2021"
|
||||
members = ["kernel"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
[build-dependencies]
|
||||
kernel = { path = "kernel", artifact = "bin", target = "x86_64-unknown-none" }
|
||||
```
|
||||
|
||||
We will use the artifact in a cargo [_build script_], so we add it to the `build-dependencies` section instead of the normal `dependencies` section.
|
||||
We specify that the `kernel` crate lives in the `kernel` subdirectory through the `path` key.
|
||||
The `artifact = "bin"` key specifies that we're interested in the compiled kernel binary (this makes the dependency an artifact dependency).
|
||||
Finally, we use the `target` key to specify that our kernel binary should be compiled for the `x86_64-unknown-none` target.
|
||||
|
||||
[_build script_]: https://doc.rust-lang.org/cargo/reference/build-scripts.html
|
||||
|
||||
Now `cargo` will automatically build our kernel before building our `blog_os` crate.
|
||||
We can see this when building the `blog_os` crate using `cargo build`:
|
||||
|
||||
@@ -583,13 +588,43 @@ Now that we have set up an artifact dependency on our kernel, we can finally cre
|
||||
|
||||
#### Using the `DiskImageBuilder`
|
||||
|
||||
The last step to create the bootable disk image is to invoke the [`DiskImageBuilder`] of the `bootloader` crate.
|
||||
For that, we first add a dependency on the `bootloader` crate to our `blog_os` crate:
|
||||
The last step is to invoke the [`DiskImageBuilder`] of the `bootloader` crate, with our kernel executable as input.
|
||||
We will do this through a cargo [_build script_], which enables us to implement custom build steps that are run on `cargo build`.
|
||||
|
||||
To set up a build script, we place a new file named `build.rs` in the root folder of our project (not in the `src` folder!).
|
||||
Inside it, we create a simple main function:
|
||||
|
||||
```rust ,hl_lines=3-5
|
||||
// build.rs
|
||||
|
||||
fn main() {
|
||||
panic!("not implemented yet")
|
||||
}
|
||||
```
|
||||
|
||||
When we run `cargo build` now, we see that the panic is hit:
|
||||
|
||||
```bash
|
||||
❯ cargo build
|
||||
Compiling blog_os v0.1.0 (/.../os)
|
||||
error: failed to run custom build command for `blog_os v0.1.0 (/.../os)`
|
||||
|
||||
Caused by:
|
||||
process didn't exit successfully: `/.../os/target/debug/build/blog_os-ff0a4f2814615867/build-script-build` (exit status: 101)
|
||||
--- stderr
|
||||
thread 'main' panicked at 'not implemented yet', build.rs:5:5
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
```
|
||||
|
||||
This panic shows us that cargo found the build script and automatically invoked it as part of `cargo build`.
|
||||
|
||||
Now we're ready to use the [`DiskImageBuilder`].
|
||||
For that, we first add a build dependency on the `bootloader` crate to our `blog_os` crate:
|
||||
|
||||
```toml ,hl_lines=5
|
||||
# in root Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
[build-dependencies]
|
||||
kernel = { path = "kernel", artifact = "bin", target = "x86_64-unknown-none" }
|
||||
bootloader = "0.11.3"
|
||||
```
|
||||
@@ -613,110 +648,224 @@ Please open an issue in the [`rust-osdev/bootloader`] repository if you encounte
|
||||
|
||||
[`rust-osdev/bootloader`]: https://github.com/rust-osdev/bootloader
|
||||
|
||||
Now we can use the [`DiskImageBuilder`] in the `main` function of our `blog_os` crate:
|
||||
After adding the dependency, we can use the [`DiskImageBuilder`] in the `main` function of our build script:
|
||||
|
||||
```rust, hl_lines=3-4 7-9
|
||||
// src/main.rs
|
||||
// build.rs
|
||||
|
||||
use bootloader::DiskImageBuilder;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
// set by cargo for the kernel artifact dependency
|
||||
let kernel_path = PathBuf::from(env!("CARGO_BIN_FILE_KERNEL"));
|
||||
let disk_builder = DiskImageBuilder::new(kernel_path);
|
||||
let kernel_path = env::var("CARGO_BIN_FILE_KERNEL").unwrap();
|
||||
let disk_builder = DiskImageBuilder::new(PathBuf::from(kernel_path));
|
||||
}
|
||||
```
|
||||
|
||||
Cargo communicates the path of artifact dependencies through environment variables.
|
||||
For our `kernel` dependency, the environment variable name is `CARGO_BIN_FILE_KERNEL`.
|
||||
The environment variable is set at build time, so we can use the [`env!`] macro to read it.
|
||||
We then wrap convert it to a [`PathBuf`] because that's the type that [`DiskImageBuilder::new`] expects.
|
||||
To read it, we use the [`std::env::var`] function.
|
||||
If it's not present, we panic using [`unwrap`].
|
||||
Then wrap convert it to a [`PathBuf`] and pass it to [`DiskImageBuilder::new`].
|
||||
|
||||
[`env!`]: https://doc.rust-lang.org/std/macro.env.html
|
||||
[`std::env::var`]: https://doc.rust-lang.org/std/env/fn.var.html
|
||||
[`unwrap`]: https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap
|
||||
[`PathBuf`]: https://doc.rust-lang.org/std/path/struct.PathBuf.html
|
||||
[`DiskImageBuilder::new`]: https://docs.rs/bootloader/0.11.3/bootloader/struct.DiskImageBuilder.html#method.new
|
||||
|
||||
Now we need to decide where we want to place the disk images.
|
||||
This is entirely up to.
|
||||
In the following, we will place the images next to `blog_os` executable, which will be under `target/debug` (for development builds) or `target/release` (for optimized builds):
|
||||
Next, we call the `create_uefi_image` and `create_bios_image` methods to create the UEFI and BIOS disk images:
|
||||
|
||||
```rust ,hl_lines=11-14 16-18
|
||||
// build.rs
|
||||
|
||||
```rust ,hl_lines=2 4 9-10 12
|
||||
use bootloader::DiskImageBuilder;
|
||||
use std::{env, error::Error, path::PathBuf};
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
fn main() {
|
||||
// set by cargo for the kernel artifact dependency
|
||||
let kernel_path = PathBuf::from(env!("CARGO_BIN_FILE_KERNEL"));
|
||||
let disk_builder = DiskImageBuilder::new(kernel_path);
|
||||
let kernel_path = env::var("CARGO_BIN_FILE_KERNEL").unwrap();
|
||||
let disk_builder = DiskImageBuilder::new(PathBuf::from(kernel_path));
|
||||
|
||||
// place the disk image files under target/debug or target/release
|
||||
let target_dir = env::current_exe()?;
|
||||
// specify output paths
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let uefi_path = out_dir.join("blog_os-uefi.img");
|
||||
let bios_path = out_dir.join("blog_os-bios.img");
|
||||
|
||||
Ok(())
|
||||
// create the disk images
|
||||
disk_builder.create_uefi_image(&uefi_path).unwrap();
|
||||
disk_builder.create_bios_image(&bios_path).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
We use the [`std::env::current_exe`] function to get the path to the `blog_os` executable.
|
||||
This function can (rarely) fail, so we add some basic error handling to our `main` function.
|
||||
For that, we change the return value of the function to a [`Result`] with a dynamic error type (a [_trait object_] of the [`Error`] trait).
|
||||
This allows us to use the [`?` operator] to exit with an error code on error.
|
||||
To prevent collisions, cargo [requires build scripts] to place all their outputs in a specific directory.
|
||||
Cargo specifies this directory through the `OUT_DIR` environment variable, which we read using [`std::env::var`] again.
|
||||
After converting the directory path to a [`PathBuf`], we can use the [`join`] method to append file names to it (choose any names you like).
|
||||
We then use the the `create_uefi_image` and `create_bios_image` methods to create bootable UEFI and BIOS disk images at these paths.
|
||||
|
||||
[`std::env::current_exe`]: https://doc.rust-lang.org/std/env/fn.current_exe.html
|
||||
[`Result`]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#recoverable-errors-with-result
|
||||
[`?` operator]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator
|
||||
[_trait object_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#using-trait-objects-that-allow-for-values-of-different-types
|
||||
[`Error`]: https://doc.rust-lang.org/std/error/trait.Error.html
|
||||
[requires build scripts]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script
|
||||
[`join`]: https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.join
|
||||
|
||||
The final step is to actually create the UEFI and BIOS disk images:
|
||||
We can now use a simple `cargo build` to cross-compile our kernel, build the bootloader, and combine them to create a bootable disk image:
|
||||
|
||||
```
|
||||
❯ cargo build
|
||||
Compiling bootloader_api v0.11.3
|
||||
Compiling blog_os v0.1.0 (/.../os)
|
||||
Compiling kernel v0.1.0 (/.../os/kernel)
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
|
||||
```
|
||||
|
||||
Cargo will automatically detect when our kernel code is modified and recompile the dependent `blog_os` crate. Builds with optimizations work too, by running `cargo build --release`.
|
||||
|
||||
#### Where is it?
|
||||
|
||||
We just have one remaining issue:
|
||||
We don't know in which directory we created the disk images.
|
||||
|
||||
So let's update our build script to make the values `uefi_path` and `bios_path` variables accessible.
|
||||
The best way to do that is to instruct `cargo` to set an environment variable.
|
||||
We can do this by printing a special [`cargo:rustc-env` string] in our build script:
|
||||
|
||||
```rust ,hl_lines=20-22
|
||||
// build.rs
|
||||
|
||||
```rust ,hl_lines=12-14 16-18
|
||||
use bootloader::DiskImageBuilder;
|
||||
use std::{env, error::Error, path::PathBuf};
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
fn main() {
|
||||
// set by cargo for the kernel artifact dependency
|
||||
let kernel_path = PathBuf::from(env!("CARGO_BIN_FILE_KERNEL"));
|
||||
let disk_builder = DiskImageBuilder::new(kernel_path);
|
||||
let kernel_path = env::var("CARGO_BIN_FILE_KERNEL").unwrap();
|
||||
let disk_builder = DiskImageBuilder::new(PathBuf::from(kernel_path));
|
||||
|
||||
// place the disk image files under target/debug or target/release
|
||||
let target_dir = env::current_exe()?;
|
||||
// specify output paths
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
let uefi_path = out_dir.join("blog_os-uefi.img");
|
||||
let bios_path = out_dir.join("blog_os-bios.img");
|
||||
|
||||
let uefi_path = target_dir.with_file_name("blog_os-uefi.img");
|
||||
disk_builder.create_uefi_image(&uefi_path)?;
|
||||
println!("Created UEFI disk image at {}", uefi_path.display());
|
||||
// create the disk images
|
||||
disk_builder.create_uefi_image(&uefi_path).unwrap();
|
||||
disk_builder.create_bios_image(&bios_path).unwrap();
|
||||
|
||||
let bios_path = target_dir.with_file_name("blog_os-bios.img");
|
||||
disk_builder.create_bios_image(&bios_path)?;
|
||||
println!("Created BIOS disk image at {}", bios_path.display());
|
||||
|
||||
Ok(())
|
||||
// pass the disk image paths via environment variables
|
||||
println!("cargo:rustc-env=UEFI_IMAGE={}", uefi_path.display());
|
||||
println!("cargo:rustc-env=BIOS_IMAGE={}", bios_path.display());
|
||||
}
|
||||
```
|
||||
|
||||
We use the [`PathBuf::with_file_name`] method to create the target paths for the disk images.
|
||||
To create the images, we call the `create_uefi_image` and `create_bios_image` methods of [`DiskImageBuilder`].
|
||||
Finally, we print the full paths to the created files.
|
||||
[`cargo:rustc-env` string]: https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-env
|
||||
|
||||
[`PathBuf::with_file_name`]: https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.with_file_name
|
||||
This sets two environment variables, `UEFI_IMAGE` and `BIOS_IMAGE`.
|
||||
These variables are now available at build time in the `src/main.rs` of our `blog_os` crate.
|
||||
This file still contains the default _"Hello, world!"_ output.
|
||||
Let's change it to print the disk image paths:
|
||||
|
||||
Now we can use a simple `cargo run` to cross-compile our kernel, build the bootloader, and combine them to create a bootable disk image:
|
||||
```rust ,hl_lines=4-5
|
||||
// src/main.rs
|
||||
|
||||
fn main() {
|
||||
println!("UEFI disk image at {}", env!("UEFI_IMAGE"));
|
||||
println!("BIOS disk image at {}", env!("BIOS_IMAGE"));
|
||||
}
|
||||
```
|
||||
|
||||
Since the environment variables are set at build time, we can use the special [`env!` macro] to fill them in.
|
||||
|
||||
[`env!` macro]: https://doc.rust-lang.org/std/macro.env.html
|
||||
|
||||
Now we can invoke `cargo run` to find out where our disk images are:
|
||||
|
||||
```
|
||||
❯ cargo run
|
||||
Compiling kernel v0.1.0 (/.../os/kernel)
|
||||
Compiling blog_os v0.1.0 (/.../os)
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.52s
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
|
||||
Running `target/debug/blog_os`
|
||||
Created UEFI disk image at /.../os/target/debug/blog_os-uefi.img
|
||||
Created BIOS disk image at /.../os/target/debug/blog_os-bios.img
|
||||
UEFI disk image at /.../os/target/debug/build/blog_os-a2f3397119bcf798/out/blog_os-uefi.img
|
||||
BIOS disk image at /.../os/target/debug/build/blog_os-a2f3397119bcf798/out/blog_os-bios.img
|
||||
```
|
||||
|
||||
Cargo will automatically detect when our kernel code is modified and recompile the dependent `blog_os` crate.
|
||||
We see that they live in some subdirectory in `target/debug/build`.
|
||||
Note that cargo includes some internals hashes in this path and that it might change this path at any time.
|
||||
|
||||
Using this long path is a bit cumbersome, so let's copy the files to the `target/debug` or `target/release` directories directly:
|
||||
|
||||
```rust ,hl_lines=3 6-14
|
||||
// src/main.rs
|
||||
|
||||
use std::{env, fs};
|
||||
|
||||
fn main() {
|
||||
let current_exe = env::current_exe().unwrap();
|
||||
let uefi_target = current_exe.with_file_name("uefi.img");
|
||||
let bios_target = current_exe.with_file_name("bios.img");
|
||||
|
||||
fs::copy(env!("UEFI_IMAGE"), &uefi_target).unwrap();
|
||||
fs::copy(env!("BIOS_IMAGE"), &bios_target).unwrap();
|
||||
|
||||
println!("UEFI disk image at {}", uefi_target.display());
|
||||
println!("BIOS disk image at {}", bios_target.display());
|
||||
}
|
||||
```
|
||||
|
||||
We exploit that the `main` function becomes an executable in `target` or `target/release` after compilation, so we can use the [`current_exe`] path to find the right directory.
|
||||
Then we use the [`with_file_name`] method to create new file paths in the same directory.
|
||||
As before, choose any name you like here.
|
||||
|
||||
[`current_exe`]: https://doc.rust-lang.org/std/env/fn.current_exe.html
|
||||
[`with_file_name`]: https://doc.rust-lang.org/std/path/struct.PathBuf.html#method.with_file_name
|
||||
|
||||
To copy the disk images to their new path, we use the [`fs::copy`] function.
|
||||
The last step is to print the new paths.
|
||||
Now we have the disk images available at a shorter and stable path, which is easier to use:
|
||||
|
||||
[`fs::copy`]: https://doc.rust-lang.org/std/fs/fn.copy.html
|
||||
|
||||
```bash
|
||||
❯ cargo run
|
||||
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
|
||||
Running `target/debug/blog_os`
|
||||
UEFI disk image at /.../os/target/debug/uefi.img
|
||||
BIOS disk image at /.../os/target/debug/bios.img
|
||||
❯ cargo run --release
|
||||
Finished release [optimized] target(s) in 0.02s
|
||||
Running `target/release/blog_os`
|
||||
UEFI disk image at /.../os/target/release/uefi.img
|
||||
BIOS disk image at /.../os/target/release/bios.img
|
||||
```
|
||||
|
||||
We see that the disk images are copied to `target/debug` for development builds and to `target/release` for optimized builds, just as we intended.
|
||||
|
||||
#### Making `rust-analyzer` happy
|
||||
|
||||
TODO
|
||||
In case you're using [`rust-analyzer`], you might notice that it reports some errors in the `kernel/src/main.rs`.
|
||||
The error might be one of these:
|
||||
|
||||
- _found duplicate lang item `panic_impl`_
|
||||
- _language item required, but not found: `eh_personality`_
|
||||
|
||||
The reason for these errors is that `rust-analyzer` tries to build tests and benchmarks for all crates in the workspace.
|
||||
This fails for our `kernel` crate because testing/benchmarking automatically includes the `std` crate, which conflicts with our `#[no_std]` implementation.
|
||||
|
||||
So to fix these errors, we need to specify that our `kernel` crate should not be tested or benchmarked.
|
||||
We can do this by adding the following to our `kernel/Cargo.toml` file:
|
||||
|
||||
```toml ,hl_lines=8-11
|
||||
# in kernel/Cargo.toml
|
||||
|
||||
[package]
|
||||
name = "kernel"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "kernel"
|
||||
test = false
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
bootloader_api = "0.11.0"
|
||||
```
|
||||
|
||||
Now `rust-analyzer` should not report any errors anymore.
|
||||
|
||||
## Running our Kernel
|
||||
|
||||
@@ -740,8 +889,7 @@ After installing QEMU, you can run `qemu-system-x86_64 --version` in a terminal
|
||||
Then you can run the BIOS disk image of our kernel through the following command:
|
||||
|
||||
```
|
||||
qemu-system-x86_64 -drive \
|
||||
format=raw,file=bootimage-bios-blog_os.img
|
||||
qemu-system-x86_64 -drive format=raw,file=target/debug/bios.img
|
||||
```
|
||||
|
||||
As a result, you should see a window open that looks like this:
|
||||
@@ -763,19 +911,17 @@ Unfortunately, the project is only [sparsely documented][ovmf-whitepaper] and do
|
||||
[ovmf-whitepaper]: https://www.linux-kvm.org/downloads/lersek/ovmf-whitepaper-c770f8c.txt
|
||||
|
||||
The easiest way to work with OVMF is to download pre-built images of the code.
|
||||
We provide such images in the [`rust-osdev/ovmf-prebuilt`] repository, which is updated daily from [Gerd Hoffman's RPM builds](https://www.kraxel.org/repos/).
|
||||
We provide such images in the [`rust-osdev/ovmf-prebuilt`] repository, ~~which is updated daily from [Gerd Hoffman's RPM builds](https://www.kraxel.org/repos/)~~.
|
||||
The compiled OVMF are provided as [GitHub releases][ovmf-prebuilt-releases].
|
||||
|
||||
[`rust-osdev/ovmf-prebuilt`]: https://github.com/rust-osdev/ovmf-prebuilt/
|
||||
[ovmf-prebuilt-releases]: https://github.com/rust-osdev/ovmf-prebuilt/releases/latest
|
||||
|
||||
To run our UEFI disk image in QEMU, we need the `OVMF_pure-efi.fd` file (other files might work as well).
|
||||
To run our UEFI disk image in QEMU, we need the `OVMF-pure-efi.fd` file (other files might work as well).
|
||||
After downloading it, we can then run our UEFI disk image using the following command:
|
||||
|
||||
```
|
||||
qemu-system-x86_64 -drive \
|
||||
format=raw,file=bootimage-uefi-blog_os.img \
|
||||
-bios /path/to/OVMF_pure-efi.fd,
|
||||
qemu-system-x86_64 -drive format=raw,file=target/debug/uefi.img -bios OVMF-pure-efi.fd
|
||||
```
|
||||
|
||||
If everything works, this command opens a window with the following content:
|
||||
@@ -786,9 +932,113 @@ If everything works, this command opens a window with the following content:
|
||||
The output is a bit different than with the BIOS disk image.
|
||||
Among other things, it explicitly mentions that this is an UEFI boot right on top.
|
||||
|
||||
### Using `cargo run`
|
||||
### QEMU Run Scripts
|
||||
|
||||
TODO
|
||||
Remembering the QEMU run commands and invoking them manually is a bit cumbersome, so let's invoke the commands from our Rust code.
|
||||
We implement this by creating a new `src/bin/qemu-bios.rs` file with the following contents:
|
||||
|
||||
```rust ,hl_lines=3-14
|
||||
// src/bin/qemu-bios.rs
|
||||
|
||||
use std::{
|
||||
env,
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let mut qemu = Command::new("qemu-system-x86_64");
|
||||
qemu.arg("-drive");
|
||||
qemu.arg(format!("format=raw,file={}", env!("BIOS_IMAGE")));
|
||||
let exit_status = qemu.status().unwrap();
|
||||
process::exit(exit_status.code().unwrap_or(-1));
|
||||
}
|
||||
```
|
||||
|
||||
Like our `src/main.rs` file, the `qemu_bios.rs` is an executable that can use the outputs of our build script.
|
||||
Instead of copying the disk images and printing their paths, we pass the original bios disk image path as input to a QEMU child process.
|
||||
We create this child process using [`Command::new`], add the arguments via [`Command::arg`], and finally start it using [`Command::status`].
|
||||
Once the command exits, we finish with the same exit code using [`std::process::exit`].
|
||||
|
||||
[`Command::new`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.new
|
||||
[`Command::arg`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.arg
|
||||
[`Command::status`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.status
|
||||
[`std::process::exit`]: https://doc.rust-lang.org/std/process/fn.exit.html
|
||||
|
||||
Now we can use `cargo run --bin qemu-bios` to build the kernel, convert it to a bootable disk image, and launch the BIOS disk image in QEMU.
|
||||
Of course, cargo will only recompile the kernel and rerun the build script if necessary.
|
||||
|
||||
Our `src/main.rs` is still usable through `cargo run --bin blog_os`.
|
||||
However, invoking `cargo run` without a `--bin` arguments will now error because cargo does not know which binary it should start in this case.
|
||||
We can specify this by adding a new `default-run` key to our top-level `Cargo.toml`:
|
||||
|
||||
```toml ,hl_lines=7
|
||||
# in Cargo.toml
|
||||
|
||||
[package]
|
||||
name = "blog_os"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
default-run = "blog_os"
|
||||
|
||||
# <...>
|
||||
```
|
||||
|
||||
Now `cargo run` works again.
|
||||
If you prefer, you can of course also set `default-run` to `qemu-bios` instead.
|
||||
|
||||
Let's make things complete by adding a `qemu-uefi` executable as well.
|
||||
We need the `OVMF-pure-efi.fd`, which we could add as normal file path.
|
||||
However, the [`ovmf-prebuilt`] crate provides an easier way:
|
||||
It includes a prebuilt copy of the `OVMF` file and provides it through its `ovmf_pure_efi` function.
|
||||
To use it, we add it as a dependency to our top-level `Cargo.toml`:
|
||||
|
||||
[`ovmf-prebuilt`]: https://docs.rs/ovmf-prebuilt/0.1.0-alpha.1/ovmf_prebuilt/
|
||||
|
||||
```toml ,hl_lines=6
|
||||
# in Cargo.toml
|
||||
|
||||
# ...
|
||||
|
||||
[dependencies]
|
||||
ovmf-prebuilt = "0.1.0-alpha"
|
||||
|
||||
[build-dependencies]
|
||||
kernel = { path = "kernel", artifact = "bin", target = "x86_64-unknown-none" }
|
||||
bootloader = "0.11.3"
|
||||
```
|
||||
|
||||
Now we can create our `qemu-uefi` executable at `src/bin/qemu-uefi.rs`:
|
||||
|
||||
```rust ,hl_lines=3-15
|
||||
// src/bin/qemu-uefi.rs
|
||||
use std::{
|
||||
env, process::{self, Command}
|
||||
};
|
||||
|
||||
use ovmf_prebuilt::{Arch, FileType, Prebuilt, Source};
|
||||
|
||||
fn main() {
|
||||
let prebuilt =
|
||||
Prebuilt::fetch(Source::LATEST, "target/ovmf").unwrap();
|
||||
let ovmf_code = prebuilt.get_file(Arch::X64, FileType::Code);
|
||||
let ovmf_vars = prebuilt.get_file(Arch::X64, FileType::Vars);
|
||||
let mut qemu = Command::new("qemu-system-x86_64");
|
||||
qemu.args([
|
||||
"-drive",
|
||||
&format!("format=raw,if=pflash,readonly=on,file={}", ovmf_code.display()),
|
||||
"-drive",
|
||||
&format!("format=raw,if=pflash,file={}", ovmf_vars.display()),
|
||||
"-drive",
|
||||
&format!("format=raw,file={}", env!("UEFI_IMAGE")),
|
||||
]);
|
||||
let exit_status = qemu.status().unwrap();
|
||||
process::exit(exit_status.code().unwrap_or(-1));
|
||||
}
|
||||
```
|
||||
|
||||
It's very similar to our `qemu-bios` executable.
|
||||
The only two differences are that it passes two additional `-drive if=pflash,..` arguments to load UEFI firmware (`OVMF_CODE.fd`) and writable NVRAM (`OVMF_VARS.fd`), and that it uses the `UEFI_IMAGE` instead of the `BIOS_IMAGE`.
|
||||
Using a quick `cargo run --bin qemu-uefi`, we can confirm that it works as intended.
|
||||
|
||||
### Screen Output
|
||||
|
||||
@@ -804,8 +1054,12 @@ The graphics card automatically reads the contents of this region on every scree
|
||||
Since the size, pixel format, and memory location of the framebuffer can vary between different systems, we need to find out these parameters first.
|
||||
The easiest way to do this is to read it from the [boot information structure][`BootInfo`] that the bootloader passes as argument to our kernel entry point:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
```rust ,hl_lines=3 7-13
|
||||
// in kernel/src/main.rs
|
||||
|
||||
use bootloader_api::BootInfo;
|
||||
|
||||
// ...
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
if let Some(framebuffer) = boot_info.framebuffer.as_ref() {
|
||||
@@ -814,6 +1068,8 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
}
|
||||
loop {}
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
Even though most systems support a framebuffer, some might not.
|
||||
@@ -834,9 +1090,8 @@ We will look into programming the framebuffer in detail in the next post.
|
||||
For now, let's just try setting the whole screen to some color.
|
||||
For this, we just set every pixel in the byte slice to some fixed value:
|
||||
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
```rust ,hl_lines=5-7
|
||||
// in kernel/src/main.rs
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||
@@ -850,42 +1105,28 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
|
||||
While it depends on the pixel color format how these values are interpreted, the result will likely be some shade of gray since we set the same value for every color channel (e.g. in the RGB color format).
|
||||
|
||||
After running `cargo kbuild` and then our `boot` script again, we can boot the new version in QEMU.
|
||||
To boot the new version in QEMU, we use `cargo run --bin qemu-bios` or `cargo run --bin qemu-uefi`.
|
||||
We see that our guess that the whole screen would turn gray was right:
|
||||
|
||||

|
||||
|
||||
We finally see some output from our own little kernel!
|
||||
|
||||
You can try experimenting with the pixel bytes if you like, for example by increasing the pixel value on each loop iteration:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||
let mut value = 0x90;
|
||||
for byte in framebuffer.buffer_mut() {
|
||||
*byte = value;
|
||||
value = value.wrapping_add(1);
|
||||
}
|
||||
}
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
We use the [`wrapping_add`] method here because Rust panics on implicit integer overflow (at least in debug mode).
|
||||
By adding a prime number, we try to add some variety.
|
||||
The result looks as follows:
|
||||
|
||||

|
||||
We finally see some output from our little kernel!
|
||||
|
||||
### Booting on Real Hardware
|
||||
|
||||
To boot on real hardware, you first need to write either the `bootimage-uefi-blog_os.img` or the `bootimage-bios-blog_os.img` disk image to an USB stick.
|
||||
This deletes everything on the stick, so be careful.
|
||||
The actual steps to do this depend on your operating system.
|
||||
To boot on real hardware, write either the `uefi.img` or the `bios.img` disk image to an USB thumb drive.
|
||||
The actual steps to do this depend on your operating system (see below).
|
||||
After writing the thumb drive, you can let your computer boot from it.
|
||||
You can typically choose the boot device by pressing some specific key during the BIOS setup that happens directly after you turn on the computer.
|
||||
|
||||
In the following, we show some ways to write a disk image to a thumb drive.
|
||||
|
||||
<div class="warning">
|
||||
|
||||
**WARNING**: Be very with the following operations.
|
||||
If you specify the wrong device as the `of=` parameter, you could end up erasing your system or other important data, so make sure that you choose the right target drive.
|
||||
|
||||
</div>
|
||||
|
||||
#### Unix-like
|
||||
|
||||
@@ -896,18 +1137,15 @@ After that, open a terminal window and run either of the following commands:
|
||||
##### Linux
|
||||
```
|
||||
# replace /dev/sdX with device filename as revealed by "sudo fdisk -l"
|
||||
$ sudo dd if=boot-uefi-blog_os.img of=/dev/sdX
|
||||
$ sudo dd if=target/release/uefi.img of=/dev/sdX
|
||||
```
|
||||
|
||||
##### macOS
|
||||
```
|
||||
# replace /dev/diskX with device filename as revealed by "diskutil list"
|
||||
$ sudo dd if=boot-uefi-blog_os.img of=/dev/diskX
|
||||
$ sudo dd if=target/release/uefi.img of=/dev/diskX
|
||||
```
|
||||
|
||||
**WARNING**: Be very careful when running this command.
|
||||
If you specify the wrong device as the `of=` parameter, you could end up wiping your system clean, so make sure the device you run it on is a removable one.
|
||||
|
||||
#### Windows
|
||||
|
||||
On Windows, you can use the [Rufus] tool, which is developed as an open-source project [on GitHub][rufus-github].
|
||||
@@ -917,210 +1155,13 @@ In the interface, you select the USB stick you want to write to.
|
||||
[Rufus]: https://rufus.ie/
|
||||
[rufus-github]: https://github.com/pbatard/rufus
|
||||
|
||||
## Summary and Next Steps
|
||||
|
||||
In this post we learned about the [boot process](#the-boot-process) on x86 machines and about the [BIOS](#bios) and [UEFI](#uefi) firmware standards.
|
||||
We used the `bootloader` and `bootloader_api` crates to convert our kernel to a [bootable disk image](#bootable-disk-image) and [started in QEMU](#running-in-qemu).
|
||||
Through advanced cargo features such as [workspaces](#creating-a-workspace), [build scripts](#using-the-diskimagebuilder), and [artifact dependencies](#adding-an-artifact-dependency), we created a nice build system that can bring us directly from source code to a running QEMU instance using a single command.
|
||||
|
||||
We also started to look into frame buffers and [screen output](#screen-output).
|
||||
In the [next post], we will continue with this and learn how to draw shapes and render text.
|
||||
|
||||
### OLD
|
||||
|
||||
The [docs of the `bootloader` crate][`bootloader` docs] describe how to create a bootable disk image for a kernel.
|
||||
The first step is to find the directory where cargo placed the source code of the `bootloader` dependency.
|
||||
Then, a special build command needs to be executed in that directory, passing the paths to the kernel binary and its `Cargo.toml` as arguments.
|
||||
This will result in multiple disk image files as output, which can be used to boot the kernel on BIOS and UEFI systems.
|
||||
|
||||
[`bootloader` docs]: https://docs.rs/bootloader/0.11.0/bootloader/
|
||||
|
||||
#### A `boot` crate
|
||||
|
||||
Since following these steps manually is cumbersome, we create a script to automate it.
|
||||
For that we create a new `boot` crate in a subdirectory, in which we will implement the build steps:
|
||||
|
||||
```
|
||||
cargo new --bin boot
|
||||
```
|
||||
|
||||
This command creates a new `boot` subfolder with a `Cargo.toml` and a `src/main.rs` in it.
|
||||
Since this new cargo project will be tightly coupled with our main project, it makes sense to combine the two crates as a [cargo workspace].
|
||||
This way, they will share the same `Cargo.lock` for their dependencies and place their compilation artifacts in a common `target` folder.
|
||||
To create such a workspace, we add the following to the `Cargo.toml` of our main project:
|
||||
|
||||
[cargo workspace]: https://doc.rust-lang.org/cargo/reference/workspaces.html
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
|
||||
[workspace]
|
||||
members = ["boot"]
|
||||
```
|
||||
|
||||
After creating the workspace, we can begin the implementation of the `boot` crate.
|
||||
Note that the crate will be invoked as part as our build process, so it can be a normal Rust executable that runs on our host system.
|
||||
This means that is has a classical `main` function and can use standard library types such as [`Path`] or [`Command`] without problems.
|
||||
|
||||
[`Path`]: https://doc.rust-lang.org/std/path/struct.Path.html
|
||||
[`Command`]: https://doc.rust-lang.org/std/process/struct.Command.html
|
||||
|
||||
#### Artifact Dependencies
|
||||
|
||||
The first step in creating the bootable disk image is to enable support for [artifact dependencies](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#artifact-dependencies) from inside your kernel's `.cargo/config.toml` because we're going to need that support later:
|
||||
|
||||
```toml
|
||||
# in .cargo/config.toml
|
||||
|
||||
[unstable]
|
||||
bindeps = true
|
||||
```
|
||||
|
||||
After this, you need to add an artifact dependency on your kernel from inside the boot crate. This tells the bootloader crate where the source code to your kernel resides:
|
||||
|
||||
```toml
|
||||
# in boot/Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
kernel = { path = "..", artifact = "bin", target = "x86_64-unknown-none" }
|
||||
```
|
||||
|
||||
Finally, you need to add a dependency on the main `bootloader` crate. Previous versions used `bootloader_locator` instead, but now, thanks to artifact dependencies, that is no longer necessary.
|
||||
|
||||
```toml
|
||||
# in boot/Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
bootloader = "0.11.0"
|
||||
```
|
||||
|
||||
We can see how this works by printing the Cargo-generated environment variable pointing to the absolute path of the kernel binary
|
||||
|
||||
```rust
|
||||
// in boot/src/main.rs
|
||||
use std::path::Path; // new
|
||||
|
||||
pub fn main() {
|
||||
let kernel_binary = Path::new(env!("CARGO_BIN_FILE_KERNEL_kernel"));
|
||||
dbg!(kernel_binary);
|
||||
}
|
||||
```
|
||||
|
||||
The `CARGO_BIN_FILE_KERNEL_kernel` environment variable is defined by Cargo as the absolute path to the binary file created after compiling an artifact dependency — and in this case, the binary file it points to is your kernel's binary. This makes it very easy to begin the process of boot image creation, as explained in detail below.
|
||||
|
||||
[`dbg!`]: https://doc.rust-lang.org/std/macro.dbg.html
|
||||
|
||||
To run the `boot` crate from our workspace root (i.e. the kernel directory), we can pass a [`--package`] argument to `cargo run`:
|
||||
|
||||
[`--package`]: https://doc.rust-lang.org/cargo/commands/cargo-run.html#package-selection
|
||||
|
||||
```
|
||||
> cargo run --package boot
|
||||
[boot/src/main.rs:5] kernel_path = "/.../target/x86_64-unknown-none/debug/deps/artifact/kernel-.../bin/kernel-..."
|
||||
```
|
||||
|
||||
It worked! We see that the kernel binary lives somewhere in the dependency tree of our `boot` crate.
|
||||
By depending on the kernel as a binary dependency of `boot`, we ensure that the bootloader and the kernel use the exact same version of the `BootInfo` type.
|
||||
This is important because the `BootInfo` type is not stable yet, so undefined behavior can occur when when using different `BootInfo` versions.
|
||||
|
||||
#### Building a Boot Image
|
||||
|
||||
The next step is to actually build the boot image.
|
||||
From the [`bootloader` docs] we learn that the crate defines two completely unique bootloader objects: `BiosBoot` for BIOS and `UefiBoot` for UEFI. To keep it simple, we will support both, although it's possible to choose which to exclusively support later to keep your workflow streamlined as your kernel becomes more complex.
|
||||
|
||||
```toml
|
||||
# in boot/Cargo.toml
|
||||
|
||||
[dependencies]
|
||||
bootloader = "0.11.0"
|
||||
kernel = { path = "..", artifact = "bin", target = "x86_64-unknown-none" }
|
||||
```
|
||||
|
||||
Once all dependencies are accounted for, it's time to put everything together:
|
||||
|
||||
```rust
|
||||
// in boot/src/main.rs
|
||||
|
||||
// new
|
||||
use bootloader::{BiosBoot, UefiBoot}
|
||||
use std::{path::Path, process::exit};
|
||||
|
||||
pub fn main() {
|
||||
// new code below
|
||||
|
||||
let kernel_dir = todo!();
|
||||
let bios_image = todo!();
|
||||
let uefi_image = todo!();
|
||||
|
||||
// invoke UEFI boot image builder
|
||||
let uefi = UefiBoot::new(&kernel_binary);
|
||||
|
||||
// invoke BIOS boot image builder
|
||||
let bios = BiosBoot::new(&kernel_binary);
|
||||
|
||||
// attempt to create UEFI boot image
|
||||
if let Err(e) = uefi.create_disk_image(&uefi_path) {
|
||||
eprintln!("{:#?}", &e);
|
||||
exit(1)
|
||||
}
|
||||
|
||||
// attempt to create BIOS boot image
|
||||
if let Err(e) = bios.create_disk_image(&bios_path) {
|
||||
eprintln!("{:#?}", &e);
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
We use both the `UefiBoot` and `BiosBoot` types to create disk images for the BIOS and UEFI implementations, respectively. By using the `if let` syntax, we can exit the build gracefully whenever an error occurs.
|
||||
|
||||
After creating the `UefiBoot` and `BiosBoot` types using the `CARGO_BIN_FILE_KERNEL_kernel` environment variable that we went over previously as the constructor argument for both, we now are ready for the next step.
|
||||
|
||||
#### Filling in the Blanks
|
||||
|
||||
We still need to fill in the paths we marked as `todo!` above. Like with the kernel binary, we can also use the `env!()` builtin for this, since another environment variable can also be used as a reference point for determining the filenames for the disk images:
|
||||
|
||||
```rust
|
||||
// in `main` in boot/src/main.rs
|
||||
|
||||
// we know that the kernel lives in the parent directory of the `boot` crate
|
||||
let kernel_dir = Path::new(env!("CARGO_MANIFEST_DIR")).manifest_dir.parent().unwrap();
|
||||
|
||||
// use the above as a target folder in which to place both the BIOS and UEFI disk images
|
||||
let bios_image = kernel_dir.join("bootimage-bios-blog_os.img");
|
||||
let uefi_image = kernel_dir.join("bootimage-uefi-blog_os.img");
|
||||
```
|
||||
|
||||
The [`CARGO_MANIFEST_DIR`] environment variable always points to the `boot` directory, even if the crate is built from a different directory (e.g. via cargo's `--manifest-path` argument).
|
||||
This gives use a good starting point for creating the paths we care about since we know that our kernel lives in the [parent][`Path::parent`] directory.
|
||||
|
||||
[`Path::parent`]: https://doc.rust-lang.org/std/path/struct.Path.html
|
||||
[`CARGO_MANIFEST_DIR`]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
|
||||
|
||||
From the `kernel_dir`, we can then construct the `bios_image` and `uefi_image` paths using the [`Path::join`] method.
|
||||
|
||||
[`Path::join`]: https://doc.rust-lang.org/std/path/struct.Path.html#method.join
|
||||
|
||||
#### Creating the Disk Images
|
||||
|
||||
There is one last step before we can create the bootable disk images: The `bootloader` build requires the [rustup component] `llvm-tools-preview`.
|
||||
To install it, we can either run `rustup component add llvm-tools-preview` or specify it in our `rust-toolchain.toml` file:
|
||||
|
||||
[rustup component]: https://rust-lang.github.io/rustup/concepts/components.html
|
||||
|
||||
```toml
|
||||
# in rust-toolchain.toml
|
||||
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
components = ["rust-src", "rustfmt", "clippy", "llvm-tools-preview"]
|
||||
```
|
||||
|
||||
After that can finally use our `boot` crate to create some bootable disk images from our kernel:
|
||||
|
||||
```bash
|
||||
> cargo run --package boot
|
||||
```
|
||||
|
||||
Because we're using artifact dependencies, when you run the `boot` package, the kernel is automatically pulled in and compiled as a dependency. Previously, in version 0.10 of the bootloader crate, you had to build the kernel binary first, but now, thanks to artifact dependencies, this is no longer required.
|
||||
Note that the command will only work from the root directory of our project.
|
||||
This is because we hardcoded the `kernel_binary` path in our `main` function.
|
||||
We will fix this later in the post, but first it is time to actually run our kernel!
|
||||
|
||||
Note also that we specified names for the image files. Although we used `bootimage-bios-blog_os.img` and `bootimage-uefi-blog_os.img` for compatibility, they can now be given whatever names you see fit.
|
||||
|
||||
|
||||
[next post]: @/edition-3/posts/03-screen-output/index.md
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -12,14 +12,480 @@ icon = '''<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi
|
||||
</svg>'''
|
||||
+++
|
||||
|
||||
Dolores qui incidunt sit fugiat amet consequatur. Qui ab vel et molestias ex nemo corporis consequatur. Quia consequuntur itaque sequi quia autem. Maxime vel quis maxime at. Tenetur eveniet velit dolor quidem temporibus tenetur.
|
||||
In this post we focus on the [framebuffer], a special memory region that controls the screen output.
|
||||
Using an [external crate], we will create functions for writing individual pixels, lines, and various shapes.
|
||||
In the the second half of this post, we will explore text rendering and learn how to print the obligatory _["Hello, World!"]_.
|
||||
|
||||
[framebuffer]: https://en.wikipedia.org/wiki/Framebuffer
|
||||
[external crate]: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
|
||||
["Hello, World!"]: https://en.wikipedia.org/wiki/Hello_world
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Molestiae quidem ipsa nihil laboriosam sapiente laudantium quia. Praesentium et repudiandae minima voluptas et. Repellendus voluptatem distinctio enim et alias distinctio recusandae quos. Dolores ex eum culpa quo sunt sint voluptate voluptates. Facere unde sequi quo ea vel nihil. Rem deleniti repellat rem molestias
|
||||
This blog is openly developed on [GitHub].
|
||||
If you have any problems or questions, please open an issue there.
|
||||
You can also leave comments [at the bottom].
|
||||
The complete source code for this post can be found in the [`post-3.3`][post branch] branch.
|
||||
|
||||
[GitHub]: https://github.com/phil-opp/blog_os
|
||||
[at the bottom]: #comments
|
||||
<!-- fix for zola anchor checker (target is in template): <a id="comments"> -->
|
||||
[post branch]: https://github.com/phil-opp/blog_os/tree/post-3.3
|
||||
|
||||
<!-- toc -->
|
||||
Molestiae quidem ipsa nihil laboriosam sapiente laudantium quia. Praesentium et repudiandae minima voluptas et. Repellendus voluptatem distinctio enim et alias distinctio recusandae quos. Dolores ex eum culpa quo sunt sint voluptate voluptates. Facere unde sequi quo ea vel nihil. Rem deleniti repellat rem molestias
|
||||
|
||||
## Bitmap Images
|
||||
|
||||
In the [previous post], we learned how to make our minimal kernel bootable.
|
||||
Using the [`BootInfo`] provided by the bootloader, we were able to access a special memory region called the _[framebuffer]_, which controls the screen output.
|
||||
We wrote some example code to display a gray background:
|
||||
|
||||
[previous post]: @/edition-3/posts/02-booting/index.md
|
||||
[`BootInfo`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/struct.BootInfo.html
|
||||
|
||||
```rust
|
||||
// in kernel/src/main.rs
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||
for byte in framebuffer.buffer_mut() {
|
||||
*byte = 0x90;
|
||||
}
|
||||
}
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
The reason that the above code affects the screen output is because the graphics card interprets the framebuffer memory as a [bitmap] image.
|
||||
A bitmap describes an image through a predefined number of bytes per pixel.
|
||||
The pixels are laid out line by line, typically starting at the top.
|
||||
|
||||
[bitmap]: https://en.wikipedia.org/wiki/Bitmap
|
||||
[RGB]: https://en.wikipedia.org/wiki/Rgb
|
||||
|
||||
For example, the pixels of an image with width 10 and height 3 would be typically stored in this order:
|
||||
|
||||
<table style = "width: fit-content;"><tbody>
|
||||
<tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td></tr>
|
||||
<tr><td>10</td><td>11</td><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td><td>19</td></tr>
|
||||
<tr><td>20</td><td>21</td><td>22</td><td>23</td><td>24</td><td>25</td><td>26</td><td>27</td><td>28</td><td>29</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
So top left pixel is stored at offset 0 in the bitmap array.
|
||||
The pixel on its right is at pixel offset 1.
|
||||
The first pixel of the next line starts at pixel offset `line_length`, which is 10 in this case.
|
||||
The last line starts at pixel offset 20, which is `line_length * 2`.
|
||||
|
||||
### Padding
|
||||
|
||||
Depending on the hardware and GPU firmware, it is often more efficient to make lines start at well-aligned offsets.
|
||||
Because of this, there is often some additional padding at the end of each line.
|
||||
So the actual memory layout of the 10x3 example image might look like this, with the padding marked as yellow:
|
||||
|
||||
<table style = "width: fit-content;"><tbody>
|
||||
<tr><td>0</td><td>1</td><td>2</td><td>3</td><td>4</td><td>5</td><td>6</td><td>7</td><td>8</td><td>9</td><td style="background-color:yellow;">10</td><td style="background-color:yellow;">11</td></tr>
|
||||
<tr><td>12</td><td>13</td><td>14</td><td>15</td><td>16</td><td>17</td><td>18</td><td>19</td><td>20</td><td>21</td><td style="background-color:yellow;">22</td><td style="background-color:yellow;">23</td></tr>
|
||||
<tr><td>24</td><td>25</td><td>26</td><td>27</td><td>28</td><td>29</td><td>30</td><td>31</td><td>32</td><td>33</td><td style="background-color:yellow;">34</td><td style="background-color:yellow;">35</td></tr>
|
||||
</tbody></table>
|
||||
|
||||
So now the second line starts at pixel offset 12.
|
||||
The two pixels at the end of each line are considered as padding and ignored.
|
||||
So if we want to set the first pixel of the second line, we need to be aware of the additional padding and set the pixel at offset 12 instead of offset 10.
|
||||
|
||||
The line length plus the padding bytes is typically called the _stride_ or _pitch_ of the buffer.
|
||||
In the example above, the stride is 12 and the line length is 10.
|
||||
|
||||
Since the amount of padding depends on the hardware, the stride is only known at runtime.
|
||||
The `bootloader` crate queries the framebuffer parameters from the UEFI or BIOS firmware and reports them as part of the `BootInfo`.
|
||||
It provides the stride of the framebuffer, among other parameters, in form of a [`FrameBufferInfo`] struct that can be created using the [`FrameBuffer::info`] method.
|
||||
|
||||
[`FrameBufferInfo`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/struct.FrameBufferInfo.html
|
||||
[`FrameBuffer::info`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/struct.FrameBuffer.html#method.info
|
||||
|
||||
### Color formats
|
||||
|
||||
The [`FrameBufferInfo`] also specifies the [`PixelFormat`] of the framebuffer, which also depends on the underlying hardware.
|
||||
Using this information, we can set pixels to different colors.
|
||||
For example, the [`PixelFormat::Rgb`] variant specifies that each pixel is represented in the [RGB color space], which stores the red, green, and blue parts of the pixel as separate bytes.
|
||||
In this model, the color red would be represented as the three bytes `[255, 0, 0]`, or `0xff0000` in [hexadecimal representation].
|
||||
The color yellow is represented the addition of red and green, which results in `[255, 255, 0]` (or `0xffff00` in hexadecimal representation).
|
||||
|
||||
[`PixelFormat`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/enum.PixelFormat.html
|
||||
[`PixelFormat::Rgb`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/enum.PixelFormat.html#variant.Rgb
|
||||
[RGB color space]: https://en.wikipedia.org/wiki/RGB_color_spaces
|
||||
[hexadecimal representation]: https://en.wikipedia.org/wiki/RGB_color_model#Numeric_representations
|
||||
|
||||
While the `Rgb` format is most common, there are also framebuffers that use a different color format.
|
||||
For example, the [`PixelFormat::Bgr`] stores the three colors in inverted order, i.e. blue first and red last.
|
||||
There are also buffers that don't support colors at all and can represent only grayscale pixels.
|
||||
The `bootloader_api` crate reports such buffers as [`PixelFormat::U8`].
|
||||
|
||||
[`PixelFormat::Bgr`]: https://docs.rs/bootloader_api/0.11.5/bootloader_api/info/enum.PixelFormat.html#variant.Bgr
|
||||
[`PixelFormat::U8`]: https://docs.rs/bootloader_api/0.11.5/bootloader_api/info/enum.PixelFormat.html#variant.U8
|
||||
|
||||
Note that there might be some additional padding at the pixel-level as well.
|
||||
For example, an `Rgb` pixel might be stored as 4 bytes instead of 3 to ensure 32-bit alignment.
|
||||
The number of bytes per pixel is reported by the bootloader in the [`FrameBufferInfo::bytes_per_pixel`] field.
|
||||
|
||||
[`FrameBufferInfo::bytes_per_pixel`]: https://docs.rs/bootloader_api/0.11/bootloader_api/info/struct.FrameBufferInfo.html#structfield.bytes_per_pixel
|
||||
|
||||
## Setting specific Pixels
|
||||
|
||||
Based on this above details, we can now create a function to set a specific pixel to a certain color.
|
||||
We start by creating a new `framebuffer` [module]:
|
||||
|
||||
[module]: https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html
|
||||
|
||||
```rust ,hl_lines=3-5
|
||||
// in kernel/src/main.rs
|
||||
|
||||
// declare a submodule -> the compiler will automatically look
|
||||
// for a file named `framebuffer.rs` or `framebuffer/mod.rs`
|
||||
mod framebuffer;
|
||||
```
|
||||
|
||||
In the new module, we create basic structs for representing pixel positions and colors:
|
||||
|
||||
```rust ,hl_lines=3-16
|
||||
// in new kernel/src/framebuffer.rs file
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Position {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Color {
|
||||
pub red: u8,
|
||||
pub green: u8,
|
||||
pub blue: u8,
|
||||
}
|
||||
```
|
||||
|
||||
By marking the structs and their fields as `pub`, we make them accessible from the parent `kernel` module.
|
||||
We use the `#[derive]` attribute to implement the [`Debug`], [`Clone`], [`Copy`], [`PartialEq`], and [`Eq`] traits of Rust's core library.
|
||||
These traits allow us to duplicate, compare, and print the structs.
|
||||
|
||||
[`Debug`]: https://doc.rust-lang.org/stable/core/fmt/trait.Debug.html
|
||||
[`Clone`]: https://doc.rust-lang.org/stable/core/clone/trait.Clone.html
|
||||
[`Copy`]: https://doc.rust-lang.org/stable/core/marker/trait.Copy.html
|
||||
[`PartialEq`]: https://doc.rust-lang.org/stable/core/cmp/trait.PartialEq.html
|
||||
[`Eq`]: https://doc.rust-lang.org/stable/core/cmp/trait.Eq.html
|
||||
|
||||
Next, we create a function for setting a specific pixel in the framebuffer to a given color:
|
||||
|
||||
```rust ,hl_lines=3 5-39
|
||||
// in new kernel/src/framebuffer.rs file
|
||||
|
||||
use bootloader_api::info::{FrameBuffer, PixelFormat};
|
||||
|
||||
pub fn set_pixel_in(framebuffer: &mut FrameBuffer, position: Position, color: Color) {
|
||||
let info = framebuffer.info();
|
||||
|
||||
// calculate offset to first byte of pixel
|
||||
let byte_offset = {
|
||||
// use stride to calculate pixel offset of target line
|
||||
let line_offset = position.y * info.stride;
|
||||
// add x position to get the absolute pixel offset in buffer
|
||||
let pixel_offset = line_offset + position.x;
|
||||
// convert to byte offset
|
||||
pixel_offset * info.bytes_per_pixel
|
||||
};
|
||||
|
||||
// set pixel based on color format
|
||||
let pixel_buffer = &mut framebuffer.buffer_mut()[byte_offset..];
|
||||
match info.pixel_format {
|
||||
PixelFormat::Rgb => {
|
||||
pixel_buffer[0] = color.red;
|
||||
pixel_buffer[1] = color.green;
|
||||
pixel_buffer[2] = color.blue;
|
||||
}
|
||||
PixelFormat::Bgr => {
|
||||
pixel_buffer[0] = color.blue;
|
||||
pixel_buffer[1] = color.green;
|
||||
pixel_buffer[2] = color.red;
|
||||
}
|
||||
PixelFormat::U8 => {
|
||||
// use a simple average-based grayscale transform
|
||||
let gray = color.red / 3 + color.green / 3 + color.blue / 3;
|
||||
pixel_buffer[0] = gray;
|
||||
}
|
||||
other => panic!("unknown pixel format {other:?}"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The first step is to calculate the byte offset within the framebuffer slice at which the pixel starts.
|
||||
For this, we first calculate the pixel offset of the line by multiplying the `y` position with the stride of the framebuffer, i.e. its line width plus the line padding.
|
||||
We then add the `x` position to get the absolute index of the pixel.
|
||||
As the framebuffer slice is a byte slice, we need to transform the pixel index to a byte offset by multiplying it with the number of `bytes_per_pixel`.
|
||||
|
||||
[`FrameBuffer::buffer_mut`]: https://docs.rs/bootloader_api/0.11.5/bootloader_api/info/struct.FrameBuffer.html#method.buffer_mut
|
||||
|
||||
The second step is to set the pixel to the desired color.
|
||||
We first use the [`FrameBuffer::buffer_mut`] method to get access to the actual bytes of the framebuffer in form of a slice.
|
||||
Then, we use the slicing operator `[byte_offset..]` to get a sub-slice starting at the `byte_offset` of the target pixel.
|
||||
As the write operation depends on the pixel format, we use a [`match`] statement:
|
||||
|
||||
[`match`]: https://doc.rust-lang.org/stable/std/keyword.match.html
|
||||
|
||||
- For `Rgb` framebuffers, we write three bytes; first red, then green, then blue.
|
||||
- For `Bgr` framebuffers, we also write three bytes, but blue first and red last.
|
||||
- For `U8` framebuffers, we first convert the color to grayscale by taking the average of the three color channels.
|
||||
Note that there are multiple [different ways to convert colors to grayscale], so you can also use different factors here.
|
||||
- For all other framebuffer formats, we [panic] for now.
|
||||
|
||||
[different ways to convert colors to grayscale]: https://www.baeldung.com/cs/convert-rgb-to-grayscale#bd-convert-rgb-to-grayscale
|
||||
[panic]: https://doc.rust-lang.org/stable/core/macro.panic.html
|
||||
|
||||
Let's try to use our new function to write a blue pixel in our `kernel_main` function:
|
||||
|
||||
```rust ,hl_lines=5-11
|
||||
// in kernel/src/main.rs
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||
let position = framebuffer::Position { x: 20, y: 100 };
|
||||
let color = framebuffer::Color {
|
||||
red: 0,
|
||||
green: 0,
|
||||
blue: 255,
|
||||
};
|
||||
framebuffer::set_pixel_in(framebuffer, position, color);
|
||||
}
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
When we run our code in QEMU using `cargo run --bin qemu-bios` (or `--bin qemu-uefi`) and look _very closely_, we can see the blue pixel.
|
||||
It's really difficult to see, so I marked with an arrow below:
|
||||
|
||||

|
||||
|
||||
As this single pixel is too difficult to see, let's draw a filled square of 100x100 pixels instead:
|
||||
|
||||
```rust ,hl_lines=10-18
|
||||
// in kernel/src/main.rs
|
||||
|
||||
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
|
||||
if let Some(framebuffer) = boot_info.framebuffer.as_mut() {
|
||||
let color = framebuffer::Color {
|
||||
red: 0,
|
||||
green: 0,
|
||||
blue: 255,
|
||||
};
|
||||
for x in 0..100 {
|
||||
for y in 0..100 {
|
||||
let position = framebuffer::Position {
|
||||
x: 20 + x,
|
||||
y: 100 + y,
|
||||
};
|
||||
framebuffer::set_pixel_in(framebuffer, position, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
loop {}
|
||||
}
|
||||
```
|
||||
|
||||
Now we clearly see that our code works as intended:
|
||||
|
||||

|
||||
|
||||
Feel free to experiment with different positions and colors if you like.
|
||||
You can also try to draw a circle instead of a square, or a line with a certain thickness.
|
||||
|
||||
As you can probably imagine, it would be a lot of work to draw more complex shapes this way.
|
||||
One example for such complex shapes is _text_, i.e. the rendering of letters and punctuation.
|
||||
Fortunately, there is the nice `no_std`-compatible [`embedded-graphics`] crate, which provides draw functions for text, various shapes, and image data.
|
||||
|
||||
[`embedded-graphics`]: https://docs.rs/embedded-graphics/latest/embedded_graphics/index.html
|
||||
|
||||
## The `embedded-graphics` crate
|
||||
|
||||
|
||||
|
||||
### Implementing `DrawTarget`
|
||||
|
||||
```rust ,hl_lines=3
|
||||
// in kernel/src/framebuffer.rs
|
||||
use embedded_graphics::{
|
||||
Pixel,
|
||||
draw_target::DrawTarget,
|
||||
geometry::{OriginDimensions, Size},
|
||||
pixelcolor::{Rgb888, RgbColor},
|
||||
};
|
||||
|
||||
pub struct Display<'f> {
|
||||
framebuffer: &'f mut FrameBuffer,
|
||||
}
|
||||
|
||||
impl<'f> Display<'f> {
|
||||
pub fn new(framebuffer: &'f mut FrameBuffer) -> Display {
|
||||
Display { framebuffer }
|
||||
}
|
||||
|
||||
fn draw_pixel(&mut self, Pixel(coordinates, color): Pixel<Rgb888>) {
|
||||
// ignore any out of bounds pixels
|
||||
let (width, height) = {
|
||||
let info = self.framebuffer.info();
|
||||
|
||||
(info.width, info.height)
|
||||
};
|
||||
|
||||
let (x, y) = {
|
||||
let c: (i32, i32) = coordinates.into();
|
||||
(c.0 as usize, c.1 as usize)
|
||||
};
|
||||
|
||||
if (0..width).contains(&x) && (0..height).contains(&y) {
|
||||
let color = Color { red: color.r(), green: color.g(), blue: color.b() };
|
||||
|
||||
set_pixel_in(self.framebuffer, Position { x, y }, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl<'f> DrawTarget for Display<'f> {
|
||||
type Color = Rgb888;
|
||||
|
||||
/// Drawing operations can never fail.
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
|
||||
where
|
||||
I: IntoIterator<Item = Pixel<Self::Color>>,
|
||||
{
|
||||
for pixel in pixels.into_iter() {
|
||||
self.draw_pixel(pixel);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f> OriginDimensions for Display<'f> {
|
||||
fn size(&self) -> Size {
|
||||
let info = self.framebuffer.info();
|
||||
|
||||
Size::new(info.width as u32, info.height as u32)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
So far, we have drawn shapes and pixels directly onto the framebuffer. That's fine and all, but how is one able to go from that to displaying text on the screen? Understanding this requires taking a deep dive into how characters are rendered behind the scenes.
|
||||
|
||||
When a key is pressed on the keyboard, it sends a character code to the CPU. It's the CPU's job at that point to then interpret the character code and match it with an image to draw on the screen. The image is then sent to either the GPU or the framebuffer (the latter in our case) to be drawn on the screen, and the user sees that image as a letter, number, CJK character, emoji, or whatever else he or she wanted to have displayed by pressing that key.
|
||||
|
||||
In most other programming languages, implementing this behind the scenes can be a daunting task. With Rust, however, we have a toolset at our disposal that can pave the way for setting up proper framebuffer logging using very little code of our own.
|
||||
|
||||
# The `log` crate
|
||||
|
||||
Rust developers used to writing user-mode code will recognize the `log` crate from a mile away:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
[dependencies]
|
||||
log = { version = "0.4.17", default-features = false }
|
||||
```
|
||||
|
||||
This crate has both a set of macros for logging either to the console or to a log file for later reading and a trait — also called `Log` with a capital L — that can be implemented to provide a backend, called a `Logger` in Rust parlance. Loggers are provided by a myriad of crates for a wide variety of use cases, and some of them even run on bare metal. We already used one such extant logger in the UEFI booting module when we used the logger provided by the `uefi` crate to print text to the UEFI console. That won't work in the kernel, however, because UEFI boot services need to be active in order for the UEFI logger to be usable.
|
||||
|
||||
If you were paying attention to the post before that one, however, you may have noticed that the bootloader is itself able to log directly to the framebuffer as it did when we booted the barebones kernel for the first time, and unlike the UEFI console logger, this logger is usable long after UEFI boot services are exited. It's this logger, therefore, that provides the easiest means of implementation on our end.
|
||||
|
||||
## `bootloader-x86_64-common`
|
||||
|
||||
In version 0.11.x of the bootloader crate, each component is separate, unlike in 0.10.x where the bootloader was a huge monolith. This is fantastic as it means that a lot of the APIs that the bootloader uses behind the scenes are also free for kernels to use, including, of course, the logger. The set of APIs that the logger belongs to are in a crate called `bootloader-x86_64-common` which also contains some other useful abstractions related to things like memory management that will come in handy later:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
[dependencies]
|
||||
bootloader-x86_64-common = "0.11.3"
|
||||
```
|
||||
|
||||
For now, however, only the logger will be used. If you are curious as to how this logger is written behind the scenes, however, don't worry; a sub-module of this chapter will include a tutorial on how to write a custom logger from scratch.
|
||||
|
||||
# Putting it all together
|
||||
|
||||
Before we use the bootloader's logger, we first need to initialize it. This requires creating a static instance, since it needs to live for as long as the kernel lives — which would mean for as long as the computer is powered on. Unfortunately, this is easier said than done, as Rust statics can be rather finicky — understandably so for security reasons. Luckily, there's a crate for this too.
|
||||
|
||||
## The `conquer_once` crate
|
||||
|
||||
Those used to using the standard library know that it provides a `OnceCell` which is exactly what it sounds like: you write to it only once, and then after that it's just there to use whenever. We're in a kernel and don't have access to the standard library, however, so is there a crate on crates.io that provides a replacement? Ah, yes there is:
|
||||
|
||||
```toml
|
||||
# in Cargo.toml
|
||||
[dependencies]
|
||||
conquer-once = { version = "0.4.0", default-features = false }
|
||||
```
|
||||
|
||||
Note that we need to add `default-features = false` to our `conquer-once` dependency —that's because the [`conquer-once` crate](https://crates.io/crates/conquer-once) tries to pull in the standard library by default, which in the kernel will result in compilation errors.
|
||||
|
||||
Now that we've added our two dependencies, it's time to use them:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
use conquer_once::spin::OnceCell;
|
||||
use bootloader_x86_64_common::logger::LockedLogger;
|
||||
// ...
|
||||
pub(crate) static LOGGER: OnceCell<LockedLogger> = OnceCell::uninit();
|
||||
```
|
||||
|
||||
By setting the logger up as a static `OnceCell` it becomes much easier to initialize. We use `pub(crate)` to ensure that the kernel can see it but nothing else can.
|
||||
|
||||
After this, it's time to actually initialize it. To do that, we use a function:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
use bootloader_api::info::FrameBufferInfo;
|
||||
// ...
|
||||
pub(crate) fn init_logger(buffer: &'static mut [u8], info: FrameBufferInfo) {
|
||||
let logger = LOGGER.get_or_init(move || LockedLogger::new(buffer, info, true, false));
|
||||
log::set_logger(logger).expect("Logger already set");
|
||||
log::set_max_level(log::LevelFilter::Trace);
|
||||
log::info!("Hello, Kernel Mode!");
|
||||
}
|
||||
```
|
||||
|
||||
This function takes two parameters: a byte slice representing a raw framebuffer and a `FrameBufferInfo` structure containing information about the first parameter. Getting those parameters, however, requires jumping through some hoops to satisfy the borrow checker:
|
||||
|
||||
```rust
|
||||
// in src/main.rs
|
||||
fn kernel_main(boot_info: &'static mut bootloader_api::BootInfo) -> ! {
|
||||
// ...
|
||||
// free the doubly wrapped framebuffer from the boot info struct
|
||||
let frame_buffer_optional = &mut boot_info.framebuffer;
|
||||
|
||||
// free the wrapped framebuffer from the FFI-safe abstraction provided by bootloader_api
|
||||
let frame_buffer_option = frame_buffer_optional.as_mut();
|
||||
|
||||
// unwrap the framebuffer
|
||||
let frame_buffer_struct = frame_buffer_option.unwrap();
|
||||
|
||||
// extract the framebuffer info and, to satisfy the borrow checker, clone it
|
||||
let frame_buffer_info = frame_buffer_struct.info().clone();
|
||||
|
||||
// get the framebuffer's mutable raw byte slice
|
||||
let raw_frame_buffer = frame_buffer_struct.buffer_mut();
|
||||
|
||||
// finally, initialize the logger using the last two variables
|
||||
init_logger(raw_frame_buffer, frame_buffer_info);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Any one of these steps, if skipped, will cause the borrow checker to throw a hissy fit due to the use of the `move ||` closure by the initializer function. With this, however, you're done, and you'll know the logger has been initialized when you see "Hello, Kernel Mode!" printed on the screen.
|
||||
|
||||
<!-- more -->
|
||||
<!-- toc -->
|
||||
|
||||
<!-- TODO: update relative link in 02-booting/uefi/index.md when this post is finished -->
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@@ -76,7 +76,7 @@ html {
|
||||
--background-color: #fff;
|
||||
--text-color: #515151;
|
||||
--heading-color: #313131;
|
||||
--heading-code-color: #a0565c;
|
||||
--heading-code-color: #313131;
|
||||
--link-color: #268bd2;
|
||||
--hr-color-top: #eee;
|
||||
--hr-color-bottom: #fff;
|
||||
@@ -181,11 +181,11 @@ h6 {
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 1rem;
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
h3 {
|
||||
margin-top: 1.5rem;
|
||||
@@ -196,6 +196,7 @@ h5,
|
||||
h6 {
|
||||
margin-top: 1rem;
|
||||
font-size: 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Body text */
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
{% macro giscus(search_term, lang) %}
|
||||
{% if lang != "en" %}
|
||||
{% set category = "Post Comments (translated)" %}
|
||||
{% set category_id = "DIC_kwDOAlvePc4CPg4c" %}
|
||||
{% set category_path = "post-comments-translated" %}
|
||||
{% else %}
|
||||
{% set category = "Post Comments" %}
|
||||
{% set category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMzMDE4OTg1" %}
|
||||
{% set category_path = "post-comments" %}
|
||||
{% endif %}
|
||||
|
||||
@@ -33,7 +35,7 @@
|
||||
data-repo="phil-opp/blog_os"
|
||||
data-repo-id="MDEwOlJlcG9zaXRvcnkzOTU3NTEwMQ=="
|
||||
data-category="{{ category }}"
|
||||
data-category-id="MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMzMDE4OTg1"
|
||||
data-category-id="{{ category_id }}"
|
||||
{% if search_term is number %}
|
||||
data-mapping="number"
|
||||
{% else %}
|
||||
|
||||
Reference in New Issue
Block a user