### "Target"的两种含义
`--all-targets` 标志与 `--target` 参数完全无关。
在 `cargo` 中,"target"这个术语有两种不同的含义:
- `--target` 标志指定了应该传递给 `rustc` 编译器的 **[编译目标][compilation target]**。这应该设置为运行我们代码的机器的[目标三元组][target triple]。
- `--all-targets` 标志指的是 Cargo 的 **[包目标][package target]**。Cargo 包可以同时是库和二进制文件,因此你可以指定你想要构建 crate 的方式。此外,Cargo 还有用于[示例](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#examples)、[测试](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#tests)和[基准测试](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#benchmarks)的包目标。这些包目标可以共存,因此你可以同时在例如库模式或测试模式下构建/检查同一个 crate。
[compilation target]: https://doc.rust-lang.org/rustc/targets/index.html
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[package target]: https://doc.rust-lang.org/cargo/reference/cargo-targets.html
默认情况下,`cargo check` 只构建 _库(library)_ 和 _二进制(binary)_ 包目标。
然而,当启用 [`checkOnSave`](https://rust-analyzer.github.io/book/configuration.html#checkOnSave) 时,`rust-analyzer` 默认选择检查所有包目标。
这就是 `rust-analyzer` 报告上述我们在 `cargo check` 中没有看到的`语言项(lang item)`错误的原因。
如果我们运行 `cargo check --all-targets`,我们也会看到这个错误:
```
error[E0152]: found duplicate lang item `panic_impl`
--> src/main.rs:13:1
|
13 | / fn panic(_info: &PanicInfo) -> ! {
14 | | loop {}
15 | | }
| |_^
|
= note: the lang item is first defined in crate `std` (which `test` depends on)
= note: first definition in `std` loaded from /home/[...]/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-8df6be531efb3fd0.rlib
= note: second definition in the local crate (`blog_os`)
```
第一个 `note` 告诉我们 panic 语言项已经在 `std` crate 中定义了,而 `std` 是 `test` crate 的一个依赖项。
`test` crate 在以[测试模式](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#tests)构建 crate 时会自动被包含进来。
这对于我们的 `#![no_std]` 内核来说不合适,因为在裸机上没有办法支持标准库。
所以这个错误与我们的项目无关,我们可以安全地忽略它。
避免这个错误的正确方法是在我们的 `Cargo.toml` 中指定我们的二进制文件不支持以 `测试(test)` 和 `基准测试(bench)` 模式构建。
我们可以通过添加一个 `[[bin]]` 部分到 `Cargo.toml` 来[构建配置](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#configuring-a-target)我们的二进制文件:
```toml
# 在 Cargo.toml 中
[[bin]]
name = "blog_os"
test = false
bench = false
```
`bin` 周围的双括号并非笔误,这是 TOML 格式定义可多次出现的键的方式。
由于一个 crate 可以有多个二进制文件,`[[bin]]` 部分也可以在 `Cargo.toml` 中出现多次。
这也是必须有 `name` 字段的原因,它需要与二进制文件的名称匹配(以便 `cargo` 知道哪些设置应该应用于哪个二进制文件)。
通过将 [`test`](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-test-field) 和 [`bench`](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-bench-field) 字段设置为 `false`,我们指示 `cargo` 不要以测试或基准测试模式构建我们的二进制文件。
现在 `cargo check --all-targets` 应该不会再抛出任何错误了,`rust-analyzer` 的 `checkOnSave` 也应该愉快工作了。
## 下篇预览
下一篇文章要做的事情基于我们这篇文章的成果,它将详细讲述编写一个最小的操作系统内核需要的步骤:如何配置特定的编译目标,如何将可执行程序与引导程序拼接,以及如何把一些特定的字符串打印到屏幕上。
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md
================================================
FILE: blog/content/edition-2/posts/01-freestanding-rust-binary/index.zh-TW.md
================================================
+++
title = "獨立的 Rust 二進制檔"
weight = 1
path = "zh-TW/freestanding-rust-binary"
date = 2018-02-10
[extra]
# Please update this when updating the translation
translation_based_on_commit = "24d04e0e39a3395ecdce795bab0963cb6afe1bfd"
# GitHub usernames of the people that translated this post
translators = ["wusyong"]
# GitHub usernames of the people that contributed to this translation
translation_contributors = ["gnitoahc"]
+++
建立我們自己的作業系統核心的第一步是建立一個不連結標準函式庫的 Rust 執行檔,這使得無需基礎作業系統即可在[裸機][bare metal]上執行 Rust 程式碼。
[bare metal]: https://en.wikipedia.org/wiki/Bare_machine
此網誌在 [GitHub] 上公開開發,如果您有任何問題或疑問,請在那開一個 issue,您也可以在[下面][at the bottom]發表評論,這篇文章的完整開源程式碼可以在 [`post-01`][post branch] 分支中找到。
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-01
## 介紹
要編寫作業系統核心,我們需要不依賴於任何作業系統功能的程式碼。這代表我們不能使用執行緒、檔案系統、堆記憶體、網路、隨機數、標準輸出或任何其他需要作業系統抽象或特定硬體的功能。這也是理所當然的,因為我們正在嘗試寫出自己的 OS 和我們的驅動程式。
這意味著我們不能使用大多數的 [Rust 標準函式庫][Rust standard library],但是我們還是可以使用 _很多_ Rust 的功能。比如說我們可以使用[疊代器][iterators]、[閉包][closures]、[模式配對][pattern matching]、[option] 和 [result]、[字串格式化][string formatting],當然還有[所有權系統][ownership system]。這些功能讓我們能夠以非常有表達力且高階的方式編寫核心,而無需擔心[未定義行為][undefined behavior]或[記憶體安全][memory safety]。
[option]: https://doc.rust-lang.org/core/option/
[result]:https://doc.rust-lang.org/core/result/
[Rust standard library]: https://doc.rust-lang.org/std/
[iterators]: https://doc.rust-lang.org/book/ch13-02-iterators.html
[closures]: https://doc.rust-lang.org/book/ch13-01-closures.html
[pattern matching]: https://doc.rust-lang.org/book/ch06-00-enums.html
[string formatting]: https://doc.rust-lang.org/core/macro.write.html
[ownership system]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html
[undefined behavior]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs
[memory safety]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention
為了在 Rust 中建立 OS 核心,我們需要建立一個無須底層作業系統即可運行的執行檔,這類的執行檔通常稱為「獨立式(freestanding)」或「裸機(bare-metal)」的執行檔。
這篇文章描述了建立一個獨立的 Rust 執行檔的必要步驟,並解釋為什麼需要這些步驟。如果您只對簡單的範例感興趣,可以直接跳到 **[總結](#summary)**。
## 停用標準函式庫
Rust 所有的 crate 在預設情況下都會連結[標準函式庫][standard library],而標準函式庫會依賴作業系統的功能,像式執行緒、檔案系統或是網路。它也會依賴 C 語言的標準函式庫 `libc`,因為其與作業系統緊密相關。既然我們的計劃是編寫自己的作業系統,我們就得用到 [`no_std` 屬性][`no_std` attribute]來停止標準函式庫的自動引用(automatic inclusion)。
[standard library]: https://doc.rust-lang.org/std/
[`no_std` attribute]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html
我們先從建立一個新的 cargo 專案開始,最簡單的辦法是輸入下面的命令:
```
cargo new blog_os --bin --edition 2024
```
我將專案命名為 `blog_os`,當然讀者也可以自己的名稱。`--bin` 選項說明我們將要建立一個執行檔(而不是一個函式庫),`--edition 2024` 選項指明我們的 crate 想使用 Rust [2024 版本][2024 edition]。當我們執行這行指令的時候,cargo 會為我們建立以下目錄結構:
[2024 edition]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/index.html
```
blog_os
├── Cargo.toml
└── src
└── main.rs
```
`Cargo.toml` 包含 crate 的設置,像是 crate 的名稱、作者、[語意化版本][semantic version]以及依賴套件。`src/main.rs` 檔案則包含 crate 的根模組(root module)以及我們的 `main` 函式。您可以用 `cargo build` 編譯您的 crate 然後在 `target/debug` 目錄下運行編譯過後的 `blog_os` 執行檔。
[semantic version]: https://semver.org/lang/zh-TW/
### no_std 屬性
現在我們的 crate 背後依然有和標準函式庫連結。讓我們加上 [`no_std` 屬性][`no_std` attribute] 來停用:
```rust
// main.rs
#![no_std]
fn main() {
println!("Hello, world!");
}
```
當我們嘗試用 `cargo build` 編譯時會出現以下錯誤訊息:
```
error: cannot find macro `println!` in this scope
--> src/main.rs:4:5
|
4 | println!("Hello, world!");
| ^^^^^^^
```
出現這個錯誤的原因是因為 [`println` 巨集(macro)][`println` macro]是標準函式庫的一部份,而我們不再包含它,所以我們無法再輸出東西來。這也是理所當然因為 `println` 會寫到[標準輸出][standard output],而這是一個由作業系統提供的特殊檔案描述符。
[`println` macro]: https://doc.rust-lang.org/std/macro.println.html
[standard output]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29
所以讓我們移除這行程式碼,然後用空的 main 函式再試一次:
```rust
// main.rs
#![no_std]
fn main() {}
```
```
> cargo build
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
```
現在編譯器告訴我們缺少 `#[panic_handler]` 函式以及 _language item_。
## 實作 panic 處理函式
`panic_handler` 屬性定義了當 [panic] 發生時編譯器需要呼叫的函式。在標準函式庫中有自己的 panic 處理函式,但在 `no_std` 的環境中我們得定義我們自己的:
[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html
```rust
// main.rs
use core::panic::PanicInfo;
/// 此函式會在 panic 時呼叫。
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
```
[`PanicInfo` parameter][PanicInfo] 包含 panic 發生時的檔案、行數以及可選的錯誤訊息。這個函式不會返回,所以它被標記為[發散函式][diverging function],只會返回[“never” 型態][“never” type] `!`。現在我們什麼事可以做,所以我們只需寫一個無限迴圈。
[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html
[diverging function]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions
[“never” type]: https://doc.rust-lang.org/nightly/std/primitive.never.html
## eh_personality Language Item
Language item 是一些編譯器需求的特殊函式或類型。舉例來說,Rust 的 [`Copy`] trait 就是一個 language item,告訴編譯器哪些類型擁有[_複製的語意_][`Copy`]。當我們搜尋 `Copy` trait 的[實作][copy code]時,我們會發現一個特殊的 `#[lang = "copy"]` 屬性將它定義為一個 language item。
我們可以自己實現 language item,但這只應是最後的手段。因為 language item 屬於非常不穩定的實作細節,而且不會做類型檢查(所以編譯器甚至不會確保它們的參數類型是否正確)。幸運的是,我們有更穩定的方式來修復上面關於 language item 的錯誤。
`eh_personality` language item 標記的函式將被用於實作[堆疊回溯][stack unwinding]。在預設情況下當 panic 發生時,Rust 會使用堆疊回溯來執行所有存在堆疊上變數的解構子(destructor)。這確保所有使用的記憶體都被釋放,並讓 parent thread 獲取 panic 資訊並繼續運行。但是堆疊回溯是一個複雜的過程,通常會需要一些 OS 的函式庫如 Linux 的 [libunwind] 或 Windows 的 [structured exception handling]。所以我們並不希望在我們的作業系統中使用它。
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
[libunwind]: https://www.nongnu.org/libunwind/
[structured exception handling]: https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling
### 停用回溯
在某些狀況下回溯可能並不是我們要的功能,因此 Rust 提供了[在 panic 時中止][abort on panic]的選項。這個選項能停用回溯標誌訊息的產生,也因此能縮小生成的二進制檔案大小。我們能用許多方式開啟這個選項,而最簡單的方式就是把以下幾行設置加入我們的 `Cargo.toml`:
```toml
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
```
這些選項能將 `dev` 設置(用於 `cargo build`)和 `release` 設置(用於 `cargo build --release`)的 panic 策略設為 `abort`。現在編譯器不會再要求我們提供 `eh_personality` language item。
[abort on panic]: https://github.com/rust-lang/rust/pull/32900
現在我們已經修復了上面的錯誤,但是如果我們嘗試編譯的話,又會出現一個新的錯誤:
```
> cargo build
error: requires `start` lang_item
```
我們的程式缺少 `start` 這個用來定義入口點(entry point)的 language item。
## `start` 屬性
我們通常會認為執行一個程式時,首先被呼叫的是 `main` 函式。但是大多數語言都擁有一個[執行時系統][runtime system],它通常負責垃圾回收(garbage collection)像是 Java 或軟體執行緒(software threads)像是 Go 的 goroutines。這個執行時系統需要在 main 函式前啟動,因為它需要讓先進行初始化。
[runtime system]: https://en.wikipedia.org/wiki/Runtime_system
在一個典型使用標準函式庫的 Rust 程式中,程式運行是從一個名為 `crt0`(“C runtime zero”)的執行時函式庫開始的,它會設置 C 程式的執行環境。這包含建立堆疊和可執行程式參數的傳入。在這之後,這個執行時函式庫會呼叫 [Rust 的執行時入口點][rt::lang_start],而此處就是由 `start` language item 標記。 Rust 只有一個非常小的執行時系統,負責處理一些小事情,像是堆疊溢位或是印出 panic 時回溯的訊息。再來執行時系統最終才會呼叫 main 函式。
[rt::lang_start]: https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73
我們的獨立式可執行檔並沒有辦法存取 Rust 執行時系統或 `crt0`,所以我們需要定義自己的入口點。實作 `start` language item 並沒有用,因為這樣還是會需要 `crt0`。所以我們要做的是直接覆寫 `crt0` 的入口點。
### 重寫入口點
為了告訴 Rust 編譯器我們不要使用一般的入口點呼叫順序,我們先加上 `#![no_main]` 屬性。
```rust
#![no_std]
#![no_main]
use core::panic::PanicInfo;
/// 此函式會在 panic 時呼叫。
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
```
您可能會注意到我們移除了 `main` 函式,原因是因為既然沒有了底層的執行時系統呼叫,那麼 `main` 也沒必要存在。我們要重寫作業系統的入口點,定義為 `_start` 函式:
```rust
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
loop {}
}
```
我們使用 `no_mangle` 屬性來停用[名稱重整][name mangling],確保 Rust 編譯器輸出的函式名稱會是 `_start`。沒有這個屬性的話,編譯器會產生符號像是 `_ZN3blog_os4_start7hb173fedf945531caE` 來讓每個函式的名稱都是獨一無二的。我們會需要這項屬性的原因是因為我們接下來希望連結器能夠呼叫入口點函式的名稱。
我們還將函式標記為 `extern "C"` 來告訴編譯器這個函式應當使用 [C 的呼叫慣例][C calling convention],而不是 Rust 的呼叫慣例。而函式名稱選用 `_start` 的原因是因為這是大多數系統的預設入口點名稱。
[name mangling]: https://en.wikipedia.org/wiki/Name_mangling
[C calling convention]: https://en.wikipedia.org/wiki/Calling_convention
`!` 返回型態代表這個函式是發散函式,它不允許返回。這是必要的因為入口點不會被任何函式呼叫,只會直接由作業系統或啟動程式(bootloader)執行。所以取代返回值的是入口點需要執行作業系統的 [`exit` 系統呼叫][`exit` system call]。在我們的例子中,關閉機器似乎是個理想的動作,因為獨立的二進制檔案返回後也沒什麼事可做。現在我們先寫一個無窮迴圈來滿足需求。
[`exit` system call]: https://en.wikipedia.org/wiki/Exit_(system_call)
當我們現在運行 `cargo build` 的話會看到很醜的 _連結器_ 錯誤。
## 連結器錯誤
連結器是用來將產生的程式碼結合起來成為執行檔的程式。因為 Linux、Windows 和 macOS 之間的執行檔格式都不同,每個系統都會有自己的連結器錯誤。不過造成錯誤的原因通常都差不多:連結器預設的設定會認為我們的程式依賴於 C 的執行時系統,但我們並沒有。
為了解決這個錯誤,我們需要告訴連結器它不需要包含 C 的執行時系統。我們可以選擇提供特定的連結器參數設定,或是選擇編譯為裸機目標。
### 編譯為裸機目標
Rust 在預設情況下會嘗試編譯出符合你目前系統環境的可執行檔。舉例來說,如果你正在 `x86_64` 上使用 Windows,那麼 Rust 就會嘗試編譯出 `.exe`,一個使用 `x86_64` 指令集的 Windows 執行檔。這樣的環境稱之為主機系統(host system)。
為了描述不同環境,Rust 使用 [_target triple_] 的字串。要查看目前系統的 target triple,你可以執行 `rustc --version --verbose`:
[_target triple_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
```
rustc 1.35.0-nightly (474e7a648 2019-04-07)
binary: rustc
commit-hash: 474e7a6486758ea6fc761893b1a49cd9076fb0ab
commit-date: 2019-04-07
host: x86_64-unknown-linux-gnu
release: 1.35.0-nightly
LLVM version: 8.0
```
上面的輸出訊息來自 `x86_64` 上的 Linux 系統,我們可以看到 `host` 的 target triple 為 `x86_64-unknown-linux-gnu`,分別代表 CPU 架構 (`x86_64`)、供應商 (`unknown`) 以及作業系統 (`linux`) 和 [ABI] (`gnu`)。
[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface
在依據主機的 triple 編譯時,Rust 編譯器和連結器理所當然地會認為預設是底層的作業系統並使用 C 執行時系統,這便是造成錯誤的原因。要避免這項錯誤,我們可以選擇編譯出沒有底層作業系統的不同環境。
其中一個裸機環境的例子是 `thumbv7em-none-eabihf` target triple,它描述了[嵌入式][embedded] [ARM] 系統。其中的細節目前並不重要,我們現在只需要知道沒有底層作業系統的 target triple 是用 `none` 描述的。想要編譯這樣的目標的話,我們需要將它新增至 rustup:
[embedded]: https://en.wikipedia.org/wiki/Embedded_system
[ARM]: https://en.wikipedia.org/wiki/ARM_architecture
```
rustup target add thumbv7em-none-eabihf
```
這會下載一份該系統的標準(以及 core)函式庫,現在我們可以用此目標建立我們的獨立執行檔了:
```
cargo build --target thumbv7em-none-eabihf
```
我們傳入 `--target` [交叉編譯][cross compile]我們在裸機系統的執行檔。因為目標系統沒有作業系統,連結器不會嘗試連結 C 執行時系統並成功建立,不會產生任何連結器錯誤。
[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler
這將會是我們到時候用來建立自己的作業系統核心的方法。不過我們不會用到 `thumbv7em-none-eabihf`,我們將會使用[自訂目標][custom target]來描述一個 `x86_64` 的裸機環境。
[custom target]: https://doc.rust-lang.org/rustc/targets/custom.html
### 連結器引數
除了編譯裸機系統為目標以外,我們也可以傳入特定的引數組合給連結器來解決錯誤。這不會是我們到時候用在我們核心的方法,所以以下的內容不是必需的,只是用來補齊資訊。點選下面的 _「連結器引數」_ 來顯示額外資訊。
"]
# `cargo build` 時需要的設置
[profile.dev]
panic = "abort" # 停用 panic 時堆疊回溯
# `cargo build --release` 時需要的設置
[profile.release]
panic = "abort" # 停用 panic 時堆疊回溯
```
要建構出此執行檔,我們需要選擇一個裸機目標來編譯像是 `thumbv7em-none-eabihf`:
```
cargo build --target thumbv7em-none-eabihf
```
不然我們也可以用主機系統來編譯,不過要加上額外的連結器引數:
```bash
# Linux
cargo rustc -- -C link-arg=-nostartfiles
# Windows
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
# macOS
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
```
注意這只是最小的 Rust 獨立執行檔範例,它還是會依賴一些事情,像是當 `_start` 函式呼叫時堆疊已經初始化完畢。**所以如果想真的使用這樣的執行檔的話還需要更多步驟。**
## 接下來呢?
[下一篇文章][next post] 將會講解如何將我們的獨立執行檔轉成最小的作業系統核心。這包含建立自訂目標、用啟動程式組合我們的執行檔,還有學習如何輸出一些東西到螢幕上。
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/_index.md
================================================
+++
title = "Extra Posts for Minimal Rust Kernel"
sort_by = "weight"
insert_anchor_links = "left"
render = false
+++
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.ko.md
================================================
+++
title = "Red Zone 기능 해제하기"
weight = 1
path = "ko/red-zone"
template = "edition-2/extra.html"
+++
[red zone]은 [System V ABI]에서 사용 가능한 최적화 기법으로, 스택 포인터를 변경하지 않은 채로 함수들이 임시적으로 스택 프레임 아래의 128 바이트 공간을 사용할 수 있게 해줍니다:
[red zone]: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone
[System V ABI]: https://wiki.osdev.org/System_V_ABI

위 사진은 `n`개의 지역 변수를 가진 함수의 스택 프레임을 보여줍니다. 함수가 호출되었을 때, 함수의 반환 주소 및 지역 변수들을 스택에 저장할 수 있도록 스택 포인터의 값이 조정됩니다.
red zone은 조정된 스택 포인터 아래의 128바이트의 메모리 구간을 가리킵니다. 함수가 또 다른 함수를 호출하지 않는 구간에서만 사용하는 임시 데이터의 경우, 함수가 이 구간에 해당 데이터를 저장하는 데 이용할 수 있습니다. 따라서 스택 포인터를 조정하기 위해 필요한 명령어 두 개를 생략할 수 있는 상황이 종종 있습니다 (예: 다른 함수를 호출하지 않는 함수).
하지만 이 최적화 기법을 사용하는 도중 소프트웨어 예외(exception) 혹은 하드웨어 인터럽트가 일어날 경우 큰 문제가 생깁니다. 함수가 red zone을 사용하던 도중 예외가 발생한 상황을 가정해보겠습니다:

CPU와 예외 처리 핸들러가 red zone에 있는 데이터를 덮어씁니다. 하지만 이 데이터는 인터럽트된 함수가 사용 중이었던 것입니다. 따라서 예외 처리 핸들러로부터 반환하여 다시 인터럽트된 함수가 계속 실행되게 되었을 때 변경된 red zone의 데이터로 인해 함수가 오작동할 수 있습니다. 이런 현상으로 인해 [디버깅하는 데에 몇 주씩 걸릴 수 있는 이상한 버그][take weeks to debug]가 발생할지도 모릅니다.
[take weeks to debug]: https://forum.osdev.org/viewtopic.php?t=21720
미래에 예외 처리 로직을 구현할 때 이러한 오류가 일어나는 것을 피하기 위해 우리는 미리 red zone 최적화 기법을 해제한 채로 프로젝트를 진행할 것입니다. 컴파일 대상 환경 설정 파일에 `"disable-redzone": true` 줄을 추가함으로써 해당 기능을 해제할 수 있습니다.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
================================================
+++
title = "Disable the Red Zone"
weight = 1
path = "red-zone"
template = "edition-2/extra.html"
+++
The [red zone] is an optimization of the [System V ABI] that allows functions to temporarily use the 128 bytes below their stack frame without adjusting the stack pointer:
[red zone]: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone
[System V ABI]: https://wiki.osdev.org/System_V_ABI

The image shows the stack frame of a function with `n` local variables. On function entry, the stack pointer is adjusted to make room on the stack for the return address and the local variables.
The red zone is defined as the 128 bytes below the adjusted stack pointer. The function can use this area for temporary data that's not needed across function calls. Thus, the two instructions for adjusting the stack pointer can be avoided in some cases (e.g. in small leaf functions).
However, this optimization leads to huge problems with exceptions or hardware interrupts. Let's assume that an exception occurs while a function uses the red zone:

The CPU and the exception handler overwrite the data in the red zone. But this data is still needed by the interrupted function. So the function won't work correctly anymore when we return from the exception handler. This might lead to strange bugs that [take weeks to debug].
[take weeks to debug]: https://forum.osdev.org/viewtopic.php?t=21720
To avoid such bugs when we implement exception handling in the future, we disable the red zone right from the beginning. This is achieved by adding the `"disable-redzone": true` line to our target configuration file.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.pt-BR.md
================================================
+++
title = "Desabilitando a Red Zone"
weight = 1
path = "pt-BR/red-zone"
template = "edition-2/extra.html"
[extra]
# Please update this when updating the translation
translation_based_on_commit = "9d079e6d3e03359469d6cf1759bb1a196d8a11ac"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
A [red zone] é uma otimização da [System V ABI] que permite que funções usem temporariamente os 128 bytes abaixo do seu stack frame sem ajustar o ponteiro de pilha:
[red zone]: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone
[System V ABI]: https://wiki.osdev.org/System_V_ABI

A imagem mostra o stack frame de uma função com `n` variáveis locais. Na entrada da função, o ponteiro de pilha é ajustado para abrir espaço na pilha para o endereço de retorno e as variáveis locais.
A red zone é definida como os 128 bytes abaixo do ponteiro de pilha ajustado. A função pode usar esta área para dados temporários que não são necessários entre chamadas de função. Assim, as duas instruções para ajustar o ponteiro de pilha podem ser evitadas em alguns casos (por exemplo, em pequenas funções folha).
No entanto, esta otimização leva a problemas enormes com exceções ou interrupções de hardware. Vamos assumir que uma exceção ocorre enquanto uma função usa a red zone:

A CPU e o handler de exceção sobrescrevem os dados na red zone. Mas estes dados ainda são necessários pela função interrompida. Então a função não funcionará mais corretamente quando retornarmos do handler de exceção. Isso pode levar a bugs estranhos que [levam semanas para depurar].
[levam semanas para depurar]: https://forum.osdev.org/viewtopic.php?t=21720
Para evitar tais bugs quando implementarmos tratamento de exceções no futuro, desabilitamos a red zone logo de início. Isso é alcançado adicionando a linha `"disable-redzone": true` ao nosso arquivo de configuração de alvo.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.ru.md
================================================
+++
title = "Отключение красной зоны"
weight = 1
path = "ru/red-zone"
template = "edition-2/extra.html"
+++
[Красная зона][red zone] — это оптимизация [System V ABI], которая позволяет функциям временно использовать 128 байт ниже своего стекового кадра без корректировки указателя стека:
[red zone]: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone
[System V ABI]: https://wiki.osdev.org/System_V_ABI

На рисунке показан стековый фрейм функции с `n` локальных переменных. При входе в функцию указатель стека корректируется, чтобы освободить место в стеке для адреса возврата и локальных переменных.
Красная зона определяется как 128 байт ниже скорректированного указателя стека. Функция может использовать эту зону для временных данных, которые не нужны при всех вызовах функции. Таким образом, в некоторых случаях (например, в небольших листовых функциях) можно обойтись без двух инструкций для корректировки указателя стека.
Однако такая оптимизация приводит к огромным проблемам при работе с исключениями или аппаратными прерываниями. Предположим, что во время использования функцией красной зоны происходит исключение:

Процессор и обработчик исключений перезаписывают данные в красной зоне. Но эти данные все еще нужны прерванной функции. Поэтому функция не будет работать правильно, когда мы вернемся из обработчика исключений. Это может привести к странным ошибкам, на отладку которых [уйдут недели][take weeks to debug].
[take weeks to debug]: https://forum.osdev.org/viewtopic.php?t=21720
Чтобы избежать подобных ошибок при реализации обработки исключений в будущем, мы отключим красную зону с самого начала. Это достигается путем добавления строки `"disable-redzone": true` в наш целевой конфигурационный файл.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.zh-CN.md
================================================
+++
title = "Disable the Red Zone"
weight = 1
path = "zh-CN/red-zone"
template = "edition-2/extra.html"
+++
[红区][red zone] 是 [System V ABI] 提供的一种优化技术,它使得函数可以在不修改栈指针的前提下,临时使用其栈帧下方的128个字节。
[red zone]: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone
[System V ABI]: https://wiki.osdev.org/System_V_ABI

上图展示了一个包含了 `n` 个局部变量的栈帧。当方法开始执行时,栈指针会被调整到一个合适的位置,为返回值和局部变量留出足够的空间。
红区是位于调整后的栈指针下方,长度为128字节的区域,函数会使用这部分空间存储不会被跨函数调用的临时数据。所以在某些情况下(比如逻辑简短的叶函数),红区可以节省用于调整栈指针的两条机器指令。
然而红区优化有时也会引发无法处理的巨大问题(异常或者硬件中断),如果使用红区时发生了某种异常:

CPU和异常处理机制会把红色区域内的数据覆盖掉,但是被中断的函数依然在引用着这些数据。当函数从错误中恢复时,错误的数据就会引发更大的错误,这类错误往往需要[追踪数周][take weeks to debug]才能找到。
[take weeks to debug]: https://forum.osdev.org/viewtopic.php?t=21720
要在编写异常处理机制时避免这些隐蔽而难以追踪的bug,我们需要从一开始就禁用红区优化,具体到配置文件中的配置项,就是 `"disable-redzone": true`。
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.ko.md
================================================
+++
title = "SIMD 해제하기"
weight = 2
path = "ko/disable-simd"
template = "edition-2/extra.html"
+++
[Single Instruction Multiple Data (SIMD)] 명령어들은 여러 데이터 word에 동시에 덧셈 등의 작업을 실행할 수 있으며, 이를 통해 프로그램의 실행 시간을 상당히 단축할 수 있습니다. `x86_64` 아키텍처는 다양한 SIMD 표준들을 지원합니다:
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
- [MMX]: _Multi Media Extension_ 명령어 집합은 1997년에 등장하였으며, `mm0`에서 `mm7`까지 8개의 64비트 레지스터들을 정의합니다. 이 레지스터들은 그저 [x87 부동 소수점 장치][x87 floating point unit]의 레지스터들을 가리키는 별칭입니다.
- [SSE]: _Streaming SIMD Extensions_ 명령어 집합은 1999년에 등장하였습니다. 부동 소수점 연산용 레지스터를 재사용하는 대신 새로운 레지스터 집합을 도입했습니다. `xmm0`에서 `xmm15`까지 16개의 새로운 128비트 레지스터를 정의합니다.
- [AVX]: _Advanced Vector Extensions_ 은 SSE에 추가로 멀티미디어 레지스터의 크기를 늘리는 확장 표준입니다. `ymm0`에서 `ymm15`까지 16개의 새로운 256비트 레지스터를 정의합니다. `ymm` 레지스터들은 기존의 `xmm` 레지스터를 확장합니다 (`xmm0`이 `ymm0` 레지스터의 하부 절반을 차지하는 식으로 다른 15개의 짝에도 같은 방식의 확장이 적용됩니다).
[MMX]: https://en.wikipedia.org/wiki/MMX_(instruction_set)
[x87 floating point unit]: https://en.wikipedia.org/wiki/X87
[SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
[AVX]: https://en.wikipedia.org/wiki/Advanced_Vector_Extensions
이러한 SIMD 표준들을 사용하면 프로그램 실행 속도를 많이 향상할 수 있는 경우가 많습니다. 우수한 컴파일러는 [자동 벡터화 (auto-vectorization)][auto-vectorization]이라는 과정을 통해 일반적인 반복문을 SIMD 코드로 변환할 수 있습니다.
[auto-vectorization]: https://en.wikipedia.org/wiki/Automatic_vectorization
하지만 운영체제 커널은 크기가 큰 SIMD 레지스터들을 사용하기에 문제가 있습니다. 그 이유는 하드웨어 인터럽트가 일어날 때마다 커널이 사용 중이던 레지스터들의 상태를 전부 메모리에 백업해야 하기 때문입니다. 이렇게 하지 않으면 인터럽트 되었던 프로그램의 실행이 다시 진행될 때 인터럽트 당시의 프로그램 상태를 보존할 수가 없습니다. 따라서 커널이 SIMD 레지스터들을 사용하는 경우, 커널이 백업해야 하는 데이터 양이 많이 늘어나게 되어 (512-1600 바이트) 커널의 성능이 눈에 띄게 나빠집니다. 이러한 성능 손실을 피하기 위해서 `sse` 및 `mmx` 기능을 해제하는 것이 바람직합니다 (`avx` 기능은 해제된 상태가 기본 상태입니다).
컴파일 대상 환경 설정 파일의 `features` 필드를 이용해 해당 기능들을 해제할 수 있습니다. `mmx` 및 `sse` 기능을 해제하려면 아래와 같이 해당 기능 이름 앞에 빼기 기호를 붙여주면 됩니다:
```json
"features": "-mmx,-sse"
```
## 부동소수점 (Floating Point)
우리의 입장에서는 안타깝게도, `x86_64` 아키텍처는 부동 소수점 계산에 SSE 레지스터를 사용합니다. 따라서 SSE 기능이 해제된 상태에서 부동 소수점 계산을 컴파일하면 LLVM이 오류를 일으킵니다. Rust의 core 라이브러리는 이미 부동 소수점 숫자들을 사용하기에 (예: `f32` 및 `f64` 에 대한 각종 trait들을 정의함), 우리의 커널에서 부동 소수점 계산을 피하더라도 부동 소수점 계산을 컴파일하는 것을 피할 수 없습니다.
다행히도 LLVM은 `soft-float` 기능을 지원합니다. 이 기능을 통해 정수 계만으로 모든 부동소수점 연산 결과를 모방하여 산출할 수 있습니다. 일반 부동소수점 계산보다는 느리겠지만, 이 기능을 통해 우리의 커널에서도 SSE 기능 없이 부동소수점을 사용할 수 있습니다.
우리의 커널에서 `soft-float` 기능을 사용하려면 컴파일 대상 환경 설정 파일의 `features` 필드에 덧셈 기호와 함께 해당 기능의 이름을 적어주면 됩니다:
```json
"features": "-mmx,-sse,+soft-float"
```
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md
================================================
+++
title = "Disable SIMD"
weight = 2
path = "disable-simd"
template = "edition-2/extra.html"
+++
[Single Instruction Multiple Data (SIMD)] instructions are able to perform an operation (e.g., addition) simultaneously on multiple data words, which can speed up programs significantly. The `x86_64` architecture supports various SIMD standards:
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
- [MMX]: The _Multi Media Extension_ instruction set was introduced in 1997 and defines eight 64-bit registers called `mm0` through `mm7`. These registers are just aliases for the registers of the [x87 floating point unit].
- [SSE]: The _Streaming SIMD Extensions_ instruction set was introduced in 1999. Instead of re-using the floating point registers, it adds a completely new register set. The sixteen new registers are called `xmm0` through `xmm15` and are 128 bits each.
- [AVX]: The _Advanced Vector Extensions_ are extensions that further increase the size of the multimedia registers. The new registers are called `ymm0` through `ymm15` and are 256 bits each. They extend the `xmm` registers, so e.g. `xmm0` is the lower half of `ymm0`.
[MMX]: https://en.wikipedia.org/wiki/MMX_(instruction_set)
[x87 floating point unit]: https://en.wikipedia.org/wiki/X87
[SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
[AVX]: https://en.wikipedia.org/wiki/Advanced_Vector_Extensions
By using such SIMD standards, programs can often speed up significantly. Good compilers are able to transform normal loops into such SIMD code automatically through a process called [auto-vectorization].
[auto-vectorization]: https://en.wikipedia.org/wiki/Automatic_vectorization
However, the large SIMD registers lead to problems in OS kernels. The reason is that the kernel has to backup all registers that it uses to memory on each hardware interrupt, because they need to have their original values when the interrupted program continues. So if the kernel uses SIMD registers, it has to backup a lot more data (512–1600 bytes), which noticeably decreases performance. To avoid this performance loss, we want to disable the `sse` and `mmx` features (the `avx` feature is disabled by default).
We can do that through the `features` field in our target specification. To disable the `mmx` and `sse` features, we add them prefixed with a minus:
```json
"features": "-mmx,-sse"
```
## Floating Point
Unfortunately for us, the `x86_64` architecture uses SSE registers for floating point operations. Thus, every use of floating point with disabled SSE causes an error in LLVM. The problem is that Rust's core library already uses floats (e.g., it implements traits for `f32` and `f64`), so avoiding floats in our kernel does not suffice.
Fortunately, LLVM has support for a `soft-float` feature that emulates all floating point operations through software functions based on normal integers. This makes it possible to use floats in our kernel without SSE; it will just be a bit slower.
To turn on the `soft-float` feature for our kernel, we add it to the `features` line in our target specification, prefixed with a plus:
```json
"features": "-mmx,-sse,+soft-float"
```
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.pt-BR.md
================================================
+++
title = "Desabilitando SIMD"
weight = 2
path = "pt-BR/disable-simd"
template = "edition-2/extra.html"
[extra]
# Please update this when updating the translation
translation_based_on_commit = "9d079e6d3e03359469d6cf1759bb1a196d8a11ac"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
Instruções [Single Instruction Multiple Data (SIMD)] são capazes de realizar uma operação (por exemplo, adição) simultaneamente em múltiplas palavras de dados, o que pode acelerar programas significativamente. A arquitetura `x86_64` suporta vários padrões SIMD:
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
- [MMX]: O conjunto de instruções _Multi Media Extension_ foi introduzido em 1997 e define oito registradores de 64 bits chamados `mm0` até `mm7`. Esses registradores são apenas aliases para os registradores da [unidade de ponto flutuante x87].
- [SSE]: O conjunto de instruções _Streaming SIMD Extensions_ foi introduzido em 1999. Em vez de reutilizar os registradores de ponto flutuante, ele adiciona um conjunto de registradores completamente novo. Os dezesseis novos registradores são chamados `xmm0` até `xmm15` e têm 128 bits cada.
- [AVX]: As _Advanced Vector Extensions_ são extensões que aumentam ainda mais o tamanho dos registradores multimídia. Os novos registradores são chamados `ymm0` até `ymm15` e têm 256 bits cada. Eles estendem os registradores `xmm`, então por exemplo `xmm0` é a metade inferior de `ymm0`.
[MMX]: https://en.wikipedia.org/wiki/MMX_(instruction_set)
[unidade de ponto flutuante x87]: https://en.wikipedia.org/wiki/X87
[SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
[AVX]: https://en.wikipedia.org/wiki/Advanced_Vector_Extensions
Ao usar tais padrões SIMD, programas frequentemente podem acelerar significativamente. Bons compiladores são capazes de transformar loops normais em tal código SIMD automaticamente através de um processo chamado [auto-vetorização].
[auto-vetorização]: https://en.wikipedia.org/wiki/Automatic_vectorization
No entanto, os grandes registradores SIMD levam a problemas em kernels de SO. A razão é que o kernel tem que fazer backup de todos os registradores que usa para a memória em cada interrupção de hardware, porque eles precisam ter seus valores originais quando o programa interrompido continua. Então, se o kernel usa registradores SIMD, ele tem que fazer backup de muito mais dados (512-1600 bytes), o que diminui notavelmente o desempenho. Para evitar esta perda de desempenho, queremos desabilitar os recursos `sse` e `mmx` (o recurso `avx` é desabilitado por padrão).
Podemos fazer isso através do campo `features` na nossa especificação de alvo. Para desabilitar os recursos `mmx` e `sse`, nós os adicionamos prefixados com um menos:
```json
"features": "-mmx,-sse"
```
## Ponto Flutuante
Infelizmente para nós, a arquitetura `x86_64` usa registradores SSE para operações de ponto flutuante. Assim, todo uso de ponto flutuante com SSE desabilitado causa um erro no LLVM. O problema é que a biblioteca core do Rust já usa floats (por exemplo, ela implementa traits para `f32` e `f64`), então evitar floats no nosso kernel não é suficiente.
Felizmente, o LLVM tem suporte para um recurso `soft-float` que emula todas as operações de ponto flutuante através de funções de software baseadas em inteiros normais. Isso torna possível usar floats no nosso kernel sem SSE; será apenas um pouco mais lento.
Para ativar o recurso `soft-float` para o nosso kernel, nós o adicionamos à linha `features` na nossa especificação de alvo, prefixado com um mais:
```json
"features": "-mmx,-sse,+soft-float"
```
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.ru.md
================================================
+++
title = "Отключение SIMD"
weight = 2
path = "ru/disable-simd"
template = "edition-2/extra.html"
+++
Инструкции [Single Instruction Multiple Data (SIMD)] способны выполнять операцию (например, сложение) одновременно над несколькими словами данных, что может значительно ускорить работу программ. Архитектура `x86_64` поддерживает различные стандарты SIMD:
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
- [MMX]: Набор инструкций _Multi Media Extension_ был представлен в 1997 году и определяет восемь 64-битных регистров, называемых `mm0` - `mm7`. Эти регистры являются псевдонимами регистров [x87 блока с плавающей запятой][x87 floating point unit].
- [SSE]: Набор инструкций _Streaming SIMD Extensions_ был представлен в 1999 году. Вместо повторного использования регистров с плавающей запятой он добавляет совершенно новый набор регистров. Шестнадцать новых регистров называются `xmm0` - `xmm15` и имеют размер 128 бит каждый.
- [AVX]: _Advanced Vector Extensions_ - это расширения, которые еще больше увеличивают размер мультимедийных регистров. Новые регистры называются `ymm0` - `ymm15` и имеют размер 256 бит каждый. Они расширяют регистры `xmm`, поэтому, например, `xmm0` - это нижняя половина `ymm0`.
[MMX]: https://en.wikipedia.org/wiki/MMX_(instruction_set)
[x87 floating point unit]: https://en.wikipedia.org/wiki/X87
[SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
[AVX]: https://en.wikipedia.org/wiki/Advanced_Vector_Extensions
Используя такие стандарты SIMD, программы часто могут значительно ускориться. Хорошие компиляторы способны автоматически преобразовывать обычные циклы в такой SIMD-код с помощью процесса, называемого [автовекторизацией][auto-vectorization].
[auto-vectorization]: https://en.wikipedia.org/wiki/Automatic_vectorization
Однако большие регистры SIMD приводят к проблемам в ядрах ОС. Причина в том, что ядро должно создавать резервные копии всех регистров, которые оно использует, в память при каждом аппаратном прерывании, потому что они должны иметь свои первоначальные значения, когда прерванная программа продолжает работу. Поэтому, если ядро использует SIMD-регистры, ему приходится резервировать гораздо больше данных (512-1600 байт), что заметно снижает производительность. Чтобы избежать этого снижения производительности, мы хотим отключить функции `sse` и `mmx` (функция `avx` отключена по умолчанию).
Мы можем сделать это через поле `features` в нашей целевой спецификации. Чтобы отключить функции `mmx` и `sse`, мы добавим их с минусом:
```json
"features": "-mmx,-sse"
```
## Числа с плавающей точкой
К сожалению для нас, архитектура `x86_64` использует регистры SSE для операций с числами с плавающей точкой. Таким образом, каждое использование чисел с плавающей точкой с отключенным SSE вызовёт ошибку в LLVM. Проблема в том, что библиотека `core` уже использует числа с плавающей точкой (например, в ней реализованы трейты для `f32` и `f64`), поэтому недостаточно избегать чисел с плавающей точкой в нашем ядре.
К счастью, LLVM поддерживает функцию `soft-float`, эмулирующую все операции с числавами с плавающей точкой через программные функции, основанные на обычных целых числах. Это позволяет использовать плавающие числа в нашем ядре без SSE, просто это будет немного медленнее.
Чтобы включить функцию `soft-float` для нашего ядра, мы добавим ее в строку `features` в спецификации цели с префиксом плюс:
```json
"features": "-mmx,-sse,+soft-float"
```
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.zh-CN.md
================================================
+++
title = "Disable SIMD"
weight = 2
path = "zh-CN/disable-simd"
template = "edition-2/extra.html"
+++
[单指令多数据][Single Instruction Multiple Data (SIMD)] 指令允许在一个操作符(比如加法)内传入多组数据,以此加速程序执行速度。`x86_64` 架构支持多种SIMD标准:
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
- [MMX]: _多媒体扩展_ 指令集于1997年发布,定义了8个64位寄存器,分别被称为 `mm0` 到 `mm7`,不过,这些寄存器只是 [x87浮点执行单元][x87 floating point unit] 中寄存器的映射而已。
- [SSE]: _流处理SIMD扩展_ 指令集于1999年发布,不同于MMX的复用浮点执行单元,该指令集加入了一个完整的新寄存器组,即被称为 `xmm0` 到 `xmm15` 的16个128位寄存器。
- [AVX]: _先进矢量扩展_ 用于进一步扩展多媒体寄存器的数量,它定义了 `ymm0` 到 `ymm15` 共16个256位寄存器,但是这些寄存器继承于 `xmm`,例如 `xmm0` 寄存器是 `ymm0` 的低128位。
[MMX]: https://en.wikipedia.org/wiki/MMX_(instruction_set)
[x87 floating point unit]: https://en.wikipedia.org/wiki/X87
[SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
[AVX]: https://en.wikipedia.org/wiki/Advanced_Vector_Extensions
通过应用这些SIMD标准,计算机程序可以显著提高执行速度。优秀的编译器可以将常规循环自动优化为适用SIMD的代码,这种优化技术被称为 [自动矢量化][auto-vectorization]。
[auto-vectorization]: https://en.wikipedia.org/wiki/Automatic_vectorization
尽管如此,SIMD会让操作系统内核出现一些问题。具体来说,就是操作系统在处理硬件中断时,需要保存所有寄存器信息到内存中,在中断结束后再将其恢复以供使用。所以说,如果内核需要使用SIMD寄存器,那么每次处理中断需要备份非常多的数据(512-1600字节),这会显著地降低性能。要避免这部分性能损失,我们需要禁用 `sse` 和 `mmx` 这两个特性(`avx` 默认已禁用)。
我们可以在编译配置文件中的 `features` 配置项做出如下修改,加入以减号为前缀的 `mmx` 和 `sse` 即可:
```json
"features": "-mmx,-sse"
```
## 浮点数
还有一件不幸的事,`x86_64` 架构在处理浮点数计算时,会用到 `sse` 寄存器,因此,禁用SSE的前提下使用浮点数计算LLVM都一定会报错。 更大的问题在于Rust核心库里就存在着为数不少的浮点数运算(如 `f32` 和 `f64` 的数个trait),所以试图避免使用浮点数是不可能的。
幸运的是,LLVM支持 `soft-float` 特性,这个特性可以使用整型运算在软件层面模拟浮点数运算,使得我们为内核关闭SSE成为了可能,只需要牺牲一点点性能。
要为内核打开 `soft-float` 特性,我们只需要在编译配置文件中的 `features` 配置项做出如下修改即可:
```json
"features": "-mmx,-sse,+soft-float"
```
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/index.es.md
================================================
+++
title = "Un Kernel Mínimo en Rust"
weight = 2
path = "es/minimal-rust-kernel"
date = 2018-02-10
[extra]
chapter = "Bare Bones"
# GitHub usernames of the people that translated this post
translators = ["dobleuber"]
+++
En esta publicación, crearemos un kernel mínimo de 64 bits en Rust para la arquitectura x86. Partiremos del [un binario Rust autónomo] de la publicación anterior para crear una imagen de disco arrancable que imprima algo en la pantalla.
[un binario Rust autónomo]: @/edition-2/posts/01-freestanding-rust-binary/index.md
Este blog se desarrolla abiertamente en [GitHub]. Si tienes problemas o preguntas, por favor abre un issue ahí. También puedes dejar comentarios [al final]. El código fuente completo para esta publicación se encuentra en la rama [`post-02`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[al final]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
## El Proceso de Arranque {#el-proceso-de-arranque}
Cuando enciendes una computadora, comienza a ejecutar código de firmware almacenado en la [ROM] de la placa madre. Este código realiza una [prueba automática de encendido], detecta la memoria RAM disponible y preinicializa la CPU y el hardware. Después, busca un disco arrancable y comienza a cargar el kernel del sistema operativo.
[ROM]: https://en.wikipedia.org/wiki/Read-only_memory
[prueba automática de encendido]: https://en.wikipedia.org/wiki/Power-on_self-test
En x86, existen dos estándares de firmware: el “Sistema Básico de Entrada/Salida” (**[BIOS]**) y la más reciente “Interfaz de Firmware Extensible Unificada” (**[UEFI]**). El estándar BIOS es antiguo y está desactualizado, pero es simple y está bien soportado en cualquier máquina x86 desde los años 80. UEFI, en contraste, es más moderno y tiene muchas más funciones, pero es más complejo de configurar (al menos en mi opinión).
[BIOS]: https://en.wikipedia.org/wiki/BIOS
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
Actualmente, solo proporcionamos soporte para BIOS, pero también planeamos agregar soporte para UEFI. Si te gustaría ayudarnos con esto, revisa el [issue en Github](https://github.com/phil-opp/blog_os/issues/349).
### Arranque con BIOS
Casi todos los sistemas x86 tienen soporte para arranque con BIOS, incluyendo máquinas más recientes basadas en UEFI que usan un BIOS emulado. Esto es excelente, porque puedes usar la misma lógica de arranque en todas las máquinas del último siglo. Sin embargo, esta amplia compatibilidad también es la mayor desventaja del arranque con BIOS, ya que significa que la CPU se coloca en un modo de compatibilidad de 16 bits llamado [modo real] antes de arrancar, para que los bootloaders arcaicos de los años 80 sigan funcionando.
Pero comencemos desde el principio:
Cuando enciendes una computadora, carga el BIOS desde una memoria flash especial ubicada en la placa madre. El BIOS ejecuta rutinas de autoprueba e inicialización del hardware, y luego busca discos arrancables. Si encuentra uno, transfiere el control a su _bootloader_ (_cargador de arranque_), que es una porción de código ejecutable de 512 bytes almacenada al inicio del disco. La mayoría de los bootloaders son más grandes que 512 bytes, por lo que suelen dividirse en una pequeña primera etapa, que cabe en esos 512 bytes, y una segunda etapa que se carga posteriormente.
El bootloader debe determinar la ubicación de la imagen del kernel en el disco y cargarla en la memoria. Tambien necesita cambiar la CPU del [modo real] de 16 bits primero al [modo protegido] de 32 bits, y luego al [modo largo] de 64 bits, donde están disponibles los registros de 64 bits y toda la memoria principal. Su tercera tarea es consultar cierta información (como un mapa de memoria) desde el BIOS y pasársela al kernel del sistema operativo.
[modo real]: https://en.wikipedia.org/wiki/Real_mode
[modo protegido]: https://en.wikipedia.org/wiki/Protected_mode
[modo largo]: https://en.wikipedia.org/wiki/Long_mode
[segmentación de memoria]: https://en.wikipedia.org/wiki/X86_memory_segmentation
Escribir un bootloader es un poco tedioso, ya que requiere lenguaje ensamblador y muchos pasos poco claros como “escribir este valor mágico en este registro del procesador”. Por ello, no cubrimos la creación de bootloaders en este artículo y en su lugar proporcionamos una herramienta llamada [bootimage] que automatiza el proceso de creación de un bootloader.
[bootimage]: https://github.com/rust-osdev/bootimage
Si te interesa construir tu propio bootloader: ¡Estén atentos! Un conjunto de artículos sobre este tema está en camino.
#### El Estándar Multiboot
Para evitar que cada sistema operativo implemente su propio bootloader, que sea compatible solo con un único sistema, la [Free Software Foundation] creó en 1995 un estándar abierto de bootloaders llamado [Multiboot]. El estándar define una interfaz entre el bootloader y el sistema operativo, de modo que cualquier bootloader compatible con Multiboot pueda cargar cualquier sistema operativo compatible con Multiboot. La implementación de referencia es [GNU GRUB], que es el bootloader más popular para sistemas Linux.
[Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation
[Multiboot]: https://wiki.osdev.org/Multiboot
[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB
Para hacer un kernel compatible con Multiboot, solo necesitas insertar un llamado [encabezado Multiboot] al inicio del archivo del kernel. Esto hace que arrancar un sistema operativo desde GRUB sea muy sencillo. Sin embargo, GRUB y el estándar Multiboot también tienen algunos problemas:
[encabezado Multiboot]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
- Solo soportan el modo protegido de 32 bits. Esto significa que aún tienes que configurar la CPU para cambiar al modo largo de 64 bits.
- Están diseñados para simplificar el cargador de arranque en lugar del kernel. Por ejemplo, el kernel necesita vincularse con un [tamaño de página predeterminado ajustado], porque GRUB no puede encontrar el encabezado Multiboot de otro modo. Otro ejemplo es que la [información de arranque], que se pasa al kernel, contiene muchas estructuras dependientes de la arquitectura en lugar de proporcionar abstracciones limpias.
- Tanto GRUB como el estándar Multiboot están escasamente documentados.
- GRUB necesita instalarse en el sistema host para crear una imagen de disco arrancable a partir del archivo del kernel. Esto dificulta el desarrollo en Windows o Mac.
[tamaño de página predeterminado ajustado]: https://wiki.osdev.org/Multiboot#Multiboot_2
[información de arranque]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
Debido a estas desventajas, decidimos no usar GRUB ni el estándar Multiboot. Sin embargo, planeamos agregar soporte para Multiboot a nuestra herramienta [bootimage], para que sea posible cargar tu kernel en un sistema GRUB también. Si te interesa escribir un kernel compatible con Multiboot, revisa la [primera edición] de esta serie de blogs.
[primera edición]: @/edition-1/_index.md
### UEFI
(Por el momento no proporcionamos soporte para UEFI, ¡pero nos encantaría hacerlo! Si deseas ayudar, por favor háznoslo saber en el [issue de Github](https://github.com/phil-opp/blog_os/issues/349).)
## Un Kernel Mínimo
Ahora que tenemos una idea general de cómo arranca una computadora, es momento de crear nuestro propio kernel mínimo. Nuestro objetivo es crear una imagen de disco que, al arrancar, imprima “Hello World!” en la pantalla. Para esto, extendemos el [un binario Rust autónomo] del artículo anterior.
Como recordarás, construimos el binario independiente mediante `cargo`, pero dependiendo del sistema operativo, necesitábamos diferentes nombres de punto de entrada y banderas de compilación. Esto se debe a que `cargo` construye por defecto para el _sistema anfitrión_, es decir, el sistema en el que estás ejecutando el comando. Esto no es lo que queremos para nuestro kernel, ya que un kernel que funcione encima, por ejemplo, de Windows, no tiene mucho sentido. En su lugar, queremos compilar para un _sistema destino_ claramente definido.
### Instalación de Rust Nightly {#instalacion-de-rust-nightly}
Rust tiene tres canales de lanzamiento: _stable_, _beta_ y _nightly_. El libro de Rust explica muy bien la diferencia entre estos canales, así que tómate un momento para [revisarlo](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). Para construir un sistema operativo, necesitaremos algunas características experimentales que solo están disponibles en el canal nightly, por lo que debemos instalar una versión nightly de Rust.
Para administrar instalaciones de Rust, recomiendo ampliamente [rustup]. Este permite instalar compiladores nightly, beta y estable lado a lado, y facilita mantenerlos actualizados. Con rustup, puedes usar un compilador nightly en el directorio actual ejecutando `rustup override set nightly`. Alternativamente, puedes agregar un archivo llamado `rust-toolchain` con el contenido `nightly` en el directorio raíz del proyecto. Puedes verificar que tienes una versión nightly instalada ejecutando `rustc --version`: el número de versión debería contener `-nightly` al final.
[rustup]: https://www.rustup.rs/
El compilador nightly nos permite activar varias características experimentales utilizando las llamadas _banderas de características_ al inicio de nuestro archivo. Por ejemplo, podríamos habilitar el macro experimental [`asm!`] para ensamblador en línea agregando `#![feature(asm)]` en la parte superior de nuestro archivo `main.rs`. Ten en cuenta que estas características experimentales son completamente inestables, lo que significa que futuras versiones de Rust podrían cambiarlas o eliminarlas sin previo aviso. Por esta razón, solo las utilizaremos si son absolutamente necesarias.
[`asm!`]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
### Especificación del Objetivo
Cargo soporta diferentes sistemas destino mediante el parámetro `--target`. El destino se describe mediante un _[tripleta de destino]_, que especifica la arquitectura de la CPU, el proveedor, el sistema operativo y el [ABI]. Por ejemplo, el tripleta de destino `x86_64-unknown-linux-gnu` describe un sistema con una CPU `x86_64`, sin un proveedor claro, y un sistema operativo Linux con el ABI GNU. Rust soporta [muchas tripleta de destino diferentes][platform-support], incluyendo `arm-linux-androideabi` para Android o [`wasm32-unknown-unknown` para WebAssembly](https://www.hellorust.com/setup/wasm-target/).
[tripleta de destino]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[ABI]: https://stackoverflow.com/a/2456882
[platform-support]: https://forge.rust-lang.org/release/platform-support.html
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
Para nuestro sistema destino, sin embargo, requerimos algunos parámetros de configuración especiales (por ejemplo, sin un sistema operativo subyacente), por lo que ninguno de los [tripletas de destino existentes][platform-support] encaja. Afortunadamente, Rust nos permite definir [nuestros propios objetivos][custom-targets] mediante un archivo JSON. Por ejemplo, un archivo JSON que describe el objetivo `x86_64-unknown-linux-gnu` se ve así:
```json
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
La mayoría de los campos son requeridos por LLVM para generar código para esa plataforma. Por ejemplo, el campo [`data-layout`] define el tamaño de varios tipos de enteros, números de punto flotante y punteros. Luego, hay campos que Rust utiliza para la compilación condicional, como `target-pointer-width`. El tercer tipo de campo define cómo debe construirse el crate. Por ejemplo, el campo `pre-link-args` especifica argumentos que se pasan al [linker].
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
[linker]: https://en.wikipedia.org/wiki/Linker_(computing)
Nuestro kernel también tiene como objetivo los sistemas `x86_64`, por lo que nuestra especificación de objetivo será muy similar a la anterior. Comencemos creando un archivo llamado `x86_64-blog_os.json` (puedes elegir el nombre que prefieras) con el siguiente contenido común:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true
}
```
Ten en cuenta que cambiamos el sistema operativo en el campo `llvm-target` y en el campo `os` a `none`, porque nuestro kernel se ejecutará directamente sobre hardware sin un sistema operativo subyacente.
Agregamos las siguientes entradas relacionadas con la construcción:
```json
"linker-flavor": "ld.lld",
"linker": "rust-lld",
```
En lugar de usar el enlazador predeterminado de la plataforma (que podría no soportar objetivos de Linux), utilizamos el enlazador multiplataforma [LLD] que se incluye con Rust para enlazar nuestro kernel.
[LLD]: https://lld.llvm.org/
```json
"panic-strategy": "abort",
```
Esta configuración especifica que el objetivo no soporta [stack unwinding] en caso de un pánico, por lo que el programa debería abortar directamente. Esto tiene el mismo efecto que la opción `panic = "abort"` en nuestro archivo Cargo.toml, por lo que podemos eliminarla de ahí. (Ten en cuenta que, a diferencia de la opción en Cargo.toml, esta opción del destino también se aplica cuando recompilamos la biblioteca `core` más adelante en este artículo. Por lo tanto, incluso si prefieres mantener la opción en Cargo.toml, asegúrate de incluir esta opción.)
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
```json
"disable-redzone": true,
```
Estamos escribiendo un kernel, por lo que en algún momento necesitaremos manejar interrupciones. Para hacerlo de manera segura, debemos deshabilitar una optimización del puntero de pila llamada _“red zone”_, ya que de lo contrario podría causar corrupción en la pila. Para más información, consulta nuestro artículo sobre [cómo deshabilitar la red zone].
[deshabilitar la red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
```json
"features": "-mmx,-sse,+soft-float",
```
El campo `features` habilita o deshabilita características del destinos. Deshabilitamos las características `mmx` y `sse` anteponiéndoles un signo menos y habilitamos la característica `soft-float` anteponiéndole un signo más. Ten en cuenta que no debe haber espacios entre las diferentes banderas, ya que de lo contrario LLVM no podrá interpretar correctamente la cadena de características.
Las características `mmx` y `sse` determinan el soporte para instrucciones [Single Instruction Multiple Data (SIMD)], que a menudo pueden acelerar significativamente los programas. Sin embargo, el uso de los registros SIMD en kernels de sistemas operativos genera problemas de rendimiento. Esto se debe a que el kernel necesita restaurar todos los registros a su estado original antes de continuar un programa interrumpido. Esto implica que el kernel debe guardar el estado completo de SIMD en la memoria principal en cada llamada al sistema o interrupción de hardware. Dado que el estado SIMD es muy grande (512–1600 bytes) y las interrupciones pueden ocurrir con mucha frecuencia, estas operaciones adicionales de guardar/restaurar afectan considerablemente el rendimiento. Para evitar esto, deshabilitamos SIMD para nuestro kernel (pero no para las aplicaciones que se ejecutan encima).
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
Un problema al deshabilitar SIMD es que las operaciones de punto flotante en `x86_64` requieren registros SIMD por defecto. Para resolver este problema, agregamos la característica `soft-float`, que emula todas las operaciones de punto flotante mediante funciones de software basadas en enteros normales.
Para más información, consulta nuestro artículo sobre [cómo deshabilitar SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
#### Juntándolo Todo
Nuestro archivo de especificación de objetivo ahora se ve así:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}
```
### Construyendo nuestro Kernel
Compilar para nuestro nuevo objetivo usará convenciones de Linux, ya que la opción de enlazador `ld.lld` instruye a LLVM a compilar con la bandera `-flavor gnu` (para más opciones del enlazador, consulta [la documentación de rustc](https://doc.rust-lang.org/rustc/codegen-options/index.html#linker-flavor)). Esto significa que necesitamos un punto de entrada llamado `_start`, como se describió en el [artículo anterior]:
[artículo anterior]: @/edition-2/posts/01-freestanding-rust-binary/index.md
```rust
// src/main.rs
#![no_std] // no enlazar con la biblioteca estándar de Rust
#![no_main] // deshabilitar todos los puntos de entrada a nivel de Rust
use core::panic::PanicInfo;
/// Esta función se llama cuando ocurre un pánico.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[unsafe(no_mangle)] // no modificar el nombre de esta función
pub extern "C" fn _start() -> ! {
// esta función es el punto de entrada, ya que el enlazador busca una función
// llamada `_start` por defecto
loop {}
}
```
Ten en cuenta que el punto de entrada debe llamarse `_start` sin importar el sistema operativo anfitrión.
Ahora podemos construir el kernel para nuestro nuevo objetivo pasando el nombre del archivo JSON como `--target`:
```
> cargo build --target x86_64-blog_os.json
error: `.json` target specs require -Zjson-target-spec
```
¡Falla! El error nos indica que las especificaciones de objetivo JSON personalizadas son una característica inestable que requiere habilitación explícita. Esto se debe a que el formato de los archivos JSON de objetivo aún no se considera estable, por lo que podrían ocurrir cambios en futuras versiones de Rust. Consulta el [issue de seguimiento para especificaciones de objetivo JSON personalizadas][json-target-spec-issue] para más información.
[json-target-spec-issue]: https://github.com/rust-lang/rust/issues/151528
#### La Opción `json-target-spec`
Para habilitar el soporte para especificaciones de objetivo JSON personalizadas, necesitamos crear un archivo de configuración local de [cargo] en `.cargo/config.toml` (la carpeta `.cargo` debería estar junto a tu carpeta `src`) con el siguiente contenido:
[cargo]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# en .cargo/config.toml
[unstable]
json-target-spec = true
```
Esto habilita la característica inestable `json-target-spec`, permitiéndonos usar archivos JSON de objetivo personalizados.
Con esta configuración en su lugar, intentemos construir nuevamente:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
```
¡Ahora vemos un error diferente! El error nos indica que el compilador de Rust ya no encuentra la [biblioteca `core`]. Esta biblioteca contiene tipos básicos de Rust como `Result`, `Option` e iteradores, y se vincula implícitamente a todos los crates con `no_std`.
[biblioteca `core`]: https://doc.rust-lang.org/nightly/core/index.html
El problema es que la biblioteca `core` se distribuye junto con el compilador de Rust como una biblioteca _precompilada_. Por lo tanto, solo es válida para tripletas de anfitrión soportados (por ejemplo, `x86_64-unknown-linux-gnu`), pero no para nuestro objetivo personalizado. Si queremos compilar código para otros objetivos, necesitamos recompilar `core` para esos objetivos primero.
#### La Opción `build-std`
Aquí es donde entra en juego la característica [`build-std`] de cargo. Esta permite recompilar `core` y otras bibliotecas estándar bajo demanda, en lugar de usar las versiones precompiladas que vienen con la instalación de Rust. Esta característica es muy nueva y aún no está terminada, por lo que está marcada como "inestable" y solo está disponible en los [compiladores de Rust nightly].
[`build-std`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[compiladores de Rust nightly]: #instalacion-de-rust-nightly
Para usar esta característica, necesitamos añadir lo siguiente a nuestro archivo de configuración de [cargo] en `.cargo/config.toml`:
```toml
# en .cargo/config.toml
[unstable]
json-target-spec = true
build-std = ["core", "compiler_builtins"]
```
Esto le indica a cargo que debe recompilar las bibliotecas `core` y `compiler_builtins`. Esta última es necesaria porque es una dependencia de `core`. Para poder recompilar estas bibliotecas, cargo necesita acceso al código fuente de Rust, el cual podemos instalar ejecutando `rustup component add rust-src`.
**Nota:** La clave de configuración `unstable.build-std` requiere al menos la versión de Rust nightly del 15 de julio de 2020.
Después de configurar la clave `unstable.build-std` e instalar el componente `rust-src`, podemos ejecutar nuevamente nuestro comando de construcción:
```
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
Vemos que `cargo build` ahora recompila las bibliotecas `core`, `rustc-std-workspace-core` (una dependencia de `compiler_builtins`) y `compiler_builtins` para nuestro objetivo personalizado.
#### Intrínsecos Relacionados con la Memoria
El compilador de Rust asume que un cierto conjunto de funciones integradas está disponible para todos los sistemas. La mayoría de estas funciones son proporcionadas por el crate `compiler_builtins`, que acabamos de recompilar. Sin embargo, hay algunas funciones relacionadas con la memoria en ese crate que no están habilitadas por defecto, ya que normalmente son proporcionadas por la biblioteca C del sistema. Estas funciones incluyen `memset`, que establece todos los bytes de un bloque de memoria a un valor dado, `memcpy`, que copia un bloque de memoria a otro, y `memcmp`, que compara dos bloques de memoria. Aunque no necesitamos estas funciones para compilar nuestro kernel en este momento, serán necesarias tan pronto como agreguemos más código (por ejemplo, al copiar estructuras).
Dado que no podemos vincularnos a la biblioteca C del sistema operativo, necesitamos una forma alternativa de proporcionar estas funciones al compilador. Una posible solución podría ser implementar nuestras propias funciones `memset`, `memcpy`, etc., y aplicarles el atributo `#[unsafe(no_mangle)]` (para evitar el renombramiento automático durante la compilación). Sin embargo, esto es peligroso, ya que el más mínimo error en la implementación de estas funciones podría conducir a un comportamiento indefinido. Por ejemplo, implementar `memcpy` con un bucle `for` podría resultar en una recursión infinita, ya que los bucles `for` llaman implícitamente al método del trait [`IntoIterator::into_iter`], que podría invocar nuevamente a `memcpy`. Por lo tanto, es una buena idea reutilizar implementaciones existentes y bien probadas.
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
Afortunadamente, el crate `compiler_builtins` ya contiene implementaciones para todas las funciones necesarias, pero están deshabilitadas por defecto para evitar conflictos con las implementaciones de la biblioteca C. Podemos habilitarlas configurando la bandera [`build-std-features`] de cargo como `["compiler-builtins-mem"]`. Al igual que la bandera `build-std`, esta bandera puede pasarse como un flag `-Z` en la línea de comandos o configurarse en la tabla `unstable` en el archivo `.cargo/config.toml`. Dado que siempre queremos compilar con esta bandera, la opción de archivo de configuración tiene más sentido para nosotros:
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
```toml
# en .cargo/config.toml
[unstable]
json-target-spec = true
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
```
(El soporte para la característica `compiler-builtins-mem` fue [añadido muy recientemente](https://github.com/rust-lang/rust/pull/77284), por lo que necesitas al menos Rust nightly `2020-09-30` para usarla).
Detrás de escena, esta bandera habilita la [característica `mem`] del crate `compiler_builtins`. El efecto de esto es que el atributo `#[unsafe(no_mangle)]` se aplica a las [implementaciones de `memcpy`, etc.] del crate, lo que las hace disponibles para el enlazador.
[característica `mem`]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
[implementaciones de `memcpy`, etc.]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
Con este cambio, nuestro kernel tiene implementaciones válidas para todas las funciones requeridas por el compilador, por lo que continuará compilándose incluso si nuestro código se vuelve más complejo.
#### Configurar un Objetivo Predeterminado
Para evitar pasar el parámetro `--target` en cada invocación de `cargo build`, podemos sobrescribir el objetivo predeterminado. Para hacer esto, añadimos lo siguiente a nuestro archivo de [configuración de cargo] en `.cargo/config.toml`:
[configuración de cargo]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# en .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
Esto le indica a `cargo` que use nuestro objetivo `x86_64-blog_os.json` cuando no se pase explícitamente el argumento `--target`. Esto significa que ahora podemos construir nuestro kernel con un simple `cargo build`. Para más información sobre las opciones de configuración de cargo, consulta la [documentación oficial][configuración de cargo].
Ahora podemos construir nuestro kernel para un objetivo bare metal con un simple `cargo build`. Sin embargo, nuestro punto de entrada `_start`, que será llamado por el cargador de arranque, aún está vacío. Es momento de mostrar algo en la pantalla desde ese punto.
### Imprimiendo en Pantalla
La forma más sencilla de imprimir texto en la pantalla en esta etapa es usando el [búfer de texto VGA]. Es un área de memoria especial mapeada al hardware VGA que contiene el contenido mostrado en pantalla. Normalmente consta de 25 líneas, cada una con 80 celdas de caracteres. Cada celda de carácter muestra un carácter ASCII con algunos colores de primer plano y fondo. La salida en pantalla se ve así:
[búfer de texto VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode

Discutiremos el diseño exacto del búfer VGA en el próximo artículo, donde escribiremos un primer controlador pequeño para él. Para imprimir “Hello World!”, solo necesitamos saber que el búfer está ubicado en la dirección `0xb8000` y que cada celda de carácter consta de un byte ASCII y un byte de color.
La implementación se ve así:
```rust
static HELLO: &[u8] = b"Hello World!";
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
Primero, convertimos el entero `0xb8000` en un [raw pointer]. Luego, [iteramos] sobre los bytes de la [cadena de bytes estática] `HELLO`. Usamos el método [`enumerate`] para obtener adicionalmente una variable de conteo `i`. En el cuerpo del bucle `for`, utilizamos el método [`offset`] para escribir el byte de la cadena y el byte de color correspondiente (`0xb` representa un cian claro).
[iteramos]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html
[raw pointer]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[estática]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
[cadena de bytes]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
Ten en cuenta que hay un bloque [`unsafe`] alrededor de todas las escrituras de memoria. Esto se debe a que el compilador de Rust no puede probar que los punteros crudos que creamos son válidos. Podrían apuntar a cualquier lugar y causar corrupción de datos. Al poner estas operaciones en un bloque `unsafe`, básicamente le decimos al compilador que estamos absolutamente seguros de que las operaciones son válidas. Sin embargo, un bloque `unsafe` no desactiva las verificaciones de seguridad de Rust; simplemente permite realizar [cinco operaciones adicionales].
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
[cinco operaciones adicionales]: https://doc.rust-lang.org/stable/book/ch20-01-unsafe-rust.html#unsafe-superpowers
Quiero enfatizar que **esta no es la forma en que queremos hacer las cosas en Rust**. Es muy fácil cometer errores al trabajar con punteros crudos dentro de bloques `unsafe`. Por ejemplo, podríamos escribir más allá del final del búfer si no somos cuidadosos.
Por lo tanto, queremos minimizar el uso de `unsafe` tanto como sea posible. Rust nos permite lograr esto creando abstracciones seguras. Por ejemplo, podríamos crear un tipo de búfer VGA que encapsule toda la inseguridad y garantice que sea _imposible_ hacer algo incorrecto desde el exterior. De esta manera, solo necesitaríamos cantidades mínimas de código `unsafe` y podríamos estar seguros de no violar la [seguridad de la memoria]. Crearemos una abstracción segura para el búfer VGA en el próximo artículo.
[seguridad de la memoria]: https://en.wikipedia.org/wiki/Memory_safety
## Ejecutando Nuestro Kernel
Ahora que tenemos un ejecutable que realiza algo perceptible, es momento de ejecutarlo. Primero, necesitamos convertir nuestro kernel compilado en una imagen de disco arrancable vinculándolo con un cargador de arranque. Luego, podemos ejecutar la imagen de disco en la máquina virtual [QEMU] o iniciarla en hardware real usando una memoria USB.
### Creando una Bootimage
Para convertir nuestro kernel compilado en una imagen de disco arrancable, debemos vincularlo con un cargador de arranque. Como aprendimos en la [sección sobre el proceso de arranque], el cargador de arranque es responsable de inicializar la CPU y cargar nuestro kernel.
[sección sobre el proceso de arranque]: #el-proceso-de-arranque
En lugar de escribir nuestro propio cargador de arranque, lo cual es un proyecto en sí mismo, usamos el crate [`bootloader`]. Este crate implementa un cargador de arranque básico para BIOS sin dependencias en C, solo Rust y ensamblador en línea. Para usarlo y arrancar nuestro kernel, necesitamos agregarlo como dependencia:
[`bootloader`]: https://crates.io/crates/bootloader
```toml
# en Cargo.toml
[dependencies]
bootloader = "0.9"
```
**Nota:** Este artículo solo es compatible con `bootloader v0.9`. Las versiones más recientes usan un sistema de construcción diferente y generarán errores de compilación al seguir este artículo.
Agregar el bootloader como dependencia no es suficiente para crear una imagen de disco arrancable. El problema es que necesitamos vincular nuestro kernel con el bootloader después de la compilación, pero cargo no tiene soporte para [scripts post-compilación].
[scripts post-compilación]: https://github.com/rust-lang/cargo/issues/545
Para resolver este problema, creamos una herramienta llamada `bootimage` que primero compila el kernel y el bootloader, y luego los vincula para crear una imagen de disco arrancable. Para instalar esta herramienta, dirígete a tu directorio de inicio (o cualquier directorio fuera de tu proyecto de cargo) y ejecuta el siguiente comando en tu terminal:
```
cargo install bootimage
```
Para ejecutar `bootimage` y compilar el bootloader, necesitas tener instalado el componente `llvm-tools-preview` de rustup. Puedes hacerlo ejecutando el comando correspondiente.
Después de instalar `bootimage` y agregar el componente `llvm-tools-preview`, puedes crear una imagen de disco arrancable regresando al directorio de tu proyecto de cargo y ejecutando:
```
> cargo bootimage
```
Vemos que la herramienta recompila nuestro kernel usando `cargo build`, por lo que automáticamente aplicará cualquier cambio que realices. Después, compila el bootloader, lo cual puede tardar un poco. Como ocurre con todas las dependencias de los crates, solo se compila una vez y luego se almacena en caché, por lo que las compilaciones posteriores serán mucho más rápidas. Finalmente, `bootimage` combina el bootloader y tu kernel en una imagen de disco arrancable.
Después de ejecutar el comando, deberías ver una imagen de disco arrancable llamada `bootimage-blog_os.bin` en tu directorio `target/x86_64-blog_os/debug`. Puedes arrancarla en una máquina virtual o copiarla a una unidad USB para arrancarla en hardware real. (Ten en cuenta que esta no es una imagen de CD, que tiene un formato diferente, por lo que grabarla en un CD no funcionará).
#### ¿Cómo funciona?
La herramienta `bootimage` realiza los siguientes pasos detrás de escena:
- Compila nuestro kernel en un archivo [ELF].
- Compila la dependencia del bootloader como un ejecutable independiente.
- Vincula los bytes del archivo ELF del kernel con el bootloader.
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
Al arrancar, el bootloader lee y analiza el archivo ELF anexado. Luego, mapea los segmentos del programa a direcciones virtuales en las tablas de páginas, inicializa a cero la sección `.bss` y configura una pila. Finalmente, lee la dirección del punto de entrada (nuestra función `_start`) y salta a ella.
### Arrancando en QEMU
Ahora podemos arrancar la imagen de disco en una máquina virtual. Para arrancarla en [QEMU], ejecuta el comando correspondiente.
[QEMU]: https://www.qemu.org/
```
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
```
Esto abre una ventana separada que debería verse similar a esto:

Vemos que nuestro "Hello World!" es visible en la pantalla.
### Máquina Real
También es posible escribir la imagen a una memoria USB y arrancarla en una máquina real, **pero ten mucho cuidado** al elegir el nombre correcto del dispositivo, porque **todo en ese dispositivo será sobrescrito**:
```
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
```
Donde `sdX` es el nombre del dispositivo de tu memoria USB.
Después de escribir la imagen en la memoria USB, puedes ejecutarla en hardware real iniciando desde ella. Probablemente necesitarás usar un menú de arranque especial o cambiar el orden de arranque en la configuración del BIOS para iniciar desde la memoria USB. Ten en cuenta que actualmente no funciona para máquinas UEFI, ya que el crate `bootloader` aún no tiene soporte para UEFI.
### Usando `cargo run`
Para facilitar la ejecución de nuestro kernel en QEMU, podemos configurar la clave de configuración `runner` para cargo:
```toml
# en .cargo/config.toml
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
```
La tabla `target.'cfg(target_os = "none")'` se aplica a todos los objetivos cuyo campo `"os"` en el archivo de configuración del objetivo esté configurado como `"none"`. Esto incluye nuestro objetivo `x86_64-blog_os.json`. La clave `runner` especifica el comando que debe ejecutarse para `cargo run`. El comando se ejecuta después de una compilación exitosa, con la ruta del ejecutable pasada como el primer argumento. Consulta la [documentación de cargo][configuración de cargo] para más detalles.
El comando `bootimage runner` está específicamente diseñado para ser utilizado como un ejecutable `runner`. Vincula el ejecutable dado con la dependencia del bootloader del proyecto y luego lanza QEMU. Consulta el [README de `bootimage`] para más detalles y posibles opciones de configuración.
[README de `bootimage`]: https://github.com/rust-osdev/bootimage
Ahora podemos usar `cargo run` para compilar nuestro kernel e iniciarlo en QEMU.
## ¿Qué sigue?
En el próximo artículo, exploraremos el búfer de texto VGA con más detalle y escribiremos una interfaz segura para él. También añadiremos soporte para el macro `println`.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/index.fa.md
================================================
+++
title = "یک هسته مینیمال با Rust"
weight = 2
path = "fa/minimal-rust-kernel"
date = 2018-02-10
[extra]
# Please update this when updating the translation
translation_based_on_commit = "7212ffaa8383122b1eb07fe1854814f99d2e1af4"
# GitHub usernames of the people that translated this post
translators = ["hamidrezakp", "MHBahrampour"]
rtl = true
+++
در این پست ما برای معماری x86 یک هسته مینیمال ۶۴ بیتی به زبان راست میسازیم. با استفاده از باینری مستقل Rust از پست قبل، یک دیسک ایمیج قابل بوت میسازیم، که متنی را در صفحه چاپ کند.
[باینری مستقل Rust]: @/edition-2/posts/01-freestanding-rust-binary/index.md
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. شما همچنین میتوانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را میتوانید در بِرَنچ [`post-02`][post branch] پیدا کنید.
[گیتهاب]: https://github.com/phil-opp/blog_os
[در زیر]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
## فرآیند بوت شدن
وقتی یک رایانه را روشن میکنید، شروع به اجرای کد فِرْموِر (کلمه: firmware) ذخیره شده در [ROM] مادربرد میکند. این کد یک [power-on self-test] انجام میدهد، رم موجود را تشخیص داده، و پردازنده و سخت افزار را پیش مقداردهی اولیه میکند. پس از آن به یک دنبال دیسک قابل بوت میگردد و شروع به بوت کردن هسته سیستم عامل میکند.
[ROM]: https://en.wikipedia.org/wiki/Read-only_memory
[power-on self-test]: https://en.wikipedia.org/wiki/Power-on_self-test
در x86، دو استاندارد فِرْموِر (کلمه: firmware) وجود دارد: «سامانهٔ ورودی/خروجیِ پایه» (**[BIOS]**) و استاندارد جدیدتر «رابط فِرْموِر توسعه یافته یکپارچه» (**[UEFI]**). استاندارد BIOS قدیمی و منسوخ است، اما ساده است و از دهه ۱۹۸۰ تاکنون در هر دستگاه x86 کاملاً پشتیبانی میشود. در مقابل، UEFI مدرنتر است و ویژگیهای بسیار بیشتری دارد، اما راه اندازی آن پیچیدهتر است (حداقل به نظر من).
[BIOS]: https://en.wikipedia.org/wiki/BIOS
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
در حال حاضر، ما فقط پشتیبانی BIOS را ارائه میدهیم، اما پشتیبانی از UEFI نیز برنامهریزی شده است. اگر میخواهید در این زمینه به ما کمک کنید، [ایشو گیتهاب](https://github.com/phil-opp/blog_os/issues/349) را بررسی کنید.
### بوت شدن BIOS
تقریباً همه سیستمهای x86 از بوت شدن BIOS پشتیبانی میکنند، از جمله سیستمهای جدیدترِ مبتنی بر UEFI که از BIOS شبیهسازی شده استفاده میکنند. این عالی است، زیرا شما میتوانید از منطق بوت یکسانی در تمام سیستمهای قرنهای گذشته استفاده کنید. اما این سازگاری گسترده در عین حال بزرگترین نقطه ضعف راهاندازی BIOS است، زیرا این بدان معناست که پردازنده قبل از بوت شدن در یک حالت سازگاری 16 بیتی به نام [real mode] قرار داده میشود تا بوتلودرهای قدیمی از دهه 1980 همچنان کار کنند.
اما بیایید از ابتدا شروع کنیم:
وقتی یک رایانه را روشن میکنید، BIOS را از حافظه فلش مخصوصی که روی مادربرد قرار دارد بارگذاری میکند. BIOS روالهای خودآزمایی و مقداردهی اولیه سخت افزار را اجرا می کند، سپس به دنبال دیسکهای قابل بوت میگردد. اگر یکی را پیدا کند، کنترل به _بوتلودرِ_ آن منتقل میشود، که یک قسمت ۵۱۲ بایتی از کد اجرایی است و در ابتدای دیسک ذخیره شده است. بیشتر بوتلودرها از ۵۱۲ بایت بزرگتر هستند، بنابراین بوتلودرها معمولاً به یک قسمت کوچک ابتدایی تقسیم میشوند که در ۵۱۲ بایت جای میگیرد و قسمت دوم که متعاقباً توسط قسمت اول بارگذاری میشود.
بوتلودر باید محل ایمیج هسته را بر روی دیسک تعیین کرده و آن را در حافظه بارگذاری کند. همچنین ابتدا باید CPU را از [real mode] (ترجمه: حالت واقعی) 16 بیتی به [protected mode] (ترجمه: حالت محافظت شده) 32 بیتی و سپس به [long mode] (ترجمه: حالت طولانی) 64 بیتی سوییچ کند، جایی که ثباتهای 64 بیتی و کل حافظه اصلی در آن در دسترس هستند. کار سوم آن پرسوجو درباره اطلاعات خاص (مانند نگاشت حافظه) از BIOS و انتقال آن به هسته سیستم عامل است.
[real mode]: https://en.wikipedia.org/wiki/Real_mode
[protected mode]: https://en.wikipedia.org/wiki/Protected_mode
[long mode]: https://en.wikipedia.org/wiki/Long_mode
[memory segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation
نوشتن بوتلودر کمی دشوار است زیرا به زبان اسمبلی و بسیاری از مراحل غیر بصیرانه مانند "نوشتن این مقدار جادویی در این ثبات پردازنده" نیاز دارد. بنابراین ما در این پست ایجاد بوتلودر را پوشش نمیدهیم و در عوض ابزاری به نام [bootimage] را ارائه میدهیم که بوتلودر را به طور خودکار به هسته شما اضافه میکند.
[bootimage]: https://github.com/rust-osdev/bootimage
اگر علاقهمند به ساخت بوتلودر هستید: با ما همراه باشید، مجموعهای از پستها در این زمینه از قبل برنامهریزی شده است!
#### استاندارد بوت چندگانه
برای جلوگیری از این که هر سیستم عاملی بوتلودر خود را پیادهسازی کند، که فقط با یک سیستم عامل سازگار است، [بنیاد نرم افزار آزاد] در سال 1995 یک استاندارد بوتلودر آزاد به نام [Multiboot] ایجاد کرد. این استاندارد یک رابط بین بوتلودر و سیستم عامل را تعریف میکند، به طوری که هر بوتلودر سازگار با Multiboot میتواند هر سیستم عامل سازگار با Multiboot را بارگذاری کند. پیادهسازی مرجع [GNU GRUB] است که محبوبترین بوتلودر برای سیستمهای لینوکس است.
[بنیاد نرم افزار آزاد]: https://en.wikipedia.org/wiki/Free_Software_Foundation
[Multiboot]: https://wiki.osdev.org/Multiboot
[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB
برای سازگار کردن هسته با Multiboot، کافیست یک به اصطلاح [Multiboot header] را در ابتدای فایل هسته اضافه کنید. با این کار بوت کردن سیستم عامل در GRUB بسیار آسان خواهد شد. با این حال، GRUB و استاندارد Multiboot نیز دارای برخی مشکلات هستند:
[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
- آنها فقط از حالت محافظت شده 32 بیتی پشتیبانی میکنند. این بدان معناست که شما برای تغییر به حالت طولانی 64 بیتی هنوز باید پیکربندی CPU را انجام دهید.
- آنها برای ساده سازی بوتلودر طراحی شدهاند نه برای ساده سازی هسته. به عنوان مثال، هسته باید با [اندازه صفحه پیش فرض تنظیم شده] پیوند داده شود، زیرا GRUB در غیر اینصورت نمیتواند هدر Multiboot را پیدا کند. مثال دیگر این است که [اطلاعات بوت]، که به هسته منتقل میشوند، به جای ارائه انتزاعات تمیز و واضح، شامل ساختارها با وابستگی زیاد به معماری هستند.
- هر دو استاندارد GRUB و Multiboot بصورت ناقص مستند شدهاند.
- برای ایجاد یک ایمیج دیسکِ قابل بوت از فایل هسته، GRUB باید روی سیستم میزبان نصب شود. این امر باعث دشوارتر شدنِ توسعه در ویندوز یا Mac میشود.
[اندازه صفحه پیش فرض تنظیم شده]: https://wiki.osdev.org/Multiboot#Multiboot_2
[اطلاعات بوت]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
به دلیل این اشکالات ما تصمیم گرفتیم از GRUB یا استاندارد Multiboot استفاده نکنیم. با این حال، ما قصد داریم پشتیبانی Multiboot را به ابزار [bootimage] خود اضافه کنیم، به طوری که امکان بارگذاری هسته شما بر روی یک سیستم با بوتلودر GRUB نیز وجود داشته باشد. اگر علاقهمند به نوشتن هسته سازگار با Multiboot هستید، [نسخه اول] مجموعه پستهای این وبلاگ را بررسی کنید.
[نسخه اول]: @/edition-1/_index.md
### UEFI
(ما در حال حاضر پشتیبانی UEFI را ارائه نمیدهیم، اما خیلی دوست داریم این کار را انجام دهیم! اگر میخواهید کمک کنید، لطفاً در [ایشو گیتهاب](https://github.com/phil-opp/blog_os/issues/349) به ما بگویید.)
## یک هسته مینیمال
اکنون که تقریباً میدانیم چگونه یک کامپیوتر بوت میشود، وقت آن است که هسته مینیمال خودمان را ایجاد کنیم. هدف ما ایجاد دیسک ایمیجی میباشد که “!Hello World” را هنگام بوت شدن چاپ کند. برای این منظور از [باینری مستقل Rust] که در پست قبل دیدید استفاده میکنیم.
همانطور که ممکن است به یاد داشته باشید، باینری مستقل را از طریق `cargo` ایجاد کردیم، اما با توجه به سیستم عامل، به نامهای ورودی و پرچمهای کامپایل مختلف نیاز داشتیم. به این دلیل که `cargo` به طور پیش فرض برای سیستم میزبان بیلد میکند، بطور مثال سیستمی که از آن برای نوشتن هسته استفاده میکنید. این چیزی نیست که ما برای هسته خود بخواهیم، زیرا منطقی نیست که هسته سیستم عاملمان را روی یک سیستم عامل دیگر اجرا کنیم. در عوض، ما میخواهیم هسته را برای یک _سیستم هدف_ کاملاً مشخص کامپایل کنیم.
### نصب Rust Nightly {#installing-rust-nightly}
راست دارای سه کانال انتشار است: _stable_, _beta_, and _nightly_ (ترجمه از چپ به راست: پایدار، بتا و شبانه). کتاب Rust تفاوت بین این کانالها را به خوبی توضیح میدهد، بنابراین یک دقیقه وقت بگذارید و [آن را بررسی کنید](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). برای ساخت یک سیستم عامل به برخی از ویژگیهای آزمایشی نیاز داریم که فقط در کانال شبانه موجود است، بنابراین باید نسخه شبانه Rust را نصب کنیم.
برای مدیریت نصبهای Rust من به شدت [rustup] را توصیه میکنم. به شما این امکان را میدهد که کامپایلرهای شبانه، بتا و پایدار را در کنار هم نصب کنید و بروزرسانی آنها را آسان میکند. با rustup شما میتوانید از یک کامپایلر شبانه برای دایرکتوری جاری استفاده کنید، کافیست دستور `rustup override set nightly` را اجرا کنید. همچنین میتوانید فایلی به نام `rust-toolchain` را با محتوای `nightly` در دایرکتوری ریشه پروژه اضافه کنید. با اجرای `rustc --version` میتوانید چک کنید که نسخه شبانه را دارید یا نه. شماره نسخه باید در پایان شامل `nightly-` باشد.
[rustup]: https://www.rustup.rs/
کامپایلر شبانه به ما امکان میدهد با استفاده از به اصطلاح _feature flags_ در بالای فایل، از ویژگیهای مختلف آزمایشی استفاده کنیم. به عنوان مثال، میتوانیم [`asm!` macro] آزمایشی را برای اجرای دستورات اسمبلیِ اینلاین (تلفظ: inline) با اضافه کردن `[feature(asm)]!#` به بالای فایل `main.rs` فعال کنیم. توجه داشته باشید که این ویژگیهای آزمایشی، کاملاً ناپایدار هستند، به این معنی که نسخههای آتی Rust ممکن است بدون هشدار قبلی آنها را تغییر داده یا حذف کند. به همین دلیل ما فقط در صورت لزوم از آنها استفاده خواهیم کرد.
[`asm!` macro]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
### مشخصات هدف
کارگو (کلمه: cargo) سیستمهای هدف مختلف را از طریق `target--` پشتیبانی میکند. سیستم هدف توسط یک به اصطلاح _[target triple]_ (ترجمه: هدف سه گانه) توصیف شده است، که معماری CPU، فروشنده، سیستم عامل، و [ABI] را شامل میشود. برای مثال، هدف سه گانه `x86_64-unknown-linux-gnu` یک سیستم را توصیف میکند که دارای سیپییو `x86_64`، بدون فروشنده مشخص و یک سیستم عامل لینوکس با GNU ABI است. Rust از [هدفهای سه گانه مختلفی][platform-support] پشتیبانی میکند، شامل `arm-linux-androideabi` برای اندروید یا [`wasm32-unknown-unknown` برای وباسمبلی](https://www.hellorust.com/setup/wasm-target/).
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[ABI]: https://stackoverflow.com/a/2456882
[platform-support]: https://forge.rust-lang.org/release/platform-support.html
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
برای سیستم هدف خود، به برخی از پارامترهای خاص پیکربندی نیاز داریم (به عنوان مثال، فاقد سیستم عامل زیرین)، بنابراین هیچ یک از [اهداف سه گانه موجود][platform-support] مناسب نیست. خوشبختانه Rust به ما اجازه میدهد تا [هدف خود][custom-targets] را از طریق یک فایل JSON تعریف کنیم. به عنوان مثال، یک فایل JSON که هدف `x86_64-unknown-linux-gnu` را توصیف میکند به این شکل است:
```json
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
اکثر فیلدها برای LLVM مورد نیاز هستند تا بتواند کد را برای آن پلتفرم ایجاد کند. برای مثال، فیلد [`data-layout`] اندازه انواع مختلف عدد صحیح، مُمَیزِ شناور و انواع اشارهگر را تعریف میکند. سپس فیلدهایی وجود دارد که Rust برای کامپایل شرطی از آنها استفاده میکند، مانند `target-pointer-width`. نوع سوم فیلدها نحوه ساخت crate (تلفظ: کرِیت) را تعریف میکنند. مثلا، فیلد `pre-link-args` آرگومانهای منتقل شده به [لینکر] را مشخص میکند.
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
[لینکر]: https://en.wikipedia.org/wiki/Linker_(computing)
ما همچنین سیستمهای `x86_64` را با هسته خود مورد هدف قرار میدهیم، بنابراین مشخصات هدف ما بسیار شبیه به مورد بالا خواهد بود. بیایید با ایجاد یک فایل `x86_64-blog_os.json` شروع کنیم (هر اسمی را که دوست دارید انتخاب کنید) با محتوای مشترک:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true
}
```
توجه داشته باشید که ما OS را در `llvm-target` و همچنین فیلد `os` را به `none` تغییر دادیم، زیرا ما هسته را روی یک bare metal اجرا میکنیم.
همچنین موارد زیر که مربوط به ساخت (ترجمه: build-related) هستند را اضافه میکنیم:
```json
"linker-flavor": "ld.lld",
"linker": "rust-lld",
```
به جای استفاده از لینکر پیش فرض پلتفرم (که ممکن است از اهداف لینوکس پشتیبانی نکند)، ما از لینکر کراس پلتفرم [LLD] استفاده میکنیم که برای پیوند دادن هسته ما با Rust ارائه میشود.
[LLD]: https://lld.llvm.org/
```json
"panic-strategy": "abort",
```
این تنظیم مشخص میکند که هدف از [stack unwinding] درهنگام panic پشتیبانی نمیکند، بنابراین به جای آن خود برنامه باید مستقیماً متوقف شود. این همان اثر است که آپشن `panic = "abort"` در فایل Cargo.toml دارد، پس میتوانیم آن را از فایل Cargo.toml حذف کنیم.(توجه داشته باشید که این آپشنِ هدف همچنین زمانی اعمال میشود که ما کتابخانه `هسته` را مجددا در ادامه همین پست کامپایل میکنیم. بنابراین حتماً این گزینه را اضافه کنید، حتی اگر ترجیح می دهید گزینه Cargo.toml را حفظ کنید.)
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
```json
"disable-redzone": true,
```
ما در حال نوشتن یک هسته هستیم، بنابراین بالاخره باید وقفهها را مدیریت کنیم. برای انجام ایمن آن، باید بهینهسازی اشارهگر پشتهای خاصی به نام _“red zone”_ (ترجمه: منطقه قرمز) را غیرفعال کنیم، زیرا در غیر این صورت باعث خراب شدن پشته میشود. برای اطلاعات بیشتر، به پست جداگانه ما در مورد [غیرفعال کردن منطقه قرمز] مراجعه کنید.
[غیرفعال کردن منطقه قرمز]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
```json
"features": "-mmx,-sse,+soft-float",
```
فیلد `features` ویژگیهای هدف را فعال/غیرفعال میکند. ما ویژگیهای `mmx` و `sse` را با گذاشتن یک منفی در ابتدای آنها غیرفعال کردیم و ویژگی `soft-float` را با اضافه کردن یک مثبت به ابتدای آن فعال کردیم. توجه داشته باشید که بین پرچمهای مختلف نباید فاصلهای وجود داشته باشد، در غیر این صورت LLVM قادر به تفسیر رشته ویژگیها نیست.
ویژگیهای `mmx` و `sse` پشتیبانی از دستورالعملهای [Single Instruction Multiple Data (SIMD)] را تعیین میکنند، که اغلب میتواند سرعت برنامهها را به میزان قابل توجهی افزایش دهد. با این حال، استفاده از ثباتهای بزرگ SIMD در هسته سیستم عامل منجر به مشکلات عملکردی میشود. دلیل آن این است که هسته قبل از ادامه یک برنامهی متوقف شده، باید تمام رجیسترها را به حالت اولیه خود برگرداند. این بدان معناست که هسته در هر فراخوانی سیستم یا وقفه سخت افزاری باید حالت کامل SIMD را در حافظه اصلی ذخیره کند. از آنجا که حالت SIMD بسیار بزرگ است (512-1600 بایت) و وقفهها ممکن است اغلب اتفاق بیفتند، این عملیات ذخیره و بازیابی اضافی به طور قابل ملاحظهای به عملکرد آسیب میرساند. برای جلوگیری از این، SIMD را برای هسته خود غیرفعال میکنیم (نه برای برنامههایی که از روی آن اجرا می شوند!).
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
یک مشکل در غیرفعال کردن SIMD این است که عملیاتهای مُمَیزِ شناور (ترجمه: floating point) در `x86_64` به طور پیش فرض به ثباتهای SIMD نیاز دارد. برای حل این مشکل، ویژگی `soft-float` را اضافه میکنیم، که از طریق عملکردهای نرمافزاری مبتنی بر اعداد صحیح عادی، تمام عملیات مُمَیزِ شناور را شبیهسازی میکند.
For more information, see our post on [disabling SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
```json
"rustc-abi": "x86-softfloat"
```
As we want to use the `soft-float` feature, we also need to tell the Rust compiler `rustc` that we want to use the corresponding ABI. We can do that by setting the `rustc-abi` field to `x86-softfloat`.
#### کنار هم قرار دادن
فایل مشخصات هدف ما اکنون به این شکل است:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float",
"rustc-abi": "x86-softfloat"
}
```
### ساخت هسته
عملیات کامپایل کردن برای هدف جدید ما از قراردادهای لینوکس استفاده خواهد کرد (کاملاً مطمئن نیستم که چرا، تصور میکنم این فقط پیش فرض LLVM باشد). این بدان معنی است که ما به یک نقطه ورود به نام `start_` نیاز داریم همانطور که در [پست قبلی] توضیح داده شد:
[پست قبلی]: @/edition-2/posts/01-freestanding-rust-binary/index.md
```rust
// src/main.rs
#![no_std] // don't link the Rust standard library
#![no_main] // disable all Rust-level entry points
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[unsafe(no_mangle)] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
// this function is the entry point, since the linker looks for a function
// named `_start` by default
loop {}
}
```
توجه داشته باشید که بدون توجه به سیستم عامل میزبان، باید نقطه ورود را `start_` بنامید.
اکنون میتوانیم با نوشتن نام فایل JSON بعنوان `target--`، هسته خود را برای هدف جدید بسازیم:
```
> cargo build --target x86_64-blog_os.json
error: `.json` target specs require -Zjson-target-spec
```
شکست میخورد! این خطا به ما میگوید که مشخصات هدف JSON سفارشی یک ویژگی ناپایدار است که نیاز به فعالسازی صریح دارد. این به این دلیل است که فرمت فایلهای هدف JSON هنوز پایدار در نظر گرفته نمیشود، بنابراین ممکن است در نسخههای آینده Rust تغییر کند. برای اطلاعات بیشتر به [مسئله پیگیری مشخصات هدف JSON سفارشی][json-target-spec-issue] مراجعه کنید.
[json-target-spec-issue]: https://github.com/rust-lang/rust/issues/151528
#### آپشن `json-target-spec`
برای فعال کردن پشتیبانی از مشخصات هدف JSON سفارشی، ما نیاز داریم تا یک فایل [پیکربندی کارگو] در `cargo/config.toml.` (پوشه `cargo.` باید کنار پوشه `src` شما باشد) با محتوای زیر بسازیم:
[پیکربندی کارگو]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
```
این ویژگی ناپایدار `json-target-spec` را فعال میکند و به ما امکان استفاده از فایلهای هدف JSON سفارشی را میدهد.
حالا با این پیکربندی، بیایید دوباره بسازیم:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
```
حالا یک خطای متفاوت میبینیم! این خطا به ما میگوید که کامپایلر Rust دیگر [کتابخانه `core`] را پیدا نمیکند. این کتابخانه شامل انواع اساسی Rust مانند `Result` ، `Option` و iterators است، و به طور ضمنی به همه کریتهای `no_std` لینک است.
[کتابخانه `core`]: https://doc.rust-lang.org/nightly/core/index.html
مشکل این است که کتابخانه core همراه با کامپایلر Rust به عنوان یک کتابخانه _precompiled_ (ترجمه: از پیش کامپایل شده) توزیع میشود. بنابراین فقط برای میزبانهای سهگانه پشتیبانی شده مجاز است (مثلا، `x86_64-unknown-linux-gnu`) اما برای هدف سفارشی ما صدق نمیکند. اگر میخواهیم برای سیستمهای هدف دیگر کدی را کامپایل کنیم، ابتدا باید `core` را برای این اهداف دوباره کامپایل کنیم.
#### آپشن `build-std`
اینجاست که [ویژگی `build-std`] کارگو وارد میشود. این امکان را میدهد تا بجای استفاده از نسخههای از پیش کامپایل شده با نصب Rust، بتوانیم `core` و کریت سایر کتابخانههای استاندارد را در صورت نیاز دوباره کامپایل کنیم. این ویژگی بسیار جدید بوده و هنوز تکمیل نشده است، بنابراین بعنوان «ناپایدار» علامت گذاری شده و فقط در [نسخه شبانه کامپایلر Rust] در دسترس میباشد.
[ویژگی `build-std`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[نسخه شبانه کامپایلر Rust]: #installing-rust-nightly
برای استفاده از این ویژگی، باید موارد زیر را به فایل [پیکربندی کارگو] در `cargo/config.toml.` اضافه کنیم:
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
build-std = ["core", "compiler_builtins"]
```
این به کارگو میگوید که باید `core` و کتابخانه `compiler_builtins` را دوباره کامپایل کند. مورد دوم لازم است زیرا یک وابستگی از `core` است. به منظور کامپایل مجدد این کتابخانهها، کارگو نیاز به دسترسی به کد منبع Rust دارد که میتوانیم آن را با `rustup component add rust-src` نصب کنیم.
**یادداشت:** کلید پیکربندی `unstable.build-std` به نسخهای جدیدتر از نسخه 2020-07-15 شبانه Rust نیاز دارد.
پس از تنظیم کلید پیکربندی `unstable.build-std` و نصب مولفه `rust-src`، میتوانیم مجددا دستور بیلد (کلمه: build) را اجرا کنیم.
```
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
میبینیم که `cargo build` دوباره `core` و `rustc-std-workspace-core` (یک وابستگی از `compiler_builtins`)، و کتابخانه `compiler_builtins` را برای سیستم هدف سفارشیمان کامپایل میکند.
#### موارد ذاتیِ مربوط به مموری
کامپایلر Rust فرض میکند که مجموعه خاصی از توابع داخلی برای همه سیستمها در دسترس است. اکثر این توابع توسط کریت `compiler_builtins` ارائه میشود که ما آن را به تازگی مجددا کامپایل کردیم. با این حال، برخی از توابع مربوط به حافظه در آن کریت وجود دارد که به طور پیشفرض فعال نیستند زیرا به طور معمول توسط کتابخانه C موجود در سیستم ارائه میشوند. این توابع شامل `memset` میباشد که مجموعه تمام بایتها را در یک بلوک حافظه بر روی یک مقدار مشخص قرار میدهد، `memcpy` که یک بلوک حافظه را در دیگری کپی میکند و `memcmp` که دو بلوک حافظه را با یکدیگر مقایسه میکند. اگرچه ما در حال حاضر به هیچ یک از این توابع برای کامپایل هسته خود نیازی نداریم، اما به محض افزودن کدهای بیشتر به آن، این توابع مورد نیاز خواهند بود (برای مثال، هنگام کپی کردن یک ساختمان).
از آنجا که نمیتوانیم به کتابخانه C سیستم عامل لینک دهیم، به روشی جایگزین برای ارائه این توابع به کامپایلر نیاز داریم. یک رویکرد ممکن برای این کار میتواند پیادهسازی توابع `memset` و غیره و اعمال صفت `#[unsafe(no_mangle)]` (برای جلوگیری از تغییر نام خودکار در هنگام کامپایل کردن) بر روی آنها اعمال باشد. با این حال، این خطرناک است زیرا کوچکترین اشتباهی در اجرای این توابع میتواند منجر به یک رفتار تعریف نشده شود. به عنوان مثال، ممکن است هنگام پیادهسازی `memcpy` با استفاده از حلقه `for` یک بازگشت بیپایان داشته باشید زیرا حلقههای `for` به طور ضمنی مِتُد تریتِ (کلمه: trait) [`IntoIterator::into_iter`] را فراخوانی میکنند، که ممکن است دوباره `memcpy` را فراخوانی کند. بنابراین بهتر است به جای آن از پیاده سازیهای تست شده موجود، مجدداً استفاده کنید.
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
خوشبختانه کریت `compiler_builtins` از قبل شامل پیاده سازی تمام توابع مورد نیازمان است، آنها فقط به طور پیش فرض غیرفعال هستند تا با پیاده سازی های کتابخانه C تداخلی نداشته باشند. ما میتوانیم آنها را با تنظیم پرچم [`build-std-features`] کارگو بر روی `["compiler-builtins-mem"]` فعال کنیم. مانند پرچم `build-std`، این پرچم میتواند به عنوان پرچم `Z-` در خط فرمان استفاده شود یا در جدول `unstable` در فایل `cargo/config.toml.` پیکربندی شود. از آنجا که همیشه میخواهیم با این پرچم بیلد کنیم، گزینه پیکربندی فایل منطقیتر است:
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
```
پشتیبانی برای ویژگی `compiler-builtins-mem` [به تازگی اضافه شده](https://github.com/rust-lang/rust/pull/77284)، پس حداقل به نسخه شبانه `2020-09-30` نیاز دارید.
در پشت صحنه، این پرچم [ویژگی `mem`] از کریت `compiler_builtins` را فعال میکند. اثرش این است که صفت `#[unsafe(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
[بهینه نشدهاند]: https://github.com/rust-lang/compiler-builtins/issues/339
[memcpy rep movsb]: https://github.com/rust-lang/compiler-builtins/pull/365
با این تغییر، هسته ما برای همه توابع مورد نیاز کامپایلر، پیاده سازی معتبری دارد، بنابراین حتی اگر کد ما پیچیدهتر باشد نیز باز کامپایل میشود.
#### تنظیم یک هدف پیش فرض
برای اینکه نیاز نباشد در هر فراخوانی `cargo build` پارامتر `target--` را وارد کنیم، میتوانیم هدف پیشفرض را بازنویسی کنیم. برای این کار، ما کد زیر را به [پیکربندی کارگو] در فایل `cargo/config.toml.` اضافه میکنیم:
[پیکربندی کارگو]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
این به `cargo` میگوید در صورتی که صریحاً از `target--` استفاده نکردیم، از هدف ما یعنی `x86_64-blog_os.json` استفاده کند. در واقع اکنون میتوانیم هسته خود را با یک `cargo build` ساده بسازیم. برای اطلاعات بیشتر در مورد گزینههای پیکربندی کارگو، [اسناد رسمی][پیکربندی کارگو] را بررسی کنید.
اکنون میتوانیم هسته را برای یک هدف bare metal با یک `cargo build` ساده بسازیم. با این حال، نقطه ورود `start_` ما، که توسط بوت لودر فراخوانی میشود، هنوز خالی است. وقت آن است که از طریق آن، چیزی را در خروجی نمایش دهیم.
### چاپ روی صفحه
سادهترین راه برای چاپ متن در صفحه در این مرحله [بافر متن VGA] است. این یک منطقه خاص حافظه است که به سخت افزار VGA نگاشت (مَپ) شده و حاوی مطالب نمایش داده شده روی صفحه است. به طور معمول از 25 خط تشکیل شده است که هر کدام شامل 80 سلول کاراکتر هستند. هر سلول کاراکتر یک کاراکتر ASCII را با برخی از رنگهای پیش زمینه و پس زمینه نشان میدهد. خروجی صفحه به این شکل است:
[بافر متن VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode

ما در پست بعدی، جایی که اولین درایور کوچک را برای آن مینویسیم، در مورد قالب دقیق بافر متن VGA بحث خواهیم کرد. برای چاپ “!Hello World”، فقط باید بدانیم که بافر در آدرس `0xb8000` قرار دارد و هر سلول کاراکتر از یک بایت ASCII و یک بایت رنگ تشکیل شده است.
پیادهسازی مشابه این است:
```rust
static HELLO: &[u8] = b"Hello World!";
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
ابتدا عدد صحیح `0xb8000` را در یک اشارهگر خام (ترجمه: raw pointer) میریزیم. سپس روی بایتهای [رشته بایت][byte string] [استاتیک][static] `HELLO` [پیمایش][iterate] میکنیم. ما از متد [`enumerate`] برای اضافه کردن متغیر درحال اجرای `i` استفاده میکنیم. در بدنه حلقه for، از متد [`offset`] برای نوشتن بایت رشته و بایت رنگ مربوطه استفاده میکنیم (`0xb` فیروزهای روشن است).
[iterate]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html
[static]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
[اشارهگر خام]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
توجه داشته باشید که یک بلوک [`unsafe`] همیشه هنگام نوشتن در حافظه مورد استفاده قرار میگیرد. دلیل این امر این است که کامپایلر Rust نمیتواند معتبر بودن اشارهگرهای خام که ایجاد میکنیم را ثابت کند. آنها میتوانند به هر کجا اشاره کنند و منجر به خراب شدن دادهها شوند. با قرار دادن آنها در یک بلوک `unsafe`، ما در اصل به کامپایلر میگوییم که کاملاً از معتبر بودن عملیات اطمینان داریم. توجه داشته باشید که یک بلوک `unsafe`، بررسیهای ایمنی Rust را خاموش نمیکند. فقط به شما این امکان را میدهد که [پنج کار اضافی] انجام دهید.
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
[پنج کار اضافی]: https://doc.rust-lang.org/stable/book/ch20-01-unsafe-rust.html#unsafe-superpowers
می خواهم تأکید کنم که **این روشی نیست که ما بخواهیم در Rust کارها را از طریق آن پبش ببریم!** به هم ریختگی هنگام کار با اشارهگرهای خام در داخل بلوکهای ناامن بسیار محتمل و ساده است، به عنوان مثال، اگر مواظب نباشیم به راحتی میتوانیم فراتر از انتهای بافر بنویسیم.
بنابراین ما میخواهیم تا آنجا که ممکن است استفاده از `unsafe` را به حداقل برسانیم. Rust با ایجاد انتزاعهای ایمن به ما توانایی انجام این کار را میدهد. به عنوان مثال، ما میتوانیم یک نوع بافر VGA ایجاد کنیم که تمام کدهای ناامن را در خود قرار داده و اطمینان حاصل کند که انجام هرگونه اشتباه از خارج از این انتزاع _غیرممکن_ است. به این ترتیب، ما فقط به حداقل مقادیر ناامن نیاز خواهیم داشت و میتوان اطمینان داشت که [ایمنی حافظه] را نقض نمیکنیم. در پست بعدی چنین انتزاع ایمن بافر VGA را ایجاد خواهیم کرد.
[ایمنی حافظه]: https://en.wikipedia.org/wiki/Memory_safety
## اجرای هسته
حال یک هسته اجرایی داریم که کار محسوسی را انجام میدهد، پس زمان اجرای آن فرا رسیده است. ابتدا، باید هسته کامپایل شده خود را با پیوند دادن آن به یک بوتلودر، به یک دیسک ایمیج قابل بوت تبدیل کنیم. سپس میتوانیم دیسک ایمیج را در ماشین مجازی [QEMU] اجرا یا با استفاده از یک درایو USB آن را بر روی سخت افزار واقعی بوت کنیم.
### ساخت دیسک ایمیج
برای تبدیل هسته کامپایل شده به یک دیسک ایمیج قابل بوت، باید آن را با یک بوت لودر پیوند دهیم. همانطور که در [بخش مربوط به بوت شدن (لینک باید اپدیت شود)] آموختیم، بوت لودر مسئول مقداردهی اولیه پردازنده و بارگیری هسته میباشد.
[بخش مربوط به بوت شدن]: #the-boot-process
به جای نوشتن یک بوت لودر مخصوص خودمان، که به تنهایی یک پروژه است، از کریت [`bootloader`] استفاده میکنیم. این کریت بوتلودر اصلی BIOS را بدون هیچگونه وابستگی به C، فقط با استفاده از Rust و اینلاین اسمبلی پیاده سازی میکند. برای استفاده از آن برای راه اندازی هسته، باید وابستگی به آن را ضافه کنیم:
[`bootloader`]: https://crates.io/crates/bootloader
```toml
# in Cargo.toml
[dependencies]
bootloader = "0.9"
```
افزودن بوتلودر به عنوان وابستگی برای ایجاد یک دیسک ایمیج قابل بوت کافی نیست. مشکل این است که ما باید هسته خود را با بوت لودر پیوند دهیم، اما کارگو از [اسکریپت های بعد از بیلد] پشتیبانی نمیکند.
[اسکریپت های بعد از بیلد]: https://github.com/rust-lang/cargo/issues/545
برای حل این مشکل، ما ابزاری به نام `bootimage` ایجاد کردیم که ابتدا هسته و بوت لودر را کامپایل میکند و سپس آنها را به یکدیگر پیوند میدهد تا یک ایمیج دیسک قابل بوت ایجاد کند. برای نصب ابزار، دستور زیر را در ترمینال خود اجرا کنید:
```
cargo install bootimage
```
برای اجرای `bootimage` و ساختن بوتلودر، شما باید `llvm-tools-preview` که یک مولفه rustup میباشد را نصب داشته باشید. شما میتوانید این کار را با اجرای دستور `rustup component add llvm-tools-preview` انجام دهید.
پس از نصب `bootimage` و اضافه کردن مولفه `llvm-tools-preview`، ما میتوانیم یک دیسک ایمیج قابل بوت را با اجرای این دستور ایجاد کنیم:
```
> cargo bootimage
```
میبینیم که این ابزار، هسته ما را با استفاده از `cargo build` دوباره کامپایل میکند، بنابراین به طور خودکار هر تغییری که ایجاد میکنید را دربر میگیرد. پس از آن بوتلودر را کامپایل میکند که ممکن است مدتی طول بکشد. مانند تمام کریتهای وابسته ، فقط یک بار بیلد میشود و سپس کش (کلمه: cache) میشود، بنابراین بیلدهای بعدی بسیار سریعتر خواهد بود. سرانجام، `bootimage`، بوتلودر و هسته شما را با یک دیسک ایمیج قابل بوت ترکیب میکند.
پس از اجرای این دستور، شما باید یک دیسک ایمیج قابل بوت به نام `bootimage-blog_os.bin` در مسیر `target/x86_64-blog_os/debug` ببینید. شما میتوانید آن را در یک ماشین مجازی بوت کنید یا آن را در یک درایو USB کپی کرده و روی یک سخت افزار واقعی بوت کنید. (توجه داشته باشید که این یک ایمیج CD نیست، بنابراین رایت کردن آن روی CD بیفایده است چرا که ایمیج CD دارای قالب متفاوتی است).
#### چگونه کار می کند؟
ابزار `bootimage` مراحل زیر را در پشت صحنه انجام می دهد:
- کرنل ما را به یک فایل [ELF] کامپایل میکند.
- وابستگی بوتلودر را به عنوان یک اجرایی مستقل (ترجمه: standalone executable) کامپایل میکند.
- بایتهای فایل ELF هسته را به بوتلودر پیوند میدهد.
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
وقتی بوت شد، بوتلودر فایل ضمیمه شده ELF را خوانده و تجزیه میکند. سپس بخشهای (ترجمه: segments) برنامه را به آدرسهای مجازی در جداول صفحه نگاشت (مپ) میکند، بخش `bss.` را صفر کرده و یک پشته را تنظیم میکند. در آخر، آدرس نقطه ورود (تابع `start_`) را خوانده و به آن پرش میکند.
### بوت کردن در QEMU
اکنون میتوانیم دیسک ایمیج را در یک ماشین مجازی بوت کنیم. برای راه اندازی آن در [QEMU]، دستور زیر را اجرا کنید:
[QEMU]: https://www.qemu.org/
```
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
```
این یک پنجره جداگانه با این شکل باز میکند:

میبینیم که “!Hello World” بر روی صفحه قابل مشاهده است.
### ماشین واقعی
همچنین میتوانید آن را بر روی یک درایو USB رایت و بر روی یک دستگاه واقعی بوت کنید:
```
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
```
در اینجا `sdX` نام دستگاه USB شماست. **مراقب باشید** که نام دستگاه را به درستی انتخاب کنید، زیرا همه دادههای موجود در آن دستگاه بازنویسی میشوند.
پس از رایت کردن ایمیج در USB، میتوانید با بوت کردن، آن را بر روی سخت افزار واقعی اجرا کنید. برای راه اندازی از طریق USB احتمالاً باید از یک منوی بوت ویژه استفاده کنید یا ترتیب بوت را در پیکربندی BIOS تغییر دهید. توجه داشته باشید که این در حال حاضر برای دستگاههای UEFI کار نمیکند، زیرا کریت `bootloader` هنوز پشتیبانی UEFI را ندارد.
### استفاده از `cargo run`
برای سهولت اجرای هسته در QEMU، میتوانیم کلید پیکربندی `runner` را برای کارگو تنظیم کنیم:
```toml
# in .cargo/config.toml
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
```
جدول `'target.'cfg(target_os = "none")` برای همه اهدافی که فیلد `"os"` فایل پیکربندی هدف خود را روی `"none"` تنظیم کردهاند، اعمال میشود. این شامل هدف `x86_64-blog_os.json` نیز میشود. `runner` دستوری را که باید برای `cargo run` فراخوانی شود مشخص میکند. دستور پس از بیلد موفقیت آمیز با مسیر فایل اجرایی که به عنوان اولین آرگومان داده شده، اجرا میشود. برای جزئیات بیشتر به [اسناد کارگو][پیکربندی کارگو] مراجعه کنید.
دستور `bootimage runner` بصورت مشخص طراحی شده تا بعنوان یک `runner` قابل اجرا مورد استفاده قرار بگیرد. فایل اجرایی داده شده را به بوتلودر پروژه پیوند داده و سپس QEMU را اجرا میکند. برای جزئیات بیشتر و گزینههای پیکربندی احتمالی، به [توضیحات `bootimage`] مراجعه کنید.
[توضیحات `bootimage`]: https://github.com/rust-osdev/bootimage
اکنون میتوانیم از `cargo run` برای کامپایل هسته خود و راه اندازی آن در QEMU استفاده کنیم.
## مرحله بعد چیست؟
در پست بعدی، ما بافر متن VGA را با جزئیات بیشتری بررسی خواهیم کرد و یک رابط امن برای آن مینویسیم. همچنین پشتیبانی از ماکرو `println` را نیز اضافه خواهیم کرد.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/index.fr.md
================================================
+++
title = "Un noyau Rust minimal"
weight = 2
path = "fr/minimal-rust-kernel"
date = 2018-02-10
[extra]
# Please update this when updating the translation
translation_based_on_commit = "c689ecf810f8e93f6b2fb3c4e1e8b89b8a0998eb"
# GitHub usernames of the people that translated this post
translators = ["TheMimiCodes", "maximevaillancourt"]
# GitHub usernames of the people that contributed to this translation
translation_contributors = ["alaincao"]
+++
Dans cet article, nous créons un noyau Rust 64-bit minimal pour l'architecture x86. Nous continuons le travail fait dans l'article précédent “[Un binaire Rust autonome][freestanding Rust binary]” pour créer une image de disque amorçable qui affiche quelque chose à l'écran.
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.fr.md
Cet article est développé de manière ouverte sur [GitHub]. Si vous avez des problèmes ou des questions, veuillez ouvrir une _Issue_ sur GitHub. Vous pouvez aussi laisser un commentaire [au bas de la page]. Le code source complet pour cet article se trouve dans la branche [`post-02`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[au bas de la page]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
## Le processus d'amorçage
Quand vous allumez un ordinateur, il commence par exécuter le code du micrologiciel qui est enregistré dans la carte mère ([ROM]). Ce code performe un [test d'auto-diagnostic de démarrage][power-on self-test], détecte la mémoire volatile disponible, et pré-initialise le processeur et le matériel. Par la suite, il recherche un disque amorçable et commence le processus d'amorçage du noyau du système d'exploitation.
[ROM]: https://fr.wikipedia.org/wiki/M%C3%A9moire_morte
[power-on self-test]: https://fr.wikipedia.org/wiki/Power-on_self-test_(informatique)
Sur x86, il existe deux standards pour les micrologiciels : le “Basic Input/Output System“ (**[BIOS]**) et le nouvel “Unified Extensible Firmware Interface” (**[UEFI]**). Le BIOS standard est vieux et dépassé, mais il est simple et bien supporté sur toutes les machines x86 depuis les années 1980. Au contraire, l'UEFI est moderne et offre davantage de fonctionnalités. Cependant, il est plus complexe à installer (du moins, selon moi).
[BIOS]: https://fr.wikipedia.org/wiki/BIOS_(informatique)
[UEFI]: https://fr.wikipedia.org/wiki/UEFI
Actuellement, nous offrons seulement un support BIOS, mais nous planifions aussi du support pour l'UEFI. Si vous aimeriez nous aider avec cela, consultez l'[_issue_ sur GitHub](https://github.com/phil-opp/blog_os/issues/349).
### Amorçage BIOS
Presque tous les systèmes x86 peuvent amorcer le BIOS, y compris les nouvelles machines UEFI qui utilisent un BIOS émulé. C'est une bonne chose car cela permet d'utiliser la même logique d'amorçage sur toutes les machines du dernier siècle. Cependant, cette grande compatibilité est aussi le plus grand inconvénient de l'amorçage BIOS, car cela signifie que le CPU est mis dans un mode de compatibilité 16-bit appelé _[real mode]_ avant l'amorçage afin que les bootloaders archaïques des années 1980 puissent encore fonctionner.
Mais commençons par le commencement :
Quand vous allumez votre ordinateur, il charge le BIOS provenant d'un emplacement de mémoire flash spéciale localisée sur la carte mère. Le BIOS exécute des tests d'auto-diagnostic et des routines d'initialisation du matériel, puis il cherche des disques amorçables. S'il en trouve un, le contrôle est transféré à son _bootloader_, qui est une portion de 512 octets de code exécutable enregistré au début du disque. Vu que la plupart des bootloaders dépassent 512 octets, ils sont généralement divisés en deux phases: la première, plus petite, tient dans ces 512 octets, tandis que la seconde phase est chargée subséquemment.
Le bootloader doit déterminer l'emplacement de l'image de noyau sur le disque et la charger en mémoire. Il doit aussi passer le processeur de 16-bit ([real mode]) à 32-bit ([protected mode]), puis à 64-bit ([long mode]), dans lequel les registres 64-bit et la totalité de la mémoire principale sont disponibles. Sa troisième responsabilité est de récupérer certaines informations (telle que les associations mémoires) du BIOS et de les transférer au noyau du système d'exploitation.
[real mode]: https://fr.wikipedia.org/wiki/Mode_r%C3%A9el
[protected mode]: https://fr.wikipedia.org/wiki/Mode_prot%C3%A9g%C3%A9
[long mode]: https://en.wikipedia.org/wiki/Long_mode
[memory segmentation]: https://fr.wikipedia.org/wiki/Segmentation_(informatique)
Implémenter un bootloader est fastidieux car cela requiert l'écriture en language assembleur ainsi que plusieurs autres étapes particulières comme “écrire une valeur magique dans un registre du processeur". Par conséquent, nous ne couvrons pas la création d'un bootloader dans cet article et fournissons plutôt un outil appelé [bootimage] qui ajoute automatiquement un bootloader au noyau.
[bootimage]: https://github.com/rust-osdev/bootimage
Si vous êtes intéressé par la création de votre propre booloader : restez dans le coin, plusieurs articles sur ce sujet sont déjà prévus à ce sujet!
#### Le standard Multiboot
Pour éviter que chaque système d'exploitation implémente son propre bootloader, qui est seulement compatible avec un seul système d'exploitation, la [Free Software Foundation] a créé en 1995 un bootloader standard public appelé [Multiboot]. Le standard définit une interface entre le bootloader et le système d'exploitation afin que n'importe quel bootloader compatible Multiboot puisse charger n'importe quel système d'exploitation compatible Multiboot. L'implémentation de référence est [GNU GRUB], qui est le bootloader le plus populaire pour les systèmes Linux.
[Free Software Foundation]: https://fr.wikipedia.org/wiki/Free_Software_Foundation
[Multiboot]: https://wiki.osdev.org/Multiboot
[GNU GRUB]: https://fr.wikipedia.org/wiki/GNU_GRUB
Pour créer un noyau compatible Multiboot, il suffit d'insérer une [en-tête Multiboot][Multiboot header] au début du fichier du noyau. Cela rend très simple l'amorçage d'un système d'exploitation depuis GRUB. Cependant, GRUB et le standard Multiboot présentent aussi quelques problèmes :
[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
- Ils supportent seulement le "protected mode" 32-bit. Cela signifie que vous devez encore effectuer la configuration du processeur pour passer au "long mode" 64-bit.
- Ils sont conçus pour simplifier le bootloader plutôt que le noyau. Par exemple, le noyau doit être lié avec une [taille de page prédéfinie][adjusted default page size], étant donné que GRUB ne peut pas trouver les entêtes Multiboot autrement. Un autre exemple est que l'[information de boot][boot information], qui est fournies au noyau, contient plusieurs structures spécifiques à l'architecture au lieu de fournir des abstractions pures.
- GRUB et le standard Multiboot sont peu documentés.
- GRUB doit être installé sur un système hôte pour créer une image de disque amorçable depuis le fichier du noyau. Cela rend le développement sur Windows ou sur Mac plus difficile.
[adjusted default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2
[boot information]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
En raison de ces désavantages, nous avons décidé de ne pas utiliser GRUB ou le standard Multiboot. Cependant, nous avons l'intention d'ajouter le support Multiboot à notre outil [bootimage], afin qu'il soit aussi possible de charger le noyau sur un système GRUB. Si vous êtes interessé par l'écriture d'un noyau Multiboot conforme, consultez la [première édition][first edition] de cette série d'articles.
[first edition]: @/edition-1/_index.md
### UEFI
(Nous ne fournissons pas le support UEFI à l'heure actuelle, mais nous aimerions bien! Si vous voulez aider, dites-le nous dans cette [_issue_ GitHub](https://github.com/phil-opp/blog_os/issues/349).)
## Un noyau minimal
Maintenant que nous savons à peu près comment un ordinateur démarre, il est temps de créer notre propre noyau minimal. Notre objectif est de créer une image de disque qui affiche “Hello World!” à l'écran lorsqu'il démarre. Nous ferons ceci en améliorant le [binaire Rust autonome][freestanding Rust binary] du dernier article.
Comme vous vous en rappelez peut-être, nous avons créé un binaire autonome grâce à `cargo`, mais selon le système d'exploitation, nous avions besoin de différents points d'entrée et d'options de compilation. C'est dû au fait que `cargo` construit pour _système hôte_ par défaut, c'est-à-dire le système que vous utilisez. Ce n'est pas ce que nous voulons pour notre noyau, car un noyau qui s'exécute, par exemple, sur Windows n'a pas de sens. Nous voulons plutôt compiler pour un _système cible_ bien défini.
### Installer une version nocturne de Rust
Rust a trois canaux de distribution : _stable_, _beta_, et _nightly_. Le Livre de Rust explique bien les différences entre ces canaux, alors prenez une minute et [jetez y un coup d'oeil](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). Pour construire un système d'exploitation, nous aurons besoin de fonctionalités expérimentales qui sont disponibles uniquement sur le canal de distribution nocturne. Donc nous devons installer une version nocturne de Rust.
Pour gérer l'installation de Rust, je recommande fortement [rustup]. Il vous permet d'installer les versions nocturne, beta et stable du compilateur côte-à-côte et facilite leurs mises à jour. Avec rustup, vous pouvez utiliser un canal de distribution nocturne pour le dossier actuel en exécutant `rustup override set nightly`. Par ailleurs, vous pouvez ajouter un fichier appelé `rust-toolchain` avec le contenu `nightly` au dossier racine du projet. Vous pouvez vérifier que vous avez une version nocturne installée en exécutant `rustc --version`: Le numéro de la version devrait comprendre `-nightly` à la fin.
[rustup]: https://www.rustup.rs/
La version nocturne du compilateur nous permet d'activer certaines fonctionnalités expérimentales en utilisant certains _drapeaux de fonctionalité_ dans le haut de notre fichier. Par exemple, nous pourrions activer [macro expérimentale `asm!`][`asm!` macro] pour écrire du code assembleur intégré en ajoutant `#![feature(asm)]` au haut de notre `main.rs`. Notez que ces fonctionnalités expérimentales sont tout à fait instables, ce qui veut dire que des versions futures de Rust pourraient les changer ou les retirer sans préavis. Pour cette raison, nous les utiliserons seulement lorsque strictement nécessaire.
[`asm!` macro]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
### Spécification de la cible
Cargo supporte différent systèmes cibles avec le paramètre `--target`. La cible est définie par un soi-disant _[triplet de cible][target triple]_, qui décrit l'architecteur du processeur, le fabricant, le système d'exploitation, et l'interface binaire d'application ([ABI]). Par exemple, le triplet `x86_64-unknown-linux-gnu` décrit un système avec un processeur `x86_64`, sans fabricant défini, et un système d'exploitation Linux avec l'interface binaire d'application GNU. Rust supporte [plusieurs différents triplets de cible][platform-support], incluant `arm-linux-androideabi` pour Android ou [`wasm32-unknown-unknown` pour WebAssembly](https://www.hellorust.com/setup/wasm-target/).
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[ABI]: https://stackoverflow.com/a/2456882
[platform-support]: https://forge.rust-lang.org/release/platform-support.html
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
Pour notre système cible toutefois, nous avons besoin de paramètres de configuration spéciaux (par exemple, pas de système d'explotation sous-jacent), donc aucun des [triplets de cible existants][platform-support] ne convient. Heureusement, Rust nous permet de définir [notre propre cible][custom-targets] par l'entremise d'un fichier JSON. Par exemple, un fichier JSON qui décrit une cible `x86_64-unknown-linux-gnu` ressemble à ceci:
```json
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
La plupart des champs sont requis par LLVM pour générer le code pour cette plateforme. Par exemple, le champ [`data-layout`] définit la taille de divers types d'entiers, de nombres à virgule flottante, et de pointeurs. Puis, il y a des champs que Rust utilise pour de la compilation conditionelle, comme `target-pointer-width`. Le troisième type de champ définit comment la crate doit être construite. Par exemple, le champ `pre-link-args` spécifie les arguments fournis au [lieur][linker].
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
[linker]: https://en.wikipedia.org/wiki/Linker_(computing)
Nous pouvons aussi cibler les systèmes `x86_64` avec notre noyau, donc notre spécification de cible ressemblera beaucoup à celle plus haut. Commençons par créer un fichier `x86_64-blog_os.json` (utilisez le nom de votre choix) avec ce contenu commun:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true
}
```
Notez que nous avons changé le système d'exploitation dans le champs `llvm-target` et `os` en `none`, puisque nous ferons l'exécution sur du "bare metal" (donc, sans système d'exploitation sous-jacent).
Nous ajoutons ensuite les champs suivants reliés à la construction:
```json
"linker-flavor": "ld.lld",
"linker": "rust-lld",
```
Plutôt que d'utiliser le lieur par défaut de la plateforme (qui pourrait ne pas supporter les cibles Linux), nous utilisons le lieur multi-plateforme [LLD] qui est inclut avec Rust pour lier notre noyau.
[LLD]: https://lld.llvm.org/
```json
"panic-strategy": "abort",
```
Ce paramètre spécifie que la cible ne permet pas le [déroulement de la pile][stack unwinding] lorsque le noyau panique, alors le système devrait plutôt s'arrêter directement. Ceci mène au même résultat que l'option `panic = "abort"` dans notre Cargo.toml, alors nous pouvons la retirer de ce fichier. (Notez que, contrairement à l'option Cargo.toml, cette option de cible s'applique aussi quand nous recompilerons la bibliothèque `core` plus loin dans cet article. Ainsi, même si vous préférez garder l'option Cargo.toml, gardez cette option.)
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
```json
"disable-redzone": true,
```
Nous écrivons un noyau, donc nous devrons éventuellement gérer les interruptions. Pour ce faire en toute sécurité, nous devons désactiver une optimisation de pointeur de pile nommée la _“zone rouge"_, puisqu'elle causerait une corruption de la pile autrement. Pour plus d'informations, lire notre article séparé à propos de la [désactivation de la zone rouge][disabling the red zone].
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
```json
"features": "-mmx,-sse,+soft-float",
```
Le champ `features` active/désactive des fonctionalités de la cible. Nous désactivons les fonctionalités `mmx` et `sse` en les précédant d'un signe "moins" et activons la fonctionnalité `soft-float` en la précédant d'un signe "plus". Notez qu'il ne doit pas y avoir d'espace entre les différentes fonctionnalités, sinon LLVM n'arrive pas à analyser la chaîne de caractères des fonctionnalités.
Les fonctionnalités `mmx` et `sse` déterminent le support les instructions [Single Instruction Multiple Data (SIMD)], qui peuvent souvent significativement accélérer les programmes. Toutefois, utiliser les grands registres SIMD dans les noyaux des systèmes d'exploitation mène à des problèmes de performance. Ceci parce que le noyau a besoin de restaurer tous les registres à leur état original avant de continuer un programme interrompu. Cela signifie que le noyau doit enregistrer l'état SIMD complet dans la mémoire principale à chaque appel système ou interruption matérielle. Puisque l'état SIMD est très grand (512–1600 octets) et que les interruptions peuvent survenir très fréquemment, ces opérations d'enregistrement/restauration additionnelles nuisent considérablement à la performance. Pour prévenir cela, nous désactivons SIMD pour notre noyau (pas pour les applications qui s'exécutent dessus!).
[Single Instruction Multiple Data (SIMD)]: https://fr.wikipedia.org/wiki/Single_instruction_multiple_data
Un problème avec la désactivation de SIMD est que les opérations sur les nombres à virgule flottante sur `x86_64` nécessitent les registres SIMD par défaut. Pour résoudre ce problème, nous ajoutons la fonctionnalité `soft-float`, qui émule toutes les opérations à virgule flottante avec des fonctions logicielles utilisant des entiers normaux.
Pour plus d'informations, voir notre article sur la [désactivation de SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
```json
"rustc-abi": "x86-softfloat"
```
As we want to use the `soft-float` feature, we also need to tell the Rust compiler `rustc` that we want to use the corresponding ABI. We can do that by setting the `rustc-abi` field to `x86-softfloat`.
#### Assembler le tout
Notre fichier de spécification de cible ressemble maintenant à ceci :
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float",
"rustc-abi": "x86-softfloat"
}
```
### Construction de notre noyau
Compiler pour notre nouvelle cible utilisera les conventions Linux (je ne suis pas trop certain pourquoi; j'assume que c'est simplement le comportement par défaut de LLVM). Cela signifie que nos avons besoin d'un point d'entrée nommé `_start` comme décrit dans [l'article précédent][previous post]:
[previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.fr.md
```rust
// src/main.rs
#![no_std] // ne pas lier la bibliothèque standard Rust
#![no_main] // désactiver tous les points d'entrée Rust
use core::panic::PanicInfo;
/// Cette fonction est invoquée lorsque le système panique
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[unsafe(no_mangle)] // ne pas massacrer le nom de cette fonction
pub extern "C" fn _start() -> ! {
// cette fonction est le point d'entrée, puisque le lieur cherche une fonction
// nommée `_start` par défaut
loop {}
}
```
Notez que le point d'entrée doit être appelé `_start` indépendamment du système d'exploitation hôte.
Nous pouvons maintenant construire le noyau pour notre nouvelle cible en fournissant le nom du fichier JSON comme `--target`:
```
> cargo build --target x86_64-blog_os.json
error: `.json` target specs require -Zjson-target-spec
```
Cela échoue! L'erreur nous dit que les spécifications de cibles JSON personnalisées sont une fonctionnalité instable qui nécessite une activation explicite. Cela s'explique par le fait que le format des fichiers JSON de cible n'est pas encore considéré comme stable, donc des modifications pourraient avoir lieu dans les futures versions de Rust. Consultez l'[issue de suivi pour les spécifications de cibles JSON personnalisées][json-target-spec-issue] pour plus d'informations.
[json-target-spec-issue]: https://github.com/rust-lang/rust/issues/151528
#### L'option `json-target-spec`
Pour activer le support des spécifications de cibles JSON personnalisées, nous devons créer un fichier de [configuration cargo][cargo configuration] dans `.cargo/config.toml` (le dossier `.cargo` doit être à côté de votre dossier `src`) avec le contenu suivant:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# dans .cargo/config.toml
[unstable]
json-target-spec = true
```
Ceci active la fonctionnalité instable `json-target-spec`, nous permettant d'utiliser des fichiers JSON de cible personnalisés.
Avec cette configuration en place, essayons de construire à nouveau:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
```
Maintenant nous voyons une erreur différente! L'erreur nous dit que le compilateur ne trouve plus la [bibliothèque `core`][`core` library]. Cette bibliothèque contient les types de base Rust comme `Result`, `Option`, les itérateurs, et est implicitement liée à toutes les crates `no_std`.
[`core` library]: https://doc.rust-lang.org/nightly/core/index.html
Le problème est que la bibliothèque `core` est distribuée avec le compilateur Rust comme biliothèque _precompilée_. Donc, elle est seulement valide pour les triplets d'hôtes supportés (par exemple, `x86_64-unknown-linux-gnu`) mais pas pour notre cible personnalisée. Si nous voulons compiler du code pour d'autres cibles, nous devons d'abord recompiler `core` pour ces cibles.
#### L'option `build-std`
C'est ici que la [fonctionnalité `build-std`][`build-std` feature] de cargo entre en jeu. Elle permet de recompiler `core` et d'autres crates de la bibliothèque standard sur demande, plutôt que d'utiliser des versions précompilées incluses avec l'installation de Rust. Cette fonctionnalité est très récente et n'est pas encore complète, donc elle est définie comme instable et est seulement disponible avec les [versions nocturnes du compilateur Rust][nightly Rust compilers].
[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[nightly Rust compilers]: #installer-une-version-nocturne-de-rust
Pour utiliser cette fonctionnalité, nous devons ajouter ce qui suit à notre fichier de [configuration cargo][cargo configuration] dans `.cargo/config.toml`:
```toml
# dans .cargo/config.toml
[unstable]
json-target-spec = true
build-std = ["core", "compiler_builtins"]
```
Ceci indique à cargo qu'il doit recompiler les bibliothèques `core` et `compiler_builtins`. Celle-ci est nécessaire pour qu'elle ait une dépendance de `core`. Afin de recompiler ces bibliothèques, cargo doit avoir accès au code source de Rust, que nous pouvons installer avec `rustup component add rust-src`.
**Note:** La clé de configuration `unstable.build-std` nécessite une version nocturne de Rust plus récente que 2020-07-15.
Après avoir défini la clé de configuration `unstable.build-std` et installé la composante `rust-src`, nous pouvons exécuter notre commande de construction à nouveau:
```
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
Nous voyons que `cargo build` recompile maintenant les bibliothèques `core`, `rustc-std-workspace-core` (une dépendance de `compiler_builtins`), et `compiler_builtins` pour notre cible personnalisée.
#### Détails reliés à la mémoire
Le compilateur Rust assume qu'un certain ensemble de fonctions intégrées sont disponibles pour tous les systèmes. La plupart de ces fonctions sont fournies par la crate `compiler_builtins` que nous venons de recompiler. Toutefois, certaines fonctions liées à la mémoire dans cette crate ne sont pas activées par défaut puisqu'elles sont normalement fournies par la bibliothèque C sur le système. Parmi ces fonctions, on retrouve `memset`, qui définit tous les octets dans un bloc mémoire à une certaine valeur, `memcpy`, qui copie un bloc mémoire vers un autre, et `memcmp`, qui compare deux blocs mémoire. Alors que nous n'avions pas besoin de ces fonctions pour compiler notre noyau maintenant, elles seront nécessaires aussitôt que nous lui ajouterons plus de code (par exemple, lorsque nous copierons des `struct`).
Puisque nous ne pouvons pas lier avec la bibliothèque C du système d'exploitation, nous avons besoin d'une méthode alternative de fournir ces fonctions au compilateur. Une approche possible pour ce faire serait d'implémenter nos propre fonctions `memset`, etc. et de leur appliquer l'attribut `#[unsafe(no_mangle)]` (pour prévenir le changement de nom automatique pendant la compilation). Or, ceci est dangereux puisque toute erreur dans l'implémentation pourrait mener à un comportement indéterminé. Par exemple, implémenter `memcpy` avec une boucle `for` pourrait mener à une recursion infinie puisque les boucles `for` invoquent implicitement la méthode _trait_ [`IntoIterator::into_iter`], qui pourrait invoquer `memcpy` de nouveau. C'est donc une bonne idée de plutôt réutiliser des implémentations existantes et éprouvées.
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
Heureusement, la crate `compiler_builtins` contient déjà des implémentations pour toutes les fonctions nécessaires, elles sont seulement désactivées par défaut pour ne pas interférer avec les implémentations de la bibliothèque C. Nous pouvons les activer en définissant le drapeau [`build-std-features`] de cargo à `["compiler-builtins-mem"]`. Comme pour le drapeau `build-std`, ce drapeau peut être soit fourni en ligne de commande avec `-Z` ou configuré dans la table `unstable` du fichier `.cargo/config.toml`. Puisque nous voulons toujours construire avec ce drapeau, l'option du fichier de configuration fait plus de sens pour nous:
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
```toml
# dans .cargo/config.toml
[unstable]
json-target-spec = true
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
```
(Le support pour la fonctionnalité `compiler-builtins-mem` a [été ajouté assez récemment](https://github.com/rust-lang/rust/pull/77284), donc vous aurez besoin de la version nocturne `2020-09-30` de Rust ou plus récent pour l'utiliser.)
Dans les coulisses, ce drapeau active la [fonctionnalité `mem`][`mem` feature] de la crate `compiler_builtins`. Le résultat est que l'attribut `#[unsafe(no_mangle)]` est appliqué aux [implémentations `memcpy` et autres][`memcpy` etc. implementations] de la caise, ce qui les rend disponible au lieur.
[`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
[`memcpy` etc. implementations]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
Avec ce changement, notre noyau a des implémentations valides pour toutes les fonctions requises par le compilateur, donc il peut continuer à compiler même si notre code devient plus complexe.
#### Définir une cible par défaut
Pour ne pas avoir à fournir le paramètre `--target` à chaque invocation de `cargo build`, nous pouvons définir la cible par défaut. Pour ce faire, nous ajoutons le code suivant à notre fichier de [configuration Cargo][cargo configuration] dans `.cargo/config.toml`:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# dans .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
Ceci indique à `cargo` d'utiliser notre cible `x86_64-blog_os.json` quand il n'y a pas d'argument de cible `--target` explicitement fourni. Ceci veut dire que nous pouvons maintenant construire notre noyau avec un simple `cargo build`. Pour plus d'informations sur les options de configuration cargo, jetez un coup d'oeil à la [documentation officielle de cargo][cargo configuration].
Nous pouvons maintenant construire notre noyau pour une cible "bare metal" avec un simple `cargo build`. Toutefois, notre point d'entrée `_start`, qui sera appelé par le bootloader, est encore vide. Il est temps de lui faire afficher quelque chose à l'écran.
### Afficher à l'écran
La façon la plus facile d'afficher à l'écran à ce stade est grâce au tampon texte VGA. C'est un emplacement mémoire spécial associé au matériel VGA qui contient le contenu affiché à l'écran. Il consiste normalement en 25 lines qui contiennent chacune 80 cellules de caractère. Chaque cellule de caractère affiche un caractère ASCII avec des couleurs d'avant-plan et d'arrière-plan. Le résultat à l'écran ressemble à ceci:
[VGA text buffer]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode

Nous discuterons de la disposition exacte du tampon VGA dans le prochain article, où nous lui écrirons un premier petit pilote. Pour afficher “Hello World!”, nous devons seulement savoir que le tampon est situé à l'adresse `0xb8000` et que chaque cellule de caractère consiste en un octet ASCII et un octet de couleur.
L'implémentation ressemble à ceci :
```rust
static HELLO: &[u8] = b"Hello World!";
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
D'abord, nous transformons l'entier `0xb8000` en un [pointeur brut][raw pointer]. Puis nous [parcourons][iterate] les octets de la [chaîne d'octets][byte string] [statique][static] `HELLO`. Nous utilisons la méthode [`enumerate`] pour aussi obtenir une variable `i`. Dans le corps de la boucle `for`, nous utilisons la méthode [`offset`] pour écrire la chaîne d'octets et l'octet de couleur correspondant(`0xb` est un cyan pâle).
[iterate]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html
[static]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
[raw pointer]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
Notez qu'il y a un bloc [`unsafe`] qui enveloppe les écritures mémoire. La raison en est que le compilateur Rust ne peut pas prouver que les pointeurs bruts que nous créons sont valides. Ils pourraient pointer n'importe où et mener à une corruption de données. En les mettant dans un bloc `unsafe`, nous disons fondamentalement au compilateur que nous sommes absolument certains que les opérations sont valides. Notez qu'un bloc `unsafe` ne désactive pas les contrôles de sécurité de Rust. Il permet seulement de faire [cinq choses supplémentaires][five additional things].
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
[five additional things]: https://doc.rust-lang.org/stable/book/ch20-01-unsafe-rust.html#unsafe-superpowers
Je veux souligner que **ce n'est pas comme cela que les choses se font en Rust!** Il est très facile de faire des erreurs en travaillant avec des pointeurs bruts à l'intérieur de blocs `unsafe`. Par exemple, nous pourrions facilement écrire au-delà de la fin du tampon si nous ne sommes pas prudents.
Alors nous voulons minimiser l'utilisation de `unsafe` autant que possible. Rust nous offre la possibilité de le faire en créant des abstractions de sécurité. Par exemple, nous pourrions créer un type tampon VGA qui encapsule les risques et qui s'assure qu'il est impossible de faire quoi que ce soit d'incorrect à l'extérieur de ce type. Ainsi, nous aurions besoin de très peu de code `unsafe` et nous serions certains que nous ne violons pas la [sécurité de mémoire][memory safety]. Nous allons créer une telle abstraction de tampon VGA buffer dans le prochain article.
[memory safety]: https://en.wikipedia.org/wiki/Memory_safety
## Exécuter notre noyau
Maintenant que nous avons un exécutable qui fait quelque chose de perceptible, il est temps de l'exécuter. D'abord, nous devons transformer notre noyau compilé en une image de disque amorçable en le liant à un bootloader. Ensuite, nous pourrons exécuter l'image de disque dans une machine virtuelle [QEMU] ou l'amorcer sur du véritable matériel en utilisant une clé USB.
### Créer une image d'amorçage
Pour transformer notre noyau compilé en image de disque amorçable, nous devons le lier avec un bootloader. Comme nous l'avons appris dans la [section à propos du lancement][section about booting], le bootloader est responsable de l'initialisation du processeur et du chargement de notre noyau.
[section about booting]: #le-processus-d-amorcage
Plutôt que d'écrire notre propre bootloader, ce qui est un projet en soi, nous utilisons la crate [`bootloader`]. Cette crate propose un bootloader BIOS de base sans dépendance C. Seulement du code Rust et de l'assembleur intégré. Pour l'utiliser afin de lancer notre noyau, nous devons ajouter une dépendance à cette crate:
[`bootloader`]: https://crates.io/crates/bootloader
```toml
# dans Cargo.toml
[dependencies]
bootloader = "0.9.8"
```
Ajouter le bootloader comme dépendance n'est pas suffisant pour réellement créer une image de disque amorçable. Le problème est que nous devons lier notre noyau avec le bootloader après la compilation, mais cargo ne supporte pas les [scripts post-build][post-build scripts].
[post-build scripts]: https://github.com/rust-lang/cargo/issues/545
Pour résoudre ce problème, nous avons créé un outil nommé `bootimage` qui compile d'abord le noyau et le bootloader, et les lie ensuite ensemble pour créer une image de disque amorçable. Pour installer cet outil, exécutez la commande suivante dans votre terminal:
```
cargo install bootimage
```
Pour exécuter `bootimage` et construire le bootloader, vous devez avoir la composante rustup `llvm-tools-preview` installée. Vous pouvez l'installer en exécutant `rustup component add llvm-tools-preview`.
Après avoir installé `bootimage` et ajouté la composante `llvm-tools-preview`, nous pouvons créer une image de disque amorçable en exécutant:
```
> cargo bootimage
```
Nous voyons que l'outil recompile notre noyau en utilisant `cargo build`, donc il utilisera automatiquement tout changements que vous faites. Ensuite, il compile le bootloader, ce qui peut prendre un certain temps. Comme toutes les dépendances de crates, il est seulement construit une fois puis il est mis en cache, donc les builds subséquentes seront beaucoup plus rapides. Enfin, `bootimage` combine le bootloader et le noyau en une image de disque amorçable.
Après avoir exécuté la commande, vous devriez voir une image de disque amorçable nommée `bootimage-blog_os.bin` dans votre dossier `target/x86_64-blog_os/debug`. Vous pouvez la lancer dans une machine virtuelle ou la copier sur une clé USB pour la lancer sur du véritable matériel. (Notez que ceci n'est pas une image CD, qui est un format différent, donc la graver sur un CD ne fonctionne pas).
#### Comment cela fonctionne-t-il?
L'outil `bootimage` effectue les étapes suivantes en arrière-plan:
- Il compile notre noyau en un fichier [ELF].
- Il compile notre dépendance bootloader en exécutable autonome.
- Il lie les octets du fichier ELF noyau au bootloader.
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
Lorsque lancé, le bootloader lit et analyse le fichier ELF ajouté. Il associe ensuite les segments du programme aux adresses virtuelles dans les tables de pages, réinitialise la section `.bss`, puis met en place une pile. Finalement, il lit le point d'entrée (notre fonction `_start`) et s'y rend.
### Amorçage dans QEMU
Nous pouvons maintenant lancer l'image disque dans une machine virtuelle. Pour la démarrer dans [QEMU], exécutez la commande suivante :
[QEMU]: https://www.qemu.org/
```
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
```
Ceci ouvre une fenêtre séparée qui devrait ressembler à cela:

Nous voyoons que notre "Hello World!" est visible à l'écran.
### Véritable ordinateur
Il est aussi possible d'écrire l'image disque sur une clé USB et de le lancer sur un véritable ordinateur, **mais soyez prudent** et choisissez le bon nom de périphérique, parce que **tout sur ce périphérique sera écrasé**:
```
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
```
Où `sdX` est le nom du périphérique de votre clé USB.
Après l'écriture de l'image sur votre clé USB, vous pouvez l'exécuter sur du véritable matériel en l'amorçant à partir de la clé USB. Vous devrez probablement utiliser un menu d'amorçage spécial ou changer l'ordre d'amorçage dans votre configuration BIOS pour amorcer à partir de la clé USB. Notez que cela ne fonctionne actuellement pas avec des ordinateurs UEFI, puisque la crate `bootloader` ne supporte pas encore UEFI.
### Utilisation de `cargo run`
Pour faciliter l'exécution de notre noyau dans QEMU, nous pouvons définir la clé de configuration `runner` pour cargo:
```toml
# dans .cargo/config.toml
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
```
La table `target.'cfg(target_os = "none")'` s'applique à toutes les cibles dont le champ `"os"` dans le fichier de configuration est défini à `"none"`. Ceci inclut notre cible `x86_64-blog_os.json`. La clé `runner` key spécifie la commande qui doit être invoquée pour `cargo run`. La commande est exécutée après une build réussie avec le chemin de l'exécutable comme premier argument. Voir la [configuration cargo][cargo configuration] pour plus de détails.
La commande `bootimage runner` est spécifiquement conçue pour être utilisable comme un exécutable `runner`. Elle lie l'exécutable fourni avec le bootloader duquel dépend le projet et lance ensuite QEMU. Voir le [README de `bootimage`][Readme of `bootimage`] pour plus de détails et les options de configuration possibles.
[Readme of `bootimage`]: https://github.com/rust-osdev/bootimage
Nous pouvons maintenant utiliser `cargo run` pour compiler notre noyau et le lancer dans QEMU.
## Et ensuite?
Dans le prochain article, nous explorerons le tampon texte VGA plus en détails et nous écrirons une interface sécuritaire pour l'utiliser. Nous allons aussi mettre en place la macro `println`.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/index.ja.md
================================================
+++
title = "Rustでつくる最小のカーネル"
weight = 2
path = "ja/minimal-rust-kernel"
date = 2018-02-10
[extra]
# Please update this when updating the translation
translation_based_on_commit = "7212ffaa8383122b1eb07fe1854814f99d2e1af4"
# GitHub usernames of the people that translated this post
translators = ["swnakamura", "JohnTitor"]
+++
この記事では、Rustで最小限の64bitカーネルを作ります。前の記事で作った[フリースタンディングなRustバイナリ][freestanding Rust binary]を下敷きにして、何かを画面に出力する、ブータブルディスクイメージを作ります。
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.ja.md
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-02` ブランチ][post branch]にあります。
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
## 起動のプロセス {#the-boot-process}
コンピュータを起動すると、マザーボードの [ROM] に保存されたファームウェアのコードを実行し始めます。このコードは、[起動時の自己テスト][power-on self-test]を実行し、使用可能なRAMを検出し、CPUとハードウェアを事前初期化します。その後、ブータブルディスクを探し、オペレーティングシステムのカーネルを起動します。
[ROM]: https://ja.wikipedia.org/wiki/Read_only_memory
[power-on self-test]: https://ja.wikipedia.org/wiki/Power_On_Self_Test
x86には2つのファームウェアの標準規格があります:"Basic Input/Output System" (**[BIOS]**) と、より新しい "Unified Extensible Firmware Interface" (**[UEFI]**) です。BIOS規格は古く時代遅れですが、シンプルでありすべてのx86のマシンで1980年代からよくサポートされています。対して、UEFIはより現代的でずっと多くの機能を持っていますが、セットアップが複雑です(少なくとも私はそう思います)。
[BIOS]: https://ja.wikipedia.org/wiki/Basic_Input/Output_System
[UEFI]: https://ja.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
今の所、このブログではBIOSしかサポートしていませんが、UEFIのサポートも計画中です。お手伝いいただける場合は、[GitHubのissue](https://github.com/phil-opp/blog_os/issues/349)をご覧ください。
### BIOSの起動
ほぼすべてのx86システムがBIOSによる起動をサポートしています。これは近年のUEFIベースのマシンも例外ではなく、それらはエミュレートされたBIOSを使います。前世紀のすべてのマシンにも同じブートロジックが使えるなんて素晴らしいですね。しかし、この広い互換性は、BIOSによる起動の最大の欠点でもあるのです。というのもこれは、1980年代の化石のようなブートローダーを動かすために、CPUが[リアルモード][real mode]と呼ばれる16bit互換モードにされてしまうということを意味しているからです。
まあ順を追って見ていくこととしましょう。
コンピュータは起動時にマザーボードにある特殊なフラッシュメモリからBIOSを読み込みます。BIOSは自己テストとハードウェアの初期化ルーチンを実行し、ブータブルディスクを探します。ディスクが見つかると、 **ブートローダー** と呼ばれる、その先頭512バイトに保存された実行可能コードへと操作権が移ります。多くのブートローダーのサイズは512バイトより大きいため、通常は512バイトに収まる小さな最初のステージと、その最初のステージによって読み込まれる第2ステージに分けられています。
ブートローダーはディスク内のカーネルイメージの場所を特定し、メモリに読み込まなければなりません。また、CPUを16bitの[リアルモード][real mode]から32bitの[プロテクトモード][protected mode]へ、そして64bitの[ロングモード][long mode]――64bitレジスタとすべてのメインメモリが利用可能になります――へと変更しなければなりません。3つ目の仕事は、特定の情報(例えばメモリーマップなどです)をBIOSから聞き出し、OSのカーネルに渡すことです。
[real mode]: https://ja.wikipedia.org/wiki/リアルモード
[protected mode]: https://ja.wikipedia.org/wiki/プロテクトモード
[long mode]: https://en.wikipedia.org/wiki/Long_mode
[memory segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation
ブートローダーを書くのにはアセンブリ言語を必要とするうえ、「何も考えずにプロセッサーのこのレジスタにこの値を書き込んでください」のような勉強の役に立たない作業がたくさんあるので、ちょっと面倒くさいです。ですのでこの記事ではブートローダーの制作については飛ばして、代わりに[bootimage]という、自動でカーネルの前にブートローダを置いてくれるツールを使いましょう。
[bootimage]: https://github.com/rust-osdev/bootimage
自前のブートローダーを作ることに興味がある人もご期待下さい、これに関する記事も計画中です!
#### Multiboot標準規格
すべてのオペレーティングシステムが、自身にのみ対応しているブートローダーを実装するということを避けるために、1995年に[フリーソフトウェア財団][Free Software Foundation]が[Multiboot]というブートローダーの公開標準規格を策定しています。この標準規格では、ブートローダーとオペレーティングシステムのインターフェースが定義されており、Multibootに準拠したブートローダーであれば、同じくそれに準拠したすべてのオペレーティングシステムが読み込めるようになっています。そのリファレンス実装として、Linuxシステムで一番人気のブートローダーである[GNU GRUB]があります。
[Free Software Foundation]: https://ja.wikipedia.org/wiki/フリーソフトウェア財団
[Multiboot]: https://wiki.osdev.org/Multiboot
[GNU GRUB]: https://ja.wikipedia.org/wiki/GNU_GRUB
カーネルをMultibootに準拠させるには、カーネルファイルの先頭にいわゆる[Multiboot header]を挿入するだけで済みます。このおかげで、OSをGRUBで起動するのはとても簡単です。しかし、GRUBとMultiboot標準規格にはいくつか問題もあります:
[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
- これらは32bitプロテクトモードしかサポートしていません。そのため、64bitロングモードに変更するためのCPUの設定は依然行う必要があります。
- これらは、カーネルではなくブートローダーがシンプルになるように設計されています。例えば、カーネルは[通常とは異なるデフォルトページサイズ][adjusted default page size]でリンクされる必要があり、そうしないとGRUBはMultiboot headerを見つけることができません。他にも、カーネルに渡される[ブート情報][boot information]は、クリーンな抽象化を与えてくれず、アーキテクチャ依存の構造を多く含んでいます。
- GRUBもMultiboot標準規格もドキュメントが充実していません。
- カーネルファイルからブータブルディスクイメージを作るには、ホストシステムにGRUBがインストールされている必要があります。これにより、MacとWindows上での開発は比較的難しくなっています。
[adjusted default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2
[boot information]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
これらの欠点を考慮し、私達はGRUBとMultiboot標準規格を使わないことに決めました。しかし、あなたのカーネルをGRUBシステム上で読み込めるように、私達の[bootimage]ツールにMultibootのサポートを追加することも計画しています。Multiboot準拠なカーネルを書きたい場合は、このブログシリーズの[第1版][first edition]をご覧ください。
[first edition]: @/edition-1/_index.md
### UEFI
(今の所UEFIのサポートは提供していませんが、ぜひともしたいと思っています!お手伝いいただける場合は、 [GitHub issue](https://github.com/phil-opp/blog_os/issues/349)で教えてください。)
## 最小のカーネル
どのようにコンピュータが起動するのかについてざっくりと理解できたので、自前で最小のカーネルを書いてみましょう。目標は、起動したら画面に"Hello, World!"と出力するようなディスクイメージを作ることです。というわけで、前の記事の[独立したRustバイナリ][freestanding Rust binary]をもとにして作っていきます。
覚えていますか、この独立したバイナリは`cargo`を使ってビルドしましたが、オペレーティングシステムに依って異なるエントリポイント名とコンパイルフラグが必要なのでした。これは`cargo`は標準では **ホストシステム**(あなたの使っているシステム)向けにビルドするためです。例えばWindows上で走るカーネルというのはあまり意味がなく、私達の望む動作ではありません。代わりに、明確に定義された **ターゲットシステム** 向けにコンパイルできると理想的です。
### RustのNightly版をインストールする {#installing-rust-nightly}
Rustには**stable**、**beta**、**nightly**の3つのリリースチャンネルがあります。Rust Bookはこれらの3つのチャンネルの違いをとても良く説明しているので、一度[確認してみてください](https://doc.rust-jp.rs/book-ja/appendix-07-nightly-rust.html)。オペレーティングシステムをビルドするには、nightlyチャンネルでしか利用できないいくつかの実験的機能を使う必要があるので、Rustのnightly版をインストールすることになります。
Rustの実行環境を管理するのには、[rustup]を強くおすすめします。nightly、beta、stable版のコンパイラをそれぞれインストールすることができますし、アップデートするのも簡単です。現在のディレクトリにnightlyコンパイラを使うようにするには、`rustup override set nightly`と実行してください。もしくは、`rust-toolchain`というファイルに`nightly`と記入してプロジェクトのルートディレクトリに置くことでも指定できます。Nightly版を使っていることは、`rustc --version`と実行することで確かめられます。表示されるバージョン名の末尾に`-nightly`とあるはずです。
[rustup]: https://www.rustup.rs/
nightlyコンパイラでは、いわゆる**feature flag**をファイルの先頭につけることで、いろいろな実験的機能を使うことを選択できます。例えば、`#![feature(asm)]`を`main.rs`の先頭につけることで、インラインアセンブリのための実験的な[`asm!`マクロ][`asm!` macro]を有効化することができます。ただし、これらの実験的機能は全くもって不安定であり、将来のRustバージョンにおいては事前の警告なく変更されたり取り除かれたりする可能性があることに注意してください。このため、絶対に必要なときにのみこれらを使うことにします。
[`asm!` macro]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
### ターゲットの仕様
Cargoは`--target`パラメータを使ってさまざまなターゲットをサポートします。ターゲットはいわゆる[target triple][target triple]によって表されます。これはCPUアーキテクチャ、製造元、オペレーティングシステム、そして[ABI]を表します。例えば、`x86_64-unknown-linux-gnu`というtarget tripleは、`x86_64`のCPU、製造元不明、GNU ABIのLinuxオペレーティングシステム向けのシステムを表します。Rustは[多くのtarget triple][platform-support]をサポートしており、その中にはAndroidのための`arm-linux-androideabi`や[WebAssemblyのための`wasm32-unknown-unknown`](https://www.hellorust.com/setup/wasm-target/)などがあります。
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[ABI]: https://stackoverflow.com/a/2456882
[platform-support]: https://forge.rust-lang.org/release/platform-support.html
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
しかしながら、私達のターゲットシステムには、いくつか特殊な設定パラメータが必要になります(例えば、その下ではOSが走っていない、など)。なので、[既存のtarget triple][platform-support]はどれも当てはまりません。ありがたいことに、RustではJSONファイルを使って[独自のターゲット][custom-targets]を定義できます。例えば、`x86_64-unknown-linux-gnu`というターゲットを表すJSONファイルはこんな感じです。
```json
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
ほとんどのフィールドはLLVMがそのプラットフォーム向けのコードを生成するために必要なものです。例えば、[`data-layout`]フィールドは種々の整数、浮動小数点数、ポインタ型の大きさを定義しています。次に、`target-pointer-width`のような、条件付きコンパイルに用いられるフィールドがあります。第3の種類のフィールドはクレートがどのようにビルドされるべきかを定義します。例えば、`pre-link-args`フィールドは[リンカ][linker]に渡される引数を指定しています。
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
[linker]: https://ja.wikipedia.org/wiki/リンケージエディタ
私達のカーネルも`x86_64`のシステムをターゲットとするので、私達のターゲット仕様も上のものと非常によく似たものになるでしょう。`x86_64-blog_os.json`というファイル(お好きな名前を選んでください)を作り、共通する要素を埋めるところから始めましょう。
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true
}
```
ベアメタル環境で実行するので、`llvm-target`のOSを変え、`os`フィールドを`none`にしたことに注目してください。
以下の、ビルドに関係する項目を追加します。
```json
"linker-flavor": "ld.lld",
"linker": "rust-lld",
```
私達のカーネルをリンクするのに、プラットフォーム標準の(Linuxターゲットをサポートしていないかもしれない)リンカではなく、Rustに付属しているクロスプラットフォームの[LLD]リンカを使用します。
[LLD]: https://lld.llvm.org/
```json
"panic-strategy": "abort",
```
この設定は、ターゲットがパニック時の[stack unwinding]をサポートしていないので、プログラムは代わりに直接中断しなければならないということを指定しています。これは、Cargo.tomlに`panic = "abort"`という設定を書くのに等しいですから、後者の設定を消しても構いません(このターゲット設定は、Cargo.tomlの設定と異なり、このあと行う`core`ライブラリの再コンパイルにも適用されます。ですので、Cargo.tomlに設定する方が好みだったとしても、この設定を追加するようにしてください)。
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
```json
"disable-redzone": true,
```
カーネルを書いている以上、ある時点で割り込みを処理しなければならなくなるでしょう。これを安全に行うために、 **"red zone"** と呼ばれる、ある種のスタックポインタ最適化を無効化する必要があります。こうしないと、スタックの破損を引き起こしてしまう恐れがあるためです。より詳しくは、[red zoneの無効化][disabling the red zone]という別記事をご覧ください。
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
```json
"features": "-mmx,-sse,+soft-float",
```
`features`フィールドは、ターゲットの機能を有効化/無効化します。マイナスを前につけることで`mmx`と`sse`という機能を無効化し、プラスを前につけることで`soft-float`という機能を有効化しています。それぞれのフラグの間にスペースは入れてはならず、もしそうするとLLVMが機能文字列の解釈に失敗してしまうことに注意してください。
`mmx`と`sse`という機能は、[Single Instruction Multiple Data (SIMD)]命令をサポートするかを決定します。この命令は、しばしばプログラムを著しく速くしてくれます。しかし、大きなSIMDレジスタをOSカーネルで使うことは性能上の問題に繋がります。 その理由は、カーネルは、割り込まれたプログラムを再開する前に、すべてのレジスタを元に戻さないといけないためです。これは、カーネルがSIMDの状態のすべてを、システムコールやハードウェア割り込みがあるたびにメインメモリに保存しないといけないということを意味します。SIMDの状態情報はとても巨大(512〜1600 bytes)で、割り込みは非常に頻繁に起こるかもしれないので、保存・復元の操作がこのように追加されるのは性能にかなりの悪影響を及ぼします。これを避けるために、(カーネルの上で走っているアプリケーションではなく!)カーネル上でSIMDを無効化するのです。
[Single Instruction Multiple Data (SIMD)]: https://ja.wikipedia.org/wiki/SIMD
SIMDを無効化することによる問題に、`x86_64`における浮動小数点演算は標準ではSIMDレジスタを必要とするということがあります。この問題を解決するため、`soft-float`機能を追加します。これは、すべての浮動小数点演算を通常の整数に基づいたソフトウェア上の関数を使ってエミュレートするというものです。
より詳しくは、[SIMDを無効化する](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md)ことに関する私達の記事を読んでください。
```json
"rustc-abi": "x86-softfloat"
```
As we want to use the `soft-float` feature, we also need to tell the Rust compiler `rustc` that we want to use the corresponding ABI. We can do that by setting the `rustc-abi` field to `x86-softfloat`.
#### まとめると
私達のターゲット仕様ファイルは今このようになっているはずです。
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float",
"rustc-abi": "x86-softfloat"
}
```
### カーネルをビルドする
私達の新しいターゲットのコンパイルにはLinuxの慣習に倣います(理由は知りません、LLVMのデフォルトであるというだけではないでしょうか)。つまり、[前の記事][previous post]で説明したように`_start`という名前のエントリポイントが要るということです。
[previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.ja.md
```rust
// src/main.rs
#![no_std] // don't link the Rust standard library
#![no_main] // disable all Rust-level entry points
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[unsafe(no_mangle)] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
// this function is the entry point, since the linker looks for a function
// named `_start` by default
loop {}
}
```
ホストOSが何であるかにかかわらず、エントリポイントは`_start`という名前でなければならないことに注意してください。
これで、私達の新しいターゲットのためのカーネルを、JSONファイル名を`--target`として渡すことでビルドできるようになりました。
```
> cargo build --target x86_64-blog_os.json
error: `.json` target specs require -Zjson-target-spec
```
失敗しましたね!エラーは、カスタムJSONターゲット仕様は明示的な有効化が必要な不安定機能であると言っています。これは、JSONターゲットファイルのフォーマットがまだ安定と見なされていないため、将来のRustのバージョンで変更される可能性があるからです。詳細は[カスタムJSONターゲット仕様のトラッキングissue][json-target-spec-issue]をご覧ください。
[json-target-spec-issue]: https://github.com/rust-lang/rust/issues/151528
#### `json-target-spec`オプション
カスタムJSONターゲット仕様のサポートを有効にするためには、[cargoの設定][cargo configuration]ファイルを`.cargo/config.toml`に作り(`.cargo`フォルダは`src`フォルダの横に置きます)、次の内容を書きましょう:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
```
これにより不安定な`json-target-spec`機能が有効になり、カスタムJSONターゲットファイルを使用できるようになります。
この設定を行ったら、もう一度ビルドしてみましょう:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
```
今度は別のエラーが出ました!エラーはRustコンパイラが[`core`ライブラリ][`core` library]を見つけられなくなったと言っています。このライブラリは、`Result` や `Option`、イテレータのような基本的なRustの型を持っており、暗黙のうちにすべての`no_std`なクレートにリンクされています。
[`core` library]: https://doc.rust-lang.org/nightly/core/index.html
問題は、coreライブラリはRustコンパイラと一緒にコンパイル済みライブラリとして配布されているということです。そのため、これは、私達独自のターゲットではなく、サポートされているhost triple(例えば `x86_64-unknown-linux-gnu`)でのみ使えるのです。他のターゲットのためにコードをコンパイルしたいときには、`core`をそれらのターゲットに向けて再コンパイルする必要があります。
#### `build-std`オプション
ここでcargoの[`build-std`機能][`build-std` feature]の出番です。これを使うと`core`やその他の標準ライブラリクレートについて、Rustインストール時に一緒についてくるコンパイル済みバージョンを使う代わりに、必要に応じて再コンパイルすることができます。これはとても新しくまだ完成していないので、不安定機能とされており、[nightly Rustコンパイラ][nightly Rust compilers]でのみ利用可能です。
[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[nightly Rust compilers]: #installing-rust-nightly
この機能を使うためには、[cargoの設定][cargo configuration]ファイル`.cargo/config.toml`に以下を追加しましょう:
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
build-std = ["core", "compiler_builtins"]
```
これはcargoに`core`と`compiler_builtins`ライブラリを再コンパイルするよう命令します。後者が必要なのは`core`がこれに依存しているためです。 これらのライブラリを再コンパイルするためには、cargoがRustのソースコードにアクセスできる必要があります。これは`rustup component add rust-src`でインストールできます。
**注意:** `unstable.build-std`設定キーを使うには、少なくとも2020-07-15以降のRust nightlyが必要です。
`unstable.build-std`設定キーをセットし、`rust-src`コンポーネントをインストールしたら、ビルドコマンドをもう一度実行しましょう。
```
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
今回は、`cargo build`が`core`、`rustc-std-workspace-core` (`compiler_builtins`の依存です)、そして `compiler_builtins`を私達のカスタムターゲット向けに再コンパイルしているということがわかります。
#### メモリ関係の組み込み関数
Rustコンパイラは、すべてのシステムにおいて、特定の組み込み関数が利用可能であるということを前提にしています。それらの関数の多くは、私達がちょうど再コンパイルした`compiler_builtins`クレートによって提供されています。しかしながら、通常システムのCライブラリによって提供されているので標準では有効化されていない、メモリ関係の関数がいくつかあります。それらの関数には、メモリブロック内のすべてのバイトを与えられた値にセットする`memset`、メモリーブロックを他のブロックへとコピーする`memcpy`、2つのメモリーブロックを比較する`memcmp`などがあります。これらの関数はどれも、現在の段階で我々のカーネルをコンパイルするのに必要というわけではありませんが、コードを追加していくとすぐに必要になるでしょう(たとえば、構造体をコピーする、など)。
オペレーティングシステムのCライブラリにリンクすることはできませんので、これらの関数をコンパイラに与えてやる別の方法が必要になります。このための方法として考えられるものの一つが、自前で`memset`を実装し、(コンパイル中の自動リネームを防ぐため)`#[unsafe(no_mangle)]`アトリビュートをこれらに適用することでしょう。しかし、こうすると、これらの関数の実装のちょっとしたミスが未定義動作に繋がりうるため危険です。たとえば、`for`ループを使って`memcpy`を実装すると無限再帰を起こしてしまうかもしれません。なぜなら、`for`ループは暗黙のうちに[`IntoIterator::into_iter`]トレイトメソッドを呼び出しており、これが`memcpy`を再び呼び出しているかもしれないためです。なので、代わりに既存のよくテストされた実装を再利用するのが良いでしょう。
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
ありがたいことに、`compiler_builtins`クレートにはこれらの必要な関数すべての実装が含まれており、標準ではCライブラリの実装と競合しないように無効化されているだけなのです。これはcargoの[`build-std-features`]フラグを`["compiler-builtins-mem"]`に設定することで有効化できます。`build-std`フラグと同じように、このフラグはコマンドラインで`-Z`フラグとして渡すこともできれば、`.cargo/config.toml`ファイルの`unstable`テーブルで設定することもできます。ビルド時は常にこのフラグをセットしたいので、設定ファイルを使う方が良いでしょう:
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
```
(`compiler-builtins-mem`機能のサポートが追加されたのは[つい最近](https://github.com/rust-lang/rust/pull/77284)なので、`2020-09-30`以降のRust nightlyが必要です。)
このとき、裏で`compiler_builtins`クレートの[`mem`機能][`mem` feature]が有効化されています。これにより、このクレートの[`memcpy`などの実装][`memcpy` etc. implementations]に`#[unsafe(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
この変更をもって、私達のカーネルはコンパイラに必要とされているすべての関数の有効な実装を手に入れたので、コードがもっと複雑になっても変わらずコンパイルできるでしょう。
#### 標準のターゲットをセットする
`cargo build`を呼び出すたびに`--target`パラメータを渡すのを避けるために、デフォルトのターゲットを書き換えることができます。これをするには、以下を`.cargo/config.toml`の[cargo設定][cargo configuration]ファイルに付け加えます:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
これは、明示的に`--target`引数が渡されていないときは、`x86_64-blog_os.json`ターゲットを使うように`cargo`に命令します。つまり、私達はカーネルをシンプルな`cargo build`コマンドでビルドできるということです。cargoの設定のオプションについてより詳しく知るには、[公式のドキュメント][cargo configuration]を読んでください。
これにより、シンプルな`cargo build`コマンドで、ベアメタルのターゲットに私達のカーネルをビルドできるようになりました。しかし、ブートローダーによって呼び出される私達の`_start`エントリポイントはまだ空っぽです。そろそろここから何かを画面に出力してみましょう。
### 画面に出力する
現在の段階で画面に文字を出力する最も簡単な方法は[VGAテキストバッファ][VGA text buffer]です。これは画面に出力されている内容を保持しているVGAハードウェアにマップされた特殊なメモリです。通常、これは25行からなり、それぞれの行は80文字セルからなります。それぞれの文字セルは、背景色と前景色付きのASCII文字を表示します。画面出力はこのように見えるでしょう:
[VGA text buffer]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode

次の記事では、VGAバッファの正確なレイアウトについて議論し、このためのちょっとしたドライバも書きます。"Hello World!"を出力するためには、バッファがアドレス`0xb8000`にあり、それぞれの文字セルはASCIIのバイトと色のバイトからなることを知っている必要があります。
実装はこんな感じになります:
```rust
static HELLO: &[u8] = b"Hello World!";
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
まず、`0xb8000`という整数を[生ポインタ][raw pointer]にキャストします。次に[静的][static]な`HELLO`という[バイト列][byte string]変数の要素に対し[イテレート][iterate]します。[`enumerate`]メソッドを使うことで、`for` ループの実行回数を表す変数 `i` も取得します。ループの内部では、[`offset`]メソッドを使って文字列のバイトと対応する色のバイト(`0xb`は明るいシアン色)を書き込んでいます。
[iterate]: https://doc.rust-jp.rs/book-ja/ch13-02-iterators.html
[static]: https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#静的ライフタイム
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
[raw pointer]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html#生ポインタを参照外しする
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
すべてのメモリへの書き込み処理のコードを、[`unsafe`][`unsafe`]ブロックが囲んでいることに注意してください。この理由は、私達の作った生ポインタが正しいものであることをRustコンパイラが証明できないためです。生ポインタはどんな場所でも指しうるので、データの破損につながるかもしれません。これらの操作を`unsafe`ブロックに入れることで、私達はこれが正しいことを確信しているとコンパイラに伝えているのです。ただし、`unsafe`ブロックはRustの安全性チェックを消すわけではなく、[追加で5つのことができるようになる][five additional things]だけということに注意してください。
**訳注:** 翻訳時点(2020-10-20)では、リンク先のThe Rust book日本語版には「追加でできるようになること」は4つしか書かれていません。
[`unsafe`]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html
[five additional things]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html#unsafeの強大な力superpower
強調しておきたいのですが、 **このような機能はRustでプログラミングするときに使いたいものではありません!** unsafeブロック内で生ポインタを扱うと非常にしくじりやすいです。たとえば、注意不足でバッファの終端のさらに奥に書き込みを行ってしまったりするかもしれません。
ですので、`unsafe`の使用は最小限にしたいです。これをするために、Rustでは安全なabstractionを作ることができます。たとえば、VGAバッファ型を作り、この中にすべてのunsafeな操作をカプセル化し、外側からの誤った操作が**不可能**であることを保証できるでしょう。こうすれば、`unsafe`の量を最小限にでき、[メモリ安全性][memory safety]を侵していないことを確かにできます。そのような安全なVGAバッファの abstraction を次の記事で作ります。
[memory safety]: https://ja.wikipedia.org/wiki/メモリ安全性
## カーネルを実行する
では、目で見て分かる処理を行う実行可能ファイルを手に入れたので、実行してみましょう。まず、コンパイルした私達のカーネルを、ブートローダーとリンクすることによってブータブルディスクイメージにする必要があります。そして、そのディスクイメージを、[QEMU]バーチャルマシン内や、USBメモリを使って実際のハードウェア上で実行できます。
### ブートイメージを作る
コンパイルされた私達のカーネルをブータブルディスクイメージに変えるには、ブートローダーとリンクする必要があります。[起動のプロセスのセクション][section about booting]で学んだように、ブートローダーはCPUを初期化しカーネルをロードする役割があります。
[section about booting]: #the-boot-process
自前のブートローダーを書くと、それだけで1つのプロジェクトになってしまうので、代わりに[`bootloader`]クレートを使いましょう。このクレートは、Cに依存せず、Rustとインラインアセンブリだけで基本的なBIOSブートローダーを実装しています。私達のカーネルを起動するためにこれを依存関係に追加する必要があります:
[`bootloader`]: https://crates.io/crates/bootloader
```toml
# in Cargo.toml
[dependencies]
bootloader = "0.9"
```
bootloaderを依存として加えることだけでブータブルディスクイメージが実際に作れるわけではなく、私達のカーネルをコンパイル後にブートローダーにリンクする必要があります。問題は、cargoが[ビルド後にスクリプトを走らせる機能][post-build scripts]を持っていないことです。
[post-build scripts]: https://github.com/rust-lang/cargo/issues/545
この問題を解決するため、私達は`bootimage`というツールを作りました。これは、まずカーネルとブートローダーをコンパイルし、そしてこれらをリンクしてブータブルディスクイメージを作ります。このツールをインストールするには、以下のコマンドをターミナルで実行してください:
```
cargo install bootimage
```
`bootimage`を実行しブートローダをビルドするには、`llvm-tools-preview`というrustupコンポーネントをインストールする必要があります。これは`rustup component add llvm-tools-preview`と実行することでできます。
`bootimage`をインストールし、`llvm-tools-preview`を追加したら、以下のように実行することでブータブルディスクイメージを作れます:
```
> cargo bootimage
```
このツールが私達のカーネルを`cargo build`を使って再コンパイルしていることがわかります。そのため、あなたの行った変更を自動で検知してくれます。その後、bootloaderをビルドします。これには少し時間がかかるかもしれません。他の依存クレートと同じように、ビルドは一度しか行われず、その都度キャッシュされるので、以降のビルドはもっと早くなります。最終的に、`bootimage`はbootloaderとあなたのカーネルを合体させ、ブータブルディスクイメージにします。
このコマンドを実行したら、`target/x86_64-blog_os/debug`ディレクトリ内に`bootimage-blog_os.bin`という名前のブータブルディスクイメージがあるはずです。これをバーチャルマシン内で起動してもいいですし、実際のハードウェア上で起動するためにUSBメモリにコピーしてもいいでしょう(ただし、これはCDイメージではありません。CDイメージは異なるフォーマットを持つので、これをCDに焼いてもうまくいきません)。
#### どういう仕組みなの?
`bootimage`ツールは、裏で以下のステップを行っています:
- 私達のカーネルを[ELF]ファイルにコンパイルする。
- 依存であるbootloaderをスタンドアロンの実行ファイルとしてコンパイルする。
- カーネルのELFファイルのバイト列をブートローダーにリンクする。
[ELF]: https://ja.wikipedia.org/wiki/Executable_and_Linkable_Format
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
起動時、ブートローダーは追加されたELFファイルを読み、解釈します。次にプログラム部をページテーブルの仮想アドレスにマップし、`.bss`部をゼロにし、スタックをセットアップします。最後に、エントリポイントのアドレス(私達の`_start`関数)を読み、そこにジャンプします。
### QEMUで起動する
これで、ディスクイメージを仮想マシンで起動できます。[QEMU]を使ってこれを起動するには、以下のコマンドを実行してください:
[QEMU]: https://www.qemu.org/
```
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
```
これにより、以下のような見た目の別のウィンドウが開きます:

私達の書いた"Hello World!"が画面に見えますね。
### 実際のマシン
USBメモリにこれを書き込んで実際のマシン上で起動することも可能です:
```
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
```
`sdX`はあなたのUSBメモリのデバイス名です。そのデバイス上のすべてのデータが上書きされてしまうので、 **正しいデバイス名を選んでいるのかよく確認してください** 。
イメージをUSBメモリに書き込んだあとは、そこから起動することによって実際のハードウェア上で走らせることができます。特殊なブートメニューを使ったり、BIOS設定で起動時の優先順位を変え、USBメモリから起動することを選択する必要があるでしょう。ただし、`bootloader`クレートはUEFIをサポートしていないので、UEFIマシン上ではうまく動作しないということに注意してください。
### `cargo run`を使う
QEMU上でより簡単に私達のカーネルを走らせるために、cargoの`runner`設定が使えます。
```toml
# in .cargo/config.toml
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
```
`target.'cfg(target_os = "none")'`テーブルは、`"os"`フィールドが`"none"`であるようなすべてのターゲットに適用されます。私達の`x86_64-blog_os.json`ターゲットもその1つです。`runner`キーは`cargo run`のときに呼ばれるコマンドを指定しています。このコマンドは、ビルドが成功した後に、実行可能ファイルのパスを第一引数として実行されます。詳しくは、[cargoのドキュメント][cargo configuration]を読んでください。
`bootimage runner`コマンドは、`runner`キーとして実行するために設計されています。このコマンドは、与えられた実行ファイルをプロジェクトの依存するbootloaderとリンクして、QEMUを立ち上げます。より詳しく知りたいときや、設定オプションについては[`bootimage`のReadme][Readme of `bootimage`]を読んでください。
[Readme of `bootimage`]: https://github.com/rust-osdev/bootimage
これで、`cargo run`を使ってカーネルをコンパイルしQEMU内で起動することができます。
## 次は?
次の記事では、VGAテキストバッファをより詳しく学び、そのための安全なインターフェースを書きます。また、`println`マクロのサポートも行います。
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/index.ko.md
================================================
+++
title = "최소 기능을 갖춘 커널"
weight = 2
path = "ko/minimal-rust-kernel"
date = 2018-02-10
[extra]
# Please update this when updating the translation
translation_based_on_commit = "c1af4e31b14e562826029999b9ab1dce86396b93"
# GitHub usernames of the people that translated this post
translators = ["JOE1994", "Quqqu"]
+++
이번 포스트에서는 x86 아키텍처에서 최소한의 기능으로 동작하는 64비트 Rust 커널을 함께 만들 것입니다. 지난 포스트 [Rust로 'Freestanding 실행파일' 만들기][freestanding Rust binary] 에서 작업한 것을 토대로 부팅 가능한 디스크 이미지를 만들고 화면에 데이터를 출력해볼 것입니다.
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.md
이 블로그는 [GitHub 저장소][GitHub]에서 오픈 소스로 개발되고 있으니, 문제나 문의사항이 있다면 저장소의 'Issue' 기능을 이용해 제보해주세요. [페이지 맨 아래][at the bottom]에 댓글을 남기실 수도 있습니다. 이 포스트와 관련된 모든 소스 코드는 저장소의 [`post-02 브랜치`][post branch]에서 확인하실 수 있습니다.
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
## 부팅 과정 {#the-boot-process}
전원이 켜졌을 때 컴퓨터가 맨 처음 하는 일은 바로 마더보드의 [롬 (ROM)][ROM]에 저장된 펌웨어 코드를 실행하는 것입니다.
이 코드는 [시동 자체 시험][power-on self-test]을 진행하고, 사용 가능한 램 (RAM)을 확인하며, CPU 및 하드웨어의 초기화 작업을 진행합니다.
그 후에는 부팅 가능한 디스크를 감지하고 운영체제 커널을 부팅하기 시작합니다.
[ROM]: https://en.wikipedia.org/wiki/Read-only_memory
[power-on self-test]: https://en.wikipedia.org/wiki/Power-on_self-test
x86 시스템에는 두 가지 펌웨어 표준이 존재합니다: 하나는 "Basic Input/Output System"(**[BIOS]**)이고 다른 하나는 "Unified Extensible Firmware Interface" (**[UEFI]**) 입니다. BIOS 표준은 구식 표준이지만, 간단하며 1980년대 이후 출시된 어떤 x86 하드웨어에서도 지원이 잘 됩니다. UEFI는 신식 표준으로서 더 많은 기능들을 갖추었지만, 제대로 설정하고 구동시키기까지의 과정이 더 복잡합니다 (적어도 제 주관적 입장에서는 그렇게 생각합니다).
[BIOS]: https://en.wikipedia.org/wiki/BIOS
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
우리가 만들 운영체제에서는 BIOS 표준만을 지원할 것이지만, UEFI 표준도 지원하고자 하는 계획이 있습니다. UEFI 표준을 지원할 수 있도록 도와주시고 싶다면 해당 [깃헙 이슈](https://github.com/phil-opp/blog_os/issues/349)를 확인해주세요.
### BIOS 부팅
UEFI 표준으로 동작하는 최신 기기들도 가상 BIOS를 지원하기에, 존재하는 거의 모든 x86 시스템들이 BIOS 부팅을 지원합니다. 덕분에 하나의 BIOS 부팅 로직을 구현하면 여태 만들어진 거의 모든 컴퓨터를 부팅시킬 수 있습니다. 동시에 이 방대한 호환성이 BIOS의 가장 큰 약점이기도 한데,
그 이유는 1980년대의 구식 부트로더들에 대한 하위 호환성을 유지하기 위해 부팅 전에는 항상 CPU를 16비트 호환 모드 ([real mode]라고도 불림)로 설정해야 하기 때문입니다.
이제 BIOS 부팅 과정의 첫 단계부터 살펴보겠습니다:
여러분이 컴퓨터의 전원을 켜면, 제일 먼저 컴퓨터는 마더보드의 특별한 플래시 메모리로부터 BIOS 이미지를 로드합니다. BIOS 이미지는 자가 점검 및 하드웨어 초기화 작업을 처리한 후에 부팅 가능한 디스크가 있는지 탐색합니다. 부팅 가능한 디스크가 있다면, 제어 흐름은 해당 디스크의 _부트로더 (bootloader)_ 에게 넘겨집니다. 이 부트로더는 디스크의 가장 앞 주소 영역에 저장되는 512 바이트 크기의 실행 파일입니다. 대부분의 부트로더들의 경우 로직을 저장하는 데에 512 바이트보다 더 큰 용량이 필요하기에, 부트로더의 로직을 둘로 쪼개어 첫 단계 로직을 첫 512 바이트 안에 담고, 두 번째 단계 로직은 첫 단계 로직에 의해 로드된 이후 실행됩니다.
부트로더는 커널 이미지가 디스크의 어느 주소에 저장되어있는지 알아낸 후 메모리에 커널 이미지를 로드해야 합니다. 그다음 CPU를 16비트 [real mode]에서 32비트 [protected mode]로 전환하고, 그 후에 다시 CPU를 64비트 [long mode]로 전환한 이후부터 64비트 레지스터 및 메인 메모리의 모든 주소를 사용할 수 있게 됩니다. 부트로더가 세 번째로 할 일은 BIOS로부터 메모리 매핑 정보 등의 필요한 정보를 알아내어 운영체제 커널에 전달하는 것입니다.
[real mode]: https://en.wikipedia.org/wiki/Real_mode
[protected mode]: https://en.wikipedia.org/wiki/Protected_mode
[long mode]: https://en.wikipedia.org/wiki/Long_mode
[memory segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation
부트로더를 작성하는 것은 상당히 성가신 작업인데, 그 이유는 어셈블리 코드도 작성해야 하고 "A 레지스터에 B 값을 저장하세요" 와 같이 원리를 단번에 이해하기 힘든 작업이 많이 수반되기 때문입니다. 따라서 이 포스트에서는 부트로더를 만드는 것 자체를 다루지는 않고, 대신 운영체제 커널의 맨 앞에 부트로더를 자동으로 추가해주는 [bootimage]라는 도구를 제공합니다.
[bootimage]: https://github.com/rust-osdev/bootimage
본인의 부트로더를 직접 작성하는 것에 흥미가 있으시다면, 이 주제로 여러 포스트가 나올 계획이니 기대해주세요!
#### Multiboot 표준
운영체제마다 부트로더 구현 방법이 다르다면 한 운영체제에서 동작하는 부트로더가 다른 운영체제에서는 호환이 되지 않을 것입니다. 이런 불편한 점을 막기 위해 [Free Software Foundation]에서 1995년에 [Multiboot]라는 부트로더 표준을 개발했습니다. 이 표준은 부트로더와 운영체제 사이의 상호 작용 방식을 정의하였는데, 이 Multiboot 표준에 따르는 부트로더는 Multiboot 표준을 지원하는 어떤 운영체제에서도 동작합니다. 이 표준을 구현한 대표적인 예로 리눅스 시스템에서 가장 인기 있는 부트로더인 [GNU GRUB]이 있습니다.
[Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation
[Multiboot]: https://wiki.osdev.org/Multiboot
[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB
운영체제 커널이 Multiboot를 지원하게 하려면 커널 파일의 맨 앞에 [Multiboot 헤더][Multiboot header]를 삽입해주면 됩니다. 이렇게 하면 GRUB에서 운영체제를 부팅하는 것이 매우 쉬워집니다. 하지만 GRUB 및 Multiboot 표준도 몇 가지 문제점들을 안고 있습니다:
[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
- 오직 32비트 protected mode만을 지원합니다. 64비트 long mode를 이용하고 싶다면 CPU 설정을 별도로 변경해주어야 합니다.
- Multiboot 표준 및 GRUB은 부트로더 구현의 단순화를 우선시하여 개발되었기에, 이에 호응하는 커널 측의 구현이 번거로워진다는 단점이 있습니다. 예를 들어, GRUB이 Multiboot 헤더를 제대로 찾을 수 있으려면 커널 측에서 [조정된 기본 페이지 크기 (adjusted default page size)][adjusted default page size]를 링크하는 것이 강제됩니다. 또한, 부트로더가 커널로 전달하는 [부팅 정보][boot information]는 적절한 추상 레벨에서 표준화된 형태로 전달되는 대신 하드웨어 아키텍처마다 상이한 형태로 제공됩니다.
- GRUB 및 Multiboot 표준에 대한 문서화 작업이 덜 되어 있습니다.
- GRUB이 호스트 시스템에 설치되어 있어야만 커널 파일로부터 부팅 가능한 디스크 이미지를 만들 수 있습니다. 이 때문에 Windows 및 Mac에서는 부트로더를 개발하는 것이 Linux보다 어렵습니다.
[adjusted default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2
[boot information]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
이러한 단점들 때문에 우리는 GRUB 및 Multiboot 표준을 사용하지 않을 것입니다. 하지만 미래에 우리의 [bootimage] 도구가 Multiboot 표준을 지원하도록 하는 것도 계획 중입니다. Multiboot 표준을 지원하는 운영체제를 커널을 개발하는 것에 관심이 있으시다면, 이 블로그 시리즈의 [첫 번째 에디션][first edition]을 확인해주세요.
[first edition]: @/edition-1/_index.md
### UEFI
(아직 UEFI 표준을 지원하지 않지만, UEFI 표준을 지원할 수 있도록 도와주시려면 해당 [깃헙 이슈](https://github.com/phil-opp/blog_os/issues/349)에 댓글을 남겨주세요!)
## 최소한의 기능을 갖춘 운영체제 커널
컴퓨터의 부팅 과정에 대해서 대략적으로 알게 되었으니, 이제 우리 스스로 최소한의 기능을 갖춘 운영체제 커널을 작성해볼 차례입니다. 우리의 목표는 부팅 이후 화면에 "Hello World!" 라는 메세지를 출력하는 디스크 이미지를 만드는 것입니다. 지난 포스트에서 만든 [freestanding Rust 실행파일][freestanding Rust binary] 을 토대로 작업을 이어나갑시다.
지난 포스트에서 우리는 `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 언어 설치 파일들을 관리할 때 [rustup]을 사용하는 것을 강력 추천합니다. rustup을 통해 nightly, beta 그리고 stable 컴파일러들을 모두 설치하고 업데이트할 수 있습니다. `rustup override set nightly` 명령어를 통해 현재 디렉토리에서 항상 nightly 버전의 Rust를 사용하도록 설정할 수 있습니다.
`rust-toolchain`이라는 파일을 프로젝트 루트 디렉토리에 만들고 이 파일에 `nightly`라는 텍스트를 적어 놓아도 같은 효과를 볼 수 있습니다. `rustc --version` 명령어를 통해 현재 nightly 버전이 설치되어 있는지 확인할 수 있습니다 (출력되는 버전 넘버가 `-nightly`라는 텍스트로 끝나야 합니다).
[rustup]: https://www.rustup.rs/
nightly 컴파일러는 _feature 플래그_ 를 소스코드의 맨 위에 추가함으로써 여러 실험적인 기능들을 선별해 이용할 수 있게 해줍니다. 예를 들어, `#![feature(asm)]` 를 `main.rs`의 맨 위에 추가하면 [`asm!` 매크로][`asm!` macro]를 사용할 수 있습니다. `asm!` 매크로는 인라인 어셈블리 코드를 작성할 때 사용합니다.
이런 실험적인 기능들은 말 그대로 "실험적인" 기능들이기에 미래의 Rust 버전들에서는 예고 없이 변경되거나 삭제될 수도 있습니다. 그렇기에 우리는 이 실험적인 기능들을 최소한으로만 사용할 것입니다.
[`asm!` macro]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
### 컴파일 대상 정의하기
Cargo는 `--target` 인자를 통해 여러 컴파일 대상 시스템들을 지원합니다. 컴파일 대상은 소위 _[target triple]_ 을 통해 표현되는데, CPU 아키텍쳐와 CPU 공급 업체, 운영체제, 그리고 [ABI]를 파악할 수 있습니다. 예를 들어 `x86_64-unknown-linux-gnu`는 `x86_64` CPU, 임의의 CPU 공급 업체, Linux 운영체제, 그리고 GNU ABI를 갖춘 시스템을 나타냅니다. Rust는 Android를 위한 `arm-linux-androideabi`와 [WebAssembly를 위한 `wasm32-unknown-unknown`](https://www.hellorust.com/setup/wasm-target/)를 비롯해 [다양한 target triple들][platform-support]을 지원합니다.
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[ABI]: https://stackoverflow.com/a/2456882
[platform-support]: https://forge.rust-lang.org/release/platform-support.html
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
우리가 목표로 하는 컴파일 대상 환경 (운영체제가 따로 없는 환경)을 정의하려면 몇 가지 특별한 설정 인자들을 사용해야 하기에 [Rust 에서 기본적으로 지원하는 target triple][platform-support] 중에서는 우리가 쓸 수 있는 것은 없습니다. 다행히도 Rust에서는 JSON 파일을 이용해 [우리가 목표로 하는 컴파일 대상 환경][custom-targets]을 직접 정의할 수 있습니다. 예를 들어, `x86_64-unknown-linux-gnu` 환경을 직접 정의하는 JSON 파일의 내용은 아래와 같습니다:
```json
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
대부분의 필드 값들은 LLVM이 해당 환경을 목표로 코드를 생성하는 과정에서 필요합니다. 예시로, [`data-layout`] 필드는 다양한 정수, 부동소수점 표기 소수, 포인터 등의 메모리 상 실제 크기를 지정합니다. 또한 `target-pointer-width`와 같이 Rust가 조건부 컴파일을 하는 과정에서 이용하는 필드들도 있습니다.
마지막 남은 종류의 필드들은 crate가 어떻게 빌드되어야 하는지 결정합니다. 예를 들어 `pre-link-args` 필드는 [링커][linker]에 전달될 인자들을 설정합니다.
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
[linker]: https://en.wikipedia.org/wiki/Linker_(computing)
우리도 `x86_64` 시스템에서 구동할 운영체제 커널을 작성할 것이기에, 우리가 사용할 컴파일 대상 환경 환경 설정 파일 (JSON 파일) 또한 위의 내용과 많이 유사할 것입니다. 일단 `x86_64-blog_os.json`이라는 파일을 만들고 아래와 같이 파일 내용을 작성해주세요:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true
}
```
우리의 운영체제는 bare metal 환경에서 동작할 것이기에, `llvm-target` 필드의 운영체제 값과 `os` 필드의 값은 `none`입니다.
아래의 빌드 관련 설정들을 추가해줍니다:
```json
"linker-flavor": "ld.lld",
"linker": "rust-lld",
```
현재 사용 중인 플랫폼의 기본 링커 대신 Rust와 함께 배포되는 크로스 플랫폼 [LLD] 링커를 사용해 커널을 링크합니다 (기본 링커는 리눅스 환경을 지원하지 않을 수 있습니다).
[LLD]: https://lld.llvm.org/
```json
"panic-strategy": "abort",
```
해당 환경이 패닉 시 [스택 되감기][stack unwinding]을 지원하지 않기에, 위 설정을 통해 패닉 시 프로그램이 즉시 실행 종료되도록 합니다. 위 설정은 Cargo.toml 파일에 `panic = "abort"` 설정을 추가하는 것과 비슷한 효과이기에, Cargo.toml에서는 해당 설정을 지우셔도 괜찮습니다 (다만, Cargo.toml에서의 설정과는 달리 이 설정은 이후 단계에서 우리가 `core` 라이브러리를 재컴파일할 때에도 유효하게 적용된다는 점이 중요합니다. 위 설정은 꼭 추가해주세요!).
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
```json
"disable-redzone": true,
```
커널을 작성하려면, 커널이 인터럽트에 대해 어떻게 대응하는지에 대한 로직도 작성하게 될 것입니다. 안전하게 이런 로직을 작성하기 위해서는 _“red zone”_ 이라고 불리는 스택 포인터 최적화 기능을 해제해야 합니다 (그렇지 않으면 해당 기능으로 인해 스택 메모리가 우리가 원치 않는 값으로 덮어쓰일 수 있습니다). 이 내용에 대해 더 자세히 알고 싶으시면 [red zone 기능 해제][disabling the red zone] 포스트를 확인해주세요.
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.ko.md
```json
"features": "-mmx,-sse,+soft-float",
```
`features` 필드는 컴파일 대상 환경의 기능들을 활성화/비활성화 하는 데 이용합니다. 우리는 `-` 기호를 통해 `mmx`와 `sse` 기능들을 비활성화시키고 `+` 기호를 통해 `soft-float` 기능을 활성화시킬 것입니다. `features` 필드의 문자열 내부 플래그들 사이에 빈칸이 없도록 해야 합니다. 그렇지 않으면 LLVM이 `features` 필드의 문자열 값을 제대로 해석하지 못하기 때문입니다.
`mmx`와 `sse`는 [Single Instruction Multiple Data (SIMD)] 명령어들의 사용 여부를 결정하는데, 해당 명령어들은 프로그램의 실행 속도를 훨씬 빠르게 만드는 데에 도움을 줄 수 있습니다. 하지만 운영체제에서 큰 SIMD 레지스터를 사용할 경우 커널의 성능에 문제가 생길 수 있습니다. 그 이유는 커널이 인터럽트 되었던 프로그램을 다시 실행하기 전에 모든 레지스터 값들을 인터럽트 직전 시점의 상태로 복원시켜야 하기 때문입니다. 커널이 SIMD 레지스터를 사용하려면 각 시스템 콜 및 하드웨어 인터럽트가 일어날 때마다 모든 SIMD 레지스터에 저장된 값들을 메인 메모리에 저장해야 할 것입니다. SIMD 레지스터들이 총 차지하는 용량은 매우 크고 (512-1600 바이트) 인터럽트 또한 자주 일어날 수 있기에,
SIMD 레지스터 값들을 메모리에 백업하고 또 다시 복구하는 과정은 커널의 성능을 심각하게 해칠 수 있습니다. 이를 피하기 위해 커널이 SIMD 명령어를 사용하지 않도록 설정합니다 (물론 우리의 커널 위에서 구동할 프로그램들은 SIMD 명령어들을 사용할 수 있습니다!).
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
`x86_64` 환경에서 SIMD 기능을 비활성화하는 것에는 걸림돌이 하나 있는데, 그것은 바로 `x86_64` 환경에서 부동소수점 계산 시 기본적으로 SIMD 레지스터가 사용된다는 것입니다. 이 문제를 해결하기 위해 `soft-float` 기능 (일반 정수 계산만을 이용해 부동소수점 계산을 소프트웨어 단에서 모방)을 활성화시킵니다.
더 자세히 알고 싶으시다면, 저희가 작성한 [SIMD 기능 해제](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.ko.md)에 관한 포스트를 확인해주세요.
```json
"rustc-abi": "x86-softfloat"
```
`soft-float` 기능을 사용하려면, Rust 컴파일러 `rustc` 에게도 해당 ABI를 사용하겠다고 알려줘야 합니다. 이를 위해 `rustc-abi` 필드를 `x86-softfloat` 으로 설정하면 됩니다.
#### 요약
컴파일 대상 환경 설정 파일을 아래와 같이 작성합니다:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float",
"rustc-abi": "x86-softfloat"
}
```
### 커널 빌드하기
우리가 정의한 새로운 컴파일 대상 환경을 목표로 컴파일할 때에 리눅스 시스템의 관례를 따를 것입니다 (LLVM이 기본적으로 리눅스 시스템 관례를 따르기에 그렇습니다). 즉, [지난 포스트][previous post]에서 설명한 것처럼 우리는 실행 시작 지점의 이름을 `_start`로 지정할 것입니다:
[previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.md
```rust
// src/main.rs
#![no_std] // Rust 표준 라이브러리를 링크하지 않도록 합니다
#![no_main] // Rust 언어에서 사용하는 실행 시작 지점 (main 함수)을 사용하지 않습니다
use core::panic::PanicInfo;
/// 패닉이 일어날 경우, 이 함수가 호출됩니다.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[unsafe(no_mangle)] // 이 함수의 이름을 mangle하지 않습니다
pub extern "C" fn _start() -> ! {
// 링커는 기본적으로 '_start' 라는 이름을 가진 함수를 실행 시작 지점으로 삼기에,
// 이 함수는 실행 시작 지점이 됩니다
loop {}
}
```
호스트 운영체제에 관계 없이 실행 시작 지점 함수의 이름은 `_start`로 지정해야 함을 기억해주세요.
이제 `--target` 인자를 통해 위에서 다룬 JSON 파일의 이름을 전달하여 우리가 정의한 새로운 컴파일 대상 환경을 목표로 커널을 빌드할 수 있습니다:
```
> cargo build --target x86_64-blog_os.json
error: `.json` target specs require -Zjson-target-spec
```
실패하였군요! 이 오류는 커스텀 JSON 타겟 스펙이 명시적인 활성화가 필요한 불안정한 기능이라는 것을 알려줍니다. JSON 타겟 파일의 형식이 아직 안정적으로 간주되지 않기 때문에 미래 Rust 버전에서 변경될 수 있습니다. 자세한 정보는 [커스텀 JSON 타겟 스펙 트래킹 이슈][json-target-spec-issue]를 참조하세요.
[json-target-spec-issue]: https://github.com/rust-lang/rust/issues/151528
#### `json-target-spec` 기능
커스텀 JSON 타겟 스펙 지원을 활성화하려면, [cargo 설정][cargo configuration] 파일 `.cargo/config.toml`을 생성해야 합니다 (`.cargo` 폴더는 `src` 폴더 옆에 위치해야 합니다):
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# .cargo/config.toml 에 들어갈 내용
[unstable]
json-target-spec = true
```
이를 통해 불안정한 `json-target-spec` 기능이 활성화되어 커스텀 JSON 타겟 파일을 사용할 수 있게 됩니다.
이 설정을 완료한 후, 다시 빌드해 봅시다:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
```
이제 다른 오류가 발생합니다! 이 오류는 Rust 컴파일러가 더 이상 [`core` 라이브러리][`core` library]를 찾지 못한다는 것을 알려줍니다. 이 라이브러리는 `Result`와 `Option` 그리고 반복자 등 Rust의 기본적인 타입들을 포함하며, 모든 `no_std` 크레이트에 암시적으로 링크됩니다.
[`core` library]: https://doc.rust-lang.org/nightly/core/index.html
문제는 core 라이브러리가 _미리 컴파일된 상태_ 의 라이브러리로 Rust 컴파일러와 함께 배포된다는 것입니다. `x86_64-unknown-linux-gnu` 등 배포된 라이브러리가 지원하는 컴파일 목표 환경을 위해 빌드하는 경우 문제가 없지만, 우리가 정의한 커스텀 환경을 위해 빌드하는 경우에는 라이브러리를 이용할 수 없습니다. 기본적으로 지원되지 않는 새로운 시스템 환경을 위해 코드를 빌드하기 위해서는 새로운 시스템 환경에서 구동 가능하도록 `core` 라이브러리를 새롭게 빌드해야 합니다.
#### `build-std` 기능
이제 cargo의 [`build-std 기능`][`build-std` feature]이 필요한 시점이 왔습니다. Rust 언어 설치파일에 함께 배포된 `core` 및 다른 표준 라이브러리 크레이트 버전을 사용하는 대신, 이 기능을 이용하여 해당 크레이트들을 직접 재컴파일하여 사용할 수 있습니다. 이 기능은 아직 비교적 새로운 기능이며 아직 완성된 기능이 아니기에, "unstable" 한 기능으로 표기되며 [nightly 버전의 Rust 컴파일러][nightly Rust compilers]에서만 이용가능합니다.
[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[nightly Rust compilers]: #installing-rust-nightly
해당 기능을 사용하려면, [cargo 설정][cargo configuration] 파일 `.cargo/config.toml`에 아래와 같이 추가해야 합니다:
```toml
# .cargo/config.toml 에 들어갈 내용
[unstable]
json-target-spec = true
build-std = ["core", "compiler_builtins"]
```
위 설정은 cargo에게 `core`와 `compiler_builtins` 라이브러리를 새로 컴파일하도록 지시합니다. `compiler_builtins`는 `core`가 사용하는 라이브러리입니다. 해당 라이브러리들의 소스 코드가 있어야 새로 컴파일할 수 있기에, `rustup component add rust-src` 명령어를 통해 소스 코드를 설치합니다.
**주의:** `unstable.build-std` 설정 키를 이용하려면 2020-07-15 혹은 그 이후에 출시된 Rust nightly 버전을 사용하셔야 합니다.
cargo 설정 키 `unstable.build-std`를 설정하고 `rust-src` 컴포넌트를 설치한 후에 다시 빌드 명령어를 실행합니다:
```
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
이제 `cargo build` 명령어가 `core`, `rustc-std-workspace-core` (`compiler_builtins`가 필요로 하는 라이브러리) 그리고 `compiler_builtins` 라이브러리를 우리의 커스텀 컴파일 대상을 위해 다시 컴파일하는 것을 확인할 수 있습니다.
#### 메모리 관련 내장 함수
Rust 컴파일러는 특정 군의 내장 함수들이 (built-in function) 모든 시스템에서 주어진다고 가정합니다. 대부분의 내장 함수들은 우리가 방금 컴파일한 `compiler_builtins` 크레이트가 이미 갖추고 있습니다. 하지만 그중 몇몇 메모리 관련 함수들은 기본적으로 사용 해제 상태가 되어 있는데, 그 이유는 해당 함수들을 호스트 시스템의 C 라이브러리가 제공하는 것이 관례이기 때문입니다. `memset`(메모리 블럭 전체에 특정 값 저장하기), `memcpy` (한 메모리 블럭의 데이터를 다른 메모리 블럭에 옮겨쓰기), `memcmp` (메모리 블럭 두 개의 데이터를 비교하기) 등이 이 분류에 해당합니다. 여태까지는 우리가 이 함수들 중 어느 하나도 사용하지 않았지만, 운영체제 구현을 더 추가하다 보면 필수적으로 사용될 함수들입니다 (예를 들어, 구조체를 복사하여 다른 곳에 저장할 때).
우리는 운영체제의 C 라이브러리를 링크할 수 없기에, 다른 방식으로 이러한 내장 함수들을 컴파일러에 전달해야 합니다. 한 방법은 우리가 직접 `memset` 등의 내장함수들을 구현하고 컴파일 과정에서 함수명이 바뀌지 않도록 `#[unsafe(no_mangle)]` 속성을 적용하는 것입니다. 하지만 이 방법의 경우 우리가 직접 구현한 함수 로직에 아주 작은 실수만 있어도 undefined behavior를 일으킬 수 있기에 위험합니다. 예를 들어 `memcpy`를 구현하는 데에 `for`문을 사용한다면 무한 재귀 루프가 발생할 수 있는데, 그 이유는 `for`문의 구현이 내부적으로 trait 함수인 [`IntoIterator::into_iter`]를 호출하고 이 함수가 다시 `memcpy` 를 호출할 수 있기 때문입니다. 그렇기에 충분히 검증된 기존의 구현 중 하나를 사용하는 것이 바람직합니다.
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
다행히도 `compiler_builtins` 크레이트가 이미 필요한 내장함수 구현을 전부 갖추고 있으며, C 라이브러리에서 오는 내장함수 구현과 충돌하지 않도록 사용 해제되어 있었던 것 뿐입니다. cargo의 [`build-std-features`] 플래그를 `["compiler-builtins-mem"]`으로 설정함으로써 `compiler_builtins`에 포함된 내장함수 구현을 사용할 수 있습니다. `build-std` 플래그와 유사하게 이 플래그 역시 커맨드 라인에서 `-Z` 플래그를 이용해 인자로 전달하거나 `.cargo/config.toml`의 `[unstable]` 테이블에서 설정할 수 있습니다. 우리는 매번 이 플래그를 사용하여 빌드할 예정이기에 `.cargo/config.toml`을 통해 설정을 하는 것이 장기적으로 더 편리할 것입니다:
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
```toml
# .cargo/config.toml 에 들어갈 내용
[unstable]
json-target-spec = true
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
```
(`compiler-builtins-mem` 기능에 대한 지원이 [굉장히 최근에 추가되었기에](https://github.com/rust-lang/rust/pull/77284), Rust nightly `2020-09-30` 이상의 버전을 사용하셔야 합니다.)
이 기능은 `compiler_builtins` 크레이트의 [`mem` 기능 (feature)][`mem` feature]를 활성화 시킵니다. 이는 `#[unsafe(no_mangle)]` 속성이 [`memcpy` 등의 함수 구현][`memcpy` etc. implementations]에 적용되게 하여 링크가 해당 함수들을 식별하고 사용할 수 있게 합니다.
[`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
[`memcpy` etc. implementations]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
이제 우리의 커널은 컴파일러가 요구하는 함수들에 대한 유효한 구현을 모두 갖추게 되었기에, 커널 코드가 더 복잡해지더라도 상관 없이 컴파일하는 데에 문제가 없을 것입니다.
#### 기본 컴파일 대상 환경 설정하기
기본 컴파일 대상 환경을 지정하여 설정해놓으면 `cargo build` 명령어를 실행할 때마다 `--target` 인자를 넘기지 않아도 됩니다. [cargo 설정][cargo configuration] 파일인 `.cargo/config.toml`에 아래의 내용을 추가해주세요:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# .cargo/config.toml 에 들어갈 내용
[build]
target = "x86_64-blog_os.json"
```
이로써 `cargo`는 명시적으로 `--target` 인자가 주어지지 않으면 `x86_64-blog_os.json`에 명시된 컴파일 대상 환경을 기본 값으로 이용합니다. `cargo build` 만으로 간단히 커널을 빌드할 수 있게 되었습니다. cargo 설정 옵션들에 대해 더 자세한 정보를 원하시면 [공식 문서][cargo configuration]을 확인해주세요.
`cargo build`만으로 이제 bare metal 환경을 목표로 커널을 빌드할 수 있지만, 아직 실행 시작 지점 함수 `_start`는 텅 비어 있습니다.
이제 이 함수에 코드를 추가하여 화면에 메세지를 출력해볼 것입니다.
### 화면에 출력하기
현재 단계에서 가장 쉽게 화면에 문자를 출력할 수 있는 방법은 바로 [VGA 텍스트 버퍼][VGA text buffer]를 이용하는 것입니다. 이것은 VGA 하드웨어에 매핑되는 특수한 메모리 영역이며 화면에 출력될 내용이 저장됩니다. 주로 이 버퍼는 주로 25행 80열 (행마다 80개의 문자 저장)로 구성됩니다. 각 문자는 ASCII 문자로서 전경색 혹은 배경색과 함께 화면에 출력됩니다. 화면 출력 결과의 모습은 아래와 같습니다:
[VGA text buffer]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode

VGA 버퍼가 정확히 어떤 구조를 하고 있는지는 다음 포스트에서 VGA 버퍼 드라이버를 작성하면서 다룰 것입니다. "Hello World!" 메시지를 출력하는 데에는 그저 버퍼의 시작 주소가 `0xb8000`이라는 것, 그리고 각 문자는 ASCII 문자를 위한 1바이트와 색상 표기를 위한 1바이트가 필요하다는 것만 알면 충분합니다.
코드 구현은 아래와 같습니다:
```rust
static HELLO: &[u8] = b"Hello World!";
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
우선 정수 `0xb8000`을 [raw 포인터][raw pointer]로 형변환 합니다. 그 다음 [static (정적 변수)][static] [바이트 문자열][byte string] `HELLO`의 반복자를 통해 각 바이트를 읽고, [`enumerate`] 함수를 통해 각 바이트의 문자열 내에서의 인덱스 값 `i`를 얻습니다. for문의 내부에서는 [`offset`] 함수를 통해 VGA 버퍼에 문자열의 각 바이트 및 색상 코드를 저장합니다 (`0xb`: light cyan 색상 코드).
[iterate]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html
[static]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
[raw pointer]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
메모리 쓰기 작업을 위한 코드 주변에 [`unsafe`] 블록이 있는 것에 주목해주세요. 여기서 `unsafe` 블록이 필요한 이유는 Rust 컴파일러가 우리가 만든 raw 포인터가 유효한 포인터인지 검증할 능력이 없기 때문입니다. `unsafe` 블록 안에 포인터에 대한 쓰기 작업 코드를 적음으로써, 우리는 컴파일러에게 해당 메모리 쓰기 작업이 확실히 안전하다고 선언한 것입니다. `unsafe` 블록이 Rust의 모든 안전성 체크를 해제하는 것은 아니며, `unsafe` 블록 안에서만 [다섯 가지 작업들을 추가적으로][five additional things] 할 수 있습니다.
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
[five additional things]: https://doc.rust-lang.org/stable/book/ch20-01-unsafe-rust.html#unsafe-superpowers
**이런 식의 Rust 코드를 작성하는 것은 절대 바람직하지 않다는 것을 강조드립니다!** unsafe 블록 안에서 raw pointer를 쓰다보면 메모리 버퍼 크기를 넘어선 메모리 주소에 데이터를 저장하는 등의 실수를 범하기 매우 쉽습니다.
그렇기에 `unsafe` 블록의 사용을 최소화하는 것이 바람직하며, 그렇게 하기 위해 Rust에서 우리는 안전한 추상 계층을 만들어 이용할 수 있습니다. 예를 들어, 모든 위험한 요소들을 전부 캡슐화한 VGA 버퍼 타입을 만들어 외부 사용자가 해당 타입을 사용 중에 메모리 안전성을 해칠 가능성을 _원천 차단_ 할 수 있습니다. 이런 설계를 통해 최소한의 `unsafe` 블록만을 사용하면서 동시에 우리가 [메모리 안전성][memory safety]을 해치는 일이 없을 것이라 자신할 수 있습니다. 이러한 안전한 추상 레벨을 더한 VGA 버퍼 타입은 다음 포스트에서 만들게 될 것입니다.
[memory safety]: https://en.wikipedia.org/wiki/Memory_safety
## 커널 실행시키기
이제 우리가 얻은 실행 파일을 실행시켜볼 차례입니다. 우선 컴파일 완료된 커널을 부트로더와 링크하여 부팅 가능한 디스크 이미지를 만들어야 합니다. 그 다음에 해당 디스크 이미지를 QEMU 가상머신에서 실행시키거나 USB 드라이브를 이용해 실제 컴퓨터에서 부팅할 수 있습니다.
### 부팅 가능한 디스크 이미지 만들기
부팅 가능한 디스크 이미지를 만들기 위해서는 컴파일된 커널을 부트로더와 링크해야합니다. [부팅에 대한 섹션][section about booting]에서 알아봤듯이, 부트로더는 CPU를 초기화하고 커널을 불러오는 역할을 합니다.
[section about booting]: #the-boot-process
우리는 부트로더를 직접 작성하는 대신에 [`bootloader`] 크레이트를 사용할 것입니다. 이 크레이트는 Rust와 인라인 어셈블리만으로 간단한 BIOS 부트로더를 구현합니다. 운영체제 커널을 부팅하는 데에 이 크레이트를 쓰기 위해 의존 크레이트 목록에 추가해줍니다:
[`bootloader`]: https://crates.io/crates/bootloader
```toml
# Cargo.toml 에 들어갈 내용
[dependencies]
bootloader = "0.9"
```
부트로더를 의존 크레이트로 추가하는 것만으로는 부팅 가능한 디스크 이미지를 만들 수 없습니다. 커널 컴파일이 끝난 후 커널을 부트로더와 함께 링크할 수 있어야 하는데, cargo는 현재 [빌드 직후 스크립트 실행][post-build scripts] 기능을 지원하지 않습니다.
[post-build scripts]: https://github.com/rust-lang/cargo/issues/545
이 문제를 해결하기 위해 저희가 `bootimage` 라는 도구를 만들었습니다. 이 도구는 커널과 부트로더를 각각 컴파일 한 이후에 둘을 링크하여 부팅 가능한 디스크 이미지를 생성해줍니다. 이 도구를 설치하려면 터미널에서 아래의 명령어를 실행해주세요.
```
cargo install bootimage
```
`bootimage` 도구를 실행시키고 부트로더를 빌드하려면 `llvm-tools-preview` 라는 rustup 컴포넌트가 필요합니다. 명령어 `rustup component add llvm-tools-preview`를 통해 해당 컴포넌트를 설치합니다.
`bootimage` 도구를 설치하고 `llvm-tools-preview` 컴포넌트를 추가하셨다면, 이제 아래의 명령어를 통해 부팅 가능한 디스크 이미지를 만들 수 있습니다:
```
> cargo bootimage
```
이 도구가 `cargo build`를 통해 커널을 다시 컴파일한다는 것을 확인하셨을 것입니다. 덕분에 커널 코드가 변경되어도 `cargo bootimage` 명령어 만으로도 해당 변경 사항이 바로 빌드에 반영됩니다. 그 다음 단계로 이 도구가 부트로더를 컴파일 할 것인데, 시간이 제법 걸릴 수 있습니다. 일반적인 의존 크레이트들과 마찬가지로 한 번 빌드한 후에 빌드 결과가 캐시(cache)되기 때문에, 두 번째 빌드부터는 소요 시간이 훨씬 적습니다. 마지막 단계로 `bootimage` 도구가 부트로더와 커널을 하나로 합쳐 부팅 가능한 디스크 이미지를 생성합니다.
명령어 실행이 끝난 후, `target/x86_64-blog_os/debug` 디렉토리에 `bootimage-blog_os.bin`이라는 부팅 가능한 디스크 이미지가 생성되어 있을 것입니다. 이것을 가상머신에서 부팅하거나 USB 드라이브에 복사한 뒤 실제 컴퓨터에서 부팅할 수 있습니다 (우리가 만든 디스크 이미지는 CD 이미지와는 파일 형식이 다르기 때문에 CD에 복사해서 부팅하실 수는 없습니다).
#### 어떻게 동작하는 걸까요?
`bootimage` 도구는 아래의 작업들을 순서대로 진행합니다:
- 커널을 컴파일하여 [ELF] 파일 생성
- 부트로더 크레이트를 독립된 실행파일로서 컴파일
- 커널의 ELF 파일을 부트로더에 링크
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
부팅이 시작되면, 부트로더는 커널의 ELF 파일을 읽고 파싱합니다. 그 다음 프로그램의 세그먼트들을 페이지 테이블의 가상 주소에 매핑하고, `bss` 섹션의 모든 메모리 값을 0으로 초기화하며, 스택을 초기화합니다. 마지막으로, 프로그램 실행 시작 지점의 주소 (`_start` 함수의 주소)에서 제어 흐름이 계속되도록 점프합니다.
### QEMU에서 커널 부팅하기
이제 우리의 커널 디스크 이미지를 가상 머신에서 부팅할 수 있습니다. [QEMU]에서 부팅하려면 아래의 명령어를 실행하세요:
[QEMU]: https://www.qemu.org/
```
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
```
위 명령어를 실행하면 아래와 같은 새로운 창이 열릴 것입니다:

화면에 "Hello World!" 메세지가 출력된 것을 확인하실 수 있습니다.
### 실제 컴퓨터에서 부팅하기
USB 드라이브에 우리의 커널을 저장한 후 실제 컴퓨터에서 부팅하는 것도 가능합니다:
```
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
```
`sdX` 대신 여러분이 소지한 USB 드라이브의 기기명을 입력하시면 됩니다. 해당 기기에 쓰인 데이터는 전부 덮어씌워지기 때문에 정확한 기기명을 입력하도록 주의해주세요.
이미지를 USB 드라이브에 다 덮어썼다면, 이제 실제 하드웨어에서 해당 이미지를 통해 부트하여 실행할 수 있습니다. 아마 특별한 부팅 메뉴를 사용하거나 BIOS 설정에서 부팅 순서를 변경하여 USB로부터 부팅하도록 설정해야 할 것입니다. `bootloader` 크레이트가 아직 UEFI를 지원하지 않기에, UEFI 표준을 사용하는 기기에서는 부팅할 수 없습니다.
### `cargo run` 명령어 사용하기
QEMU에서 커널을 쉽게 실행할 수 있게 아래처럼 `runner`라는 새로운 cargo 설정 키 값을 추가합니다.
```toml
# .cargo/config.toml 에 들어갈 내용
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
```
`target.'cfg(target_os = "none")'`가 붙은 키 값은 `"os"` 필드 설정이 `"none"`으로 되어 있는 컴파일 대상 환경에만 적용됩니다. 따라서 우리의 `x86_64-blog_os.json` 또한 적용 대상에 포함됩니다. `runner` 키 값은 `cargo run` 명령어 실행 시 어떤 명령어를 실행할지 지정합니다. 빌드가 성공적으로 끝난 후에 `runner` 키 값의 명령어가 실행됩니다. [cargo 공식 문서][cargo configuration]를 통해 더 자세한 내용을 확인하실 수 있습니다.
명령어 `bootimage runner`는 프로젝트의 부트로더 라이브러리를 링크한 후에 QEMU를 실행시킵니다.
그렇기에 일반적인 `runner` 실행파일을 실행하듯이 `bootimage runner` 명령어를 사용하실 수 있습니다. [`bootimage` 도구의 Readme 문서][Readme of `bootimage`]를 통해 더 자세한 내용 및 다른 가능한 설정 옵션들을 확인하세요.
[Readme of `bootimage`]: https://github.com/rust-osdev/bootimage
이제 `cargo run` 명령어를 통해 우리의 커널을 컴파일하고 QEMU에서 부팅할 수 있습니다.
## 다음 단계는 무엇일까요?
다음 글에서는 VGA 텍스트 버퍼 (text buffer)에 대해 더 알아보고 VGA text buffer와 안전하게 상호작용할 수 있는 방법을 구현할 것입니다.
또한 `println` 매크로를 사용할 수 있도록 기능을 추가할 것입니다.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/index.md
================================================
+++
title = "A Minimal Rust Kernel"
weight = 2
path = "minimal-rust-kernel"
date = 2018-02-10
[extra]
chapter = "Bare Bones"
+++
In this post, we create a minimal 64-bit Rust kernel for the x86 architecture. We build upon the [freestanding Rust binary] from the previous post to create a bootable disk image that prints something to the screen.
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.md
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-02`][post branch] branch.
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
## The Boot Process
When you turn on a computer, it begins executing firmware code that is stored in motherboard [ROM]. This code performs a [power-on self-test], detects available RAM, and pre-initializes the CPU and hardware. Afterwards, it looks for a bootable disk and starts booting the operating system kernel.
[ROM]: https://en.wikipedia.org/wiki/Read-only_memory
[power-on self-test]: https://en.wikipedia.org/wiki/Power-on_self-test
On x86, there are two firmware standards: the “Basic Input/Output System“ (**[BIOS]**) and the newer “Unified Extensible Firmware Interface” (**[UEFI]**). The BIOS standard is old and outdated, but simple and well-supported on any x86 machine since the 1980s. UEFI, in contrast, is more modern and has much more features, but is more complex to set up (at least in my opinion).
[BIOS]: https://en.wikipedia.org/wiki/BIOS
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
Currently, we only provide BIOS support, but support for UEFI is planned, too. If you'd like to help us with this, check out the [Github issue](https://github.com/phil-opp/blog_os/issues/349).
### BIOS Boot
Almost all x86 systems have support for BIOS booting, including newer UEFI-based machines that use an emulated BIOS. This is great, because you can use the same boot logic across all machines from the last century. But this wide compatibility is at the same time the biggest disadvantage of BIOS booting, because it means that 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.
But let's start from the beginning:
When you turn on a computer, it loads the BIOS from some special flash memory located on the motherboard. The BIOS runs self-test and initialization routines of the hardware, then it looks for bootable disks. If it finds one, control is transferred to its _bootloader_, which is a 512-byte portion of executable code stored at the disk's beginning. Most bootloaders are larger than 512 bytes, so bootloaders are commonly split into a small first stage, which fits into 512 bytes, and a second stage, which is subsequently loaded by the first stage.
The bootloader has to determine the location of the kernel image on the disk and load it into memory. It also needs to switch the CPU from the 16-bit [real mode] first to the 32-bit [protected mode], and then to the 64-bit [long mode], where 64-bit registers and the complete main memory are available. Its third job is to query certain information (such as a memory map) from the BIOS and pass it to the OS kernel.
[real mode]: https://en.wikipedia.org/wiki/Real_mode
[protected mode]: https://en.wikipedia.org/wiki/Protected_mode
[long mode]: https://en.wikipedia.org/wiki/Long_mode
[memory segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation
Writing a bootloader is a bit cumbersome as it requires assembly language and a lot of non insightful steps like “write this magic value to this processor register”. Therefore, we don't cover bootloader creation in this post and instead provide a tool named [bootimage] that automatically prepends a bootloader to your kernel.
[bootimage]: https://github.com/rust-osdev/bootimage
If you are interested in building your own bootloader: Stay tuned, a set of posts on this topic is already planned!
#### The Multiboot Standard
To avoid that every operating system implements its own bootloader, which is only compatible with a single OS, the [Free Software Foundation] created an open bootloader standard called [Multiboot] in 1995. The standard defines an interface between the bootloader and the operating system, so that any Multiboot-compliant bootloader can load any Multiboot-compliant operating system. The reference implementation is [GNU GRUB], which is the most popular bootloader for Linux systems.
[Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation
[Multiboot]: https://wiki.osdev.org/Multiboot
[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB
To make a kernel Multiboot compliant, one just needs to insert a so-called [Multiboot header] at the beginning of the kernel file. This makes it very easy to boot an OS from GRUB. However, GRUB and the Multiboot standard have some problems too:
[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
- They support only the 32-bit protected mode. This means that you still have to do the CPU configuration to switch to the 64-bit long mode.
- They are designed to make the bootloader simple instead of the kernel. For example, the kernel needs to be linked with an [adjusted default page size], because GRUB can't find the Multiboot header otherwise. Another example is that the [boot information], which is passed to the kernel, contains lots of architecture-dependent structures instead of providing clean abstractions.
- Both GRUB and the Multiboot standard are only sparsely documented.
- GRUB needs to be installed on the host system to create a bootable disk image from the kernel file. This makes development on Windows or Mac more difficult.
[adjusted default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2
[boot information]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
Because of these drawbacks, we decided to not use GRUB or the Multiboot standard. However, we plan to add Multiboot support to our [bootimage] tool, so that it's possible to load your kernel on a GRUB system too. If you're interested in writing a Multiboot compliant kernel, check out the [first edition] of this blog series.
[first edition]: @/edition-1/_index.md
### UEFI
(We don't provide UEFI support at the moment, but we would love to! If you'd like to help, please tell us in the [Github issue](https://github.com/phil-opp/blog_os/issues/349).)
## A Minimal Kernel
Now that we roughly know how a computer boots, it's time to create our own minimal kernel. Our goal is to create a disk image that prints a “Hello World!” to the screen when booted. We do this by extending the previous post's [freestanding Rust binary].
As you may remember, we built the freestanding binary through `cargo`, but depending on the operating system, we needed different entry point names and compile flags. That's because `cargo` builds for the _host system_ by default, i.e., the system you're running on. This isn't something we want for our kernel, because a kernel that runs on top of, e.g., Windows, does not make much sense. Instead, we want to compile for a clearly defined _target system_.
### Installing Rust Nightly
Rust has three release channels: _stable_, _beta_, and _nightly_. The Rust Book explains the difference between these channels really well, so take a minute and [check it out](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). For building an operating system, we will need some experimental features that are only available on the nightly channel, so we need to install a nightly version of Rust.
To manage Rust installations, I highly recommend [rustup]. It allows you to install nightly, beta, and stable compilers side-by-side and makes it easy to update them. With rustup, you can use a nightly compiler for the current directory by running `rustup override set nightly`. Alternatively, you can add a file called `rust-toolchain` with the content `nightly` to the project's root directory. You can check that you have a nightly version installed by running `rustc --version`: The version number should contain `-nightly` at the end.
[rustup]: https://www.rustup.rs/
The nightly compiler allows us to opt-in to various experimental features by using so-called _feature flags_ at the top of our file. For example, we could enable the experimental [`asm!` macro] for inline assembly by adding `#![feature(asm)]` to the top of our `main.rs`. Note that such experimental features are completely unstable, which means that future Rust versions might change or remove them without prior warning. For this reason, we will only use them if absolutely necessary.
[`asm!` macro]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
### Target Specification
Cargo supports different target systems through the `--target` parameter. The target is described by a so-called _[target triple]_, which describes the CPU architecture, the vendor, the operating system, and the [ABI]. For example, the `x86_64-unknown-linux-gnu` target triple describes a system with an `x86_64` CPU, no clear vendor, and a Linux operating system with the GNU ABI. Rust supports [many different target triples][platform-support], including `arm-linux-androideabi` for Android or [`wasm32-unknown-unknown` for WebAssembly](https://www.hellorust.com/setup/wasm-target/).
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[ABI]: https://stackoverflow.com/a/2456882
[platform-support]: https://forge.rust-lang.org/release/platform-support.html
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
For our target system, however, we require some special configuration parameters (e.g. no underlying OS), so none of the [existing target triples][platform-support] fits. Fortunately, Rust allows us to define [our own target][custom-targets] through a JSON file. For example, a JSON file that describes the `x86_64-unknown-linux-gnu` target looks like this:
```json
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
Most fields are required by LLVM to generate code for that platform. For example, the [`data-layout`] field defines the size of various integer, floating point, and pointer types. Then there are fields that Rust uses for conditional compilation, such as `target-pointer-width`. The third kind of field defines how the crate should be built. For example, the `pre-link-args` field specifies arguments passed to the [linker].
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
[linker]: https://en.wikipedia.org/wiki/Linker_(computing)
We also target `x86_64` systems with our kernel, so our target specification will look very similar to the one above. Let's start by creating an `x86_64-blog_os.json` file (choose any name you like) with the common content:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true
}
```
Note that we changed the OS in the `llvm-target` and the `os` field to `none`, because we will run on bare metal.
We add the following build-related entries:
```json
"linker-flavor": "ld.lld",
"linker": "rust-lld",
```
Instead of using the platform's default linker (which might not support Linux targets), we use the cross-platform [LLD] linker that is shipped with Rust for linking our kernel.
[LLD]: https://lld.llvm.org/
```json
"panic-strategy": "abort",
```
This setting specifies that the target doesn't support [stack unwinding] on panic, so instead the program should abort directly. This has the same effect as the `panic = "abort"` option in our Cargo.toml, so we can remove it from there. (Note that, in contrast to the Cargo.toml option, this target option also applies when we recompile the `core` library later in this post. So, even if you prefer to keep the Cargo.toml option, make sure to include this option.)
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
```json
"disable-redzone": true,
```
We're writing a kernel, so we'll need to handle interrupts at some point. To do that safely, we have to disable a certain stack pointer optimization called the _“red zone”_, because it would cause stack corruption otherwise. For more information, see our separate post about [disabling the red zone].
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
```json
"features": "-mmx,-sse,+soft-float",
```
The `features` field enables/disables target features. We disable the `mmx` and `sse` features by prefixing them with a minus and enable the `soft-float` feature by prefixing it with a plus. Note that there must be no spaces between different flags, otherwise LLVM fails to interpret the features string.
The `mmx` and `sse` features determine support for [Single Instruction Multiple Data (SIMD)] instructions, which can often speed up programs significantly. However, using the large SIMD registers in OS kernels leads to performance problems. The reason is that the kernel needs to restore all registers to their original state before continuing an interrupted program. This means that the kernel has to save the complete SIMD state to main memory on each system call or hardware interrupt. Since the SIMD state is very large (512–1600 bytes) and interrupts can occur very often, these additional save/restore operations considerably harm performance. To avoid this, we disable SIMD for our kernel (not for applications running on top!).
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
A problem with disabling SIMD is that floating point operations on `x86_64` require SIMD registers by default. To solve this problem, we add the `soft-float` feature, which emulates all floating point operations through software functions based on normal integers.
For more information, see our post on [disabling SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
```json
"rustc-abi": "x86-softfloat"
```
As we want to use the `soft-float` feature, we also need to tell the Rust compiler `rustc` that we want to use the corresponding ABI. We can do that by setting the `rustc-abi` field to `x86-softfloat`.
#### Putting it Together
Our target specification file now looks like this:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float",
"rustc-abi": "x86-softfloat"
}
```
### Building our Kernel
Compiling for our new target will use Linux conventions, since the ld.lld linker-flavor instructs llvm to compile with the `-flavor gnu` flag (for more linker options, see [the rustc documentation](https://doc.rust-lang.org/rustc/codegen-options/index.html#linker-flavor)). This means that we need an entry point named `_start` as described in the [previous post]:
[previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.md
```rust
// src/main.rs
#![no_std] // don't link the Rust standard library
#![no_main] // disable all Rust-level entry points
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[unsafe(no_mangle)] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
// this function is the entry point, since the linker looks for a function
// named `_start` by default
loop {}
}
```
Note that the entry point needs to be called `_start` regardless of your host OS.
We can now build the kernel for our new target by passing the name of the JSON file as `--target`:
```
> cargo build --target x86_64-blog_os.json
error: `.json` target specs require -Zjson-target-spec
```
It fails! The error tells us that custom JSON target specifications are an unstable feature that requires explicit opt-in. This is because the format of the JSON target files is not considered stable yet, so changes to it might occur in future versions of Rust. See the [tracking issue for custom JSON target specs][json-target-spec-issue] for more information.
[json-target-spec-issue]: https://github.com/rust-lang/rust/issues/151528
#### The `json-target-spec` Option
To enable support for custom JSON target specifications, we need to create a local [cargo configuration] file at `.cargo/config.toml` (the `.cargo` folder should be next to your `src` folder) with the following content:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
```
This enables the unstable `json-target-spec` feature, allowing us to use custom JSON target files.
With this configuration in place, let's try building again:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
```
It still fails, but with a new error. The error tells us that the Rust compiler does not find the [`core` library]. This library contains basic Rust types such as `Result`, `Option`, and iterators, and is implicitly linked to all `no_std` crates.
[`core` library]: https://doc.rust-lang.org/nightly/core/index.html
The problem is that the core library is distributed together with the Rust compiler as a _precompiled_ library. So it is only valid for supported host triples (e.g., `x86_64-unknown-linux-gnu`) but not for our custom target. If we want to compile code for other targets, we need to recompile `core` for these targets first.
#### The `build-std` Option
That's where the [`build-std` feature] of cargo comes in. It allows to recompile `core` and other standard library crates on demand, instead of using the precompiled versions shipped with the Rust installation. This feature is very new and still not finished, so it is marked as "unstable" and only available on [nightly Rust compilers].
[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[nightly Rust compilers]: #installing-rust-nightly
To use the feature, we need to add the following to our [cargo configuration] file at `.cargo/config.toml`:
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
build-std = ["core", "compiler_builtins"]
```
This tells cargo that it should recompile the `core` and `compiler_builtins` libraries. The latter is required because it is a dependency of `core`. In order to recompile these libraries, cargo needs access to the rust source code, which we can install with `rustup component add rust-src`.
**Note:** The `unstable.build-std` configuration key requires at least the Rust nightly from 2020-07-15.
After setting the `unstable.build-std` configuration key and installing the `rust-src` component, we can rerun our build command:
```
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
We see that `cargo build` now recompiles the `core`, `rustc-std-workspace-core` (a dependency of `compiler_builtins`), and `compiler_builtins` libraries for our custom target.
#### Memory-Related Intrinsics
The Rust compiler assumes that a certain set of built-in functions is available for all systems. Most of these functions are provided by the `compiler_builtins` crate that we just recompiled. However, there are some memory-related functions in that crate that are not enabled by default because they are normally provided by the C library on the system. These functions include `memset`, which sets all bytes in a memory block to a given value, `memcpy`, which copies one memory block to another, and `memcmp`, which compares two memory blocks. While we didn't need any of these functions to compile our kernel right now, they will be required as soon as we add some more code to it (e.g. when copying structs around).
Since we can't link to the C library of the operating system, we need an alternative way to provide these functions to the compiler. One possible approach for this could be to implement our own `memset` etc. functions and apply the `#[unsafe(no_mangle)]` attribute to them (to avoid the automatic renaming during compilation). However, this is dangerous since the slightest mistake in the implementation of these functions could lead to undefined behavior. For example, implementing `memcpy` with a `for` loop may result in an infinite recursion because `for` loops implicitly call the [`IntoIterator::into_iter`] trait method, which may call `memcpy` again. So it's a good idea to reuse existing, well-tested implementations instead.
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
Fortunately, the `compiler_builtins` crate already contains implementations for all the needed functions, they are just disabled by default to not collide with the implementations from the C library. We can enable them by setting cargo's [`build-std-features`] flag to `["compiler-builtins-mem"]`. Like the `build-std` flag, this flag can be either passed on the command line as a `-Z` flag or configured in the `unstable` table in the `.cargo/config.toml` file. Since we always want to build with this flag, the config file option makes more sense for us:
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
```
(Support for the `compiler-builtins-mem` feature was only [added very recently](https://github.com/rust-lang/rust/pull/77284), so you need at least Rust nightly `2020-09-30` for it.)
Behind the scenes, this flag enables the [`mem` feature] of the `compiler_builtins` crate. The effect of this is that the `#[unsafe(no_mangle)]` attribute is applied to the [`memcpy` etc. implementations] of the crate, which makes them available to the linker.
[`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
[`memcpy` etc. implementations]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
With this change, our kernel has valid implementations for all compiler-required functions, so it will continue to compile even if our code gets more complex.
#### Set a Default Target
To avoid passing the `--target` parameter on every invocation of `cargo build`, we can override the default target. To do this, we add the following to our [cargo configuration] file at `.cargo/config.toml`:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
This tells `cargo` to use our `x86_64-blog_os.json` target when no explicit `--target` argument is passed. This means that we can now build our kernel with a simple `cargo build`. For more information on cargo configuration options, check out the [official documentation][cargo configuration].
We are now able to build our kernel for a bare metal target with a simple `cargo build`. However, our `_start` entry point, which will be called by the boot loader, is still empty. It's time that we output something to screen from it.
### Printing to Screen
The easiest way to print text to the screen at this stage is the [VGA text buffer]. It is a special memory area mapped to the VGA hardware that contains the contents displayed on screen. It normally consists of 25 lines that each contain 80 character cells. Each character cell displays an ASCII character with some foreground and background colors. The screen output looks like this:
[VGA text buffer]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode

We will discuss the exact layout of the VGA buffer in the next post, where we write a first small driver for it. For printing “Hello World!”, we just need to know that the buffer is located at address `0xb8000` and that each character cell consists of an ASCII byte and a color byte.
The implementation looks like this:
```rust
static HELLO: &[u8] = b"Hello World!";
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
First, we cast the integer `0xb8000` into a [raw pointer]. Then we [iterate] over the bytes of the [static] `HELLO` [byte string]. We use the [`enumerate`] method to additionally get a running variable `i`. In the body of the for loop, we use the [`offset`] method to write the string byte and the corresponding color byte (`0xb` is a light cyan).
[iterate]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html
[static]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
[raw pointer]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
Note that there's an [`unsafe`] block around all memory writes. The reason is that the Rust compiler can't prove that the raw pointers we create are valid. They could point anywhere and lead to data corruption. By putting them into an `unsafe` block, we're basically telling the compiler that we are absolutely sure that the operations are valid. Note that an `unsafe` block does not turn off Rust's safety checks. It only allows you to do [five additional things].
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
[five additional things]: https://doc.rust-lang.org/stable/book/ch20-01-unsafe-rust.html#unsafe-superpowers
I want to emphasize that **this is not the way we want to do things in Rust!** It's very easy to mess up when working with raw pointers inside unsafe blocks. For example, we could easily write beyond the buffer's end if we're not careful.
So we want to minimize the use of `unsafe` as much as possible. Rust gives us the ability to do this by creating safe abstractions. For example, we could create a VGA buffer type that encapsulates all unsafety and ensures that it is _impossible_ to do anything wrong from the outside. This way, we would only need minimal amounts of `unsafe` code and can be sure that we don't violate [memory safety]. We will create such a safe VGA buffer abstraction in the next post.
[memory safety]: https://en.wikipedia.org/wiki/Memory_safety
## Running our Kernel
Now that we have an executable that does something perceptible, it is time to run it. First, we need to turn our compiled kernel into a bootable disk image by linking it with a bootloader. Then we can run the disk image in the [QEMU] virtual machine or boot it on real hardware using a USB stick.
### Creating a Bootimage
To turn our compiled kernel into a bootable disk image, we need to link it with a bootloader. As we learned in the [section about booting], the bootloader is responsible for initializing the CPU and loading our kernel.
[section about booting]: #the-boot-process
Instead of writing our own bootloader, which is a project on its own, we use the [`bootloader`] crate. This crate implements a basic BIOS bootloader without any C dependencies, just Rust and inline assembly. To use it for booting our kernel, we need to add a dependency on it:
[`bootloader`]: https://crates.io/crates/bootloader
```toml
# in Cargo.toml
[dependencies]
bootloader = "0.9"
```
**Note:** This post is only compatible with `bootloader v0.9`. Newer versions use a different build system and will result in build errors when following this post.
Adding the bootloader as a dependency is not enough to actually create a bootable disk image. The problem is that we need to link our kernel with the bootloader after compilation, but cargo has no support for [post-build scripts].
[post-build scripts]: https://github.com/rust-lang/cargo/issues/545
To solve this problem, we created a tool named `bootimage` that first compiles the kernel and bootloader, and then links them together to create a bootable disk image. To install the tool, go into your home directory (or any directory outside of your cargo project) and execute the following command in your terminal:
```
cargo install bootimage
```
For running `bootimage` and building the bootloader, you need to have the `llvm-tools-preview` rustup component installed. You can do so by executing `rustup component add llvm-tools-preview`.
After installing `bootimage` and adding the `llvm-tools-preview` component, you can create a bootable disk image by going back into your cargo project directory and executing:
```
> cargo bootimage
```
We see that the tool recompiles our kernel using `cargo build`, so it will automatically pick up any changes you make. Afterwards, it compiles the bootloader, which might take a while. Like all crate dependencies, it is only built once and then cached, so subsequent builds will be much faster. Finally, `bootimage` combines the bootloader and your kernel into a bootable disk image.
After executing the command, you should see a bootable disk image named `bootimage-blog_os.bin` in your `target/x86_64-blog_os/debug` directory. You can boot it in a virtual machine or copy it to a USB drive to boot it on real hardware. (Note that this is not a CD image, which has a different format, so burning it to a CD doesn't work).
#### How does it work?
The `bootimage` tool performs the following steps behind the scenes:
- It compiles our kernel to an [ELF] file.
- It compiles the bootloader dependency as a standalone executable.
- It links the bytes of the kernel ELF file to the bootloader.
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
When booted, the bootloader reads and parses the appended ELF file. It then maps the program segments to virtual addresses in the page tables, zeroes the `.bss` section, and sets up a stack. Finally, it reads the entry point address (our `_start` function) and jumps to it.
### Booting it in QEMU
We can now boot the disk image in a virtual machine. To boot it in [QEMU], execute the following command:
[QEMU]: https://www.qemu.org/
```
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
```
This opens a separate window which should look similar to this:

We see that our "Hello World!" is visible on the screen.
### Real Machine
It is also possible to write it to a USB stick and boot it on a real machine, **but be careful** to choose the correct device name, because **everything on that device is overwritten**:
```
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
```
Where `sdX` is the device name of your USB stick.
After writing the image to the USB stick, you can run it on real hardware by booting from it. You probably need to use a special boot menu or change the boot order in your BIOS configuration to boot from the USB stick. Note that it currently doesn't work for UEFI machines, since the `bootloader` crate has no UEFI support yet.
### Using `cargo run`
To make it easier to run our kernel in QEMU, we can set the `runner` configuration key for cargo:
```toml
# in .cargo/config.toml
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
```
The `target.'cfg(target_os = "none")'` table applies to all targets whose target configuration file's `"os"` field is set to `"none"`. This includes our `x86_64-blog_os.json` target. The `runner` key specifies the command that should be invoked for `cargo run`. The command is run after a successful build with the executable path passed as the first argument. See the [cargo documentation][cargo configuration] for more details.
The `bootimage runner` command is specifically designed to be usable as a `runner` executable. It links the given executable with the project's bootloader dependency and then launches QEMU. See the [Readme of `bootimage`] for more details and possible configuration options.
[Readme of `bootimage`]: https://github.com/rust-osdev/bootimage
Now we can use `cargo run` to compile our kernel and boot it in QEMU.
## What's next?
In the next post, we will explore the VGA text buffer in more detail and write a safe interface for it. We will also add support for the `println` macro.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/index.pt-BR.md
================================================
+++
title = "Um Kernel Rust Mínimo"
weight = 2
path = "pt-BR/minimal-rust-kernel"
date = 2018-02-10
[extra]
chapter = "O Básico"
# Please update this when updating the translation
translation_based_on_commit = "95d4fbd54c6b0e5a874981558c0cc1fe85d31606"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
Neste post, criamos um kernel Rust mínimo de 64 bits para a arquitetura x86. Construímos sobre o [binário Rust independente] do post anterior para criar uma imagem de disco inicializável que imprime algo na tela.
[binário Rust independente]: @/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-02`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[na parte inferior]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
## O Processo de Boot
Quando você liga um computador, ele começa a executar código de firmware que está armazenado na [ROM] da placa-mãe. Este código executa um [teste automático de inicialização], detecta a RAM disponível e pré-inicializa a CPU e o hardware. Depois, ele procura por um disco inicializável e começa a inicializar o kernel do sistema operacional.
[ROM]: https://en.wikipedia.org/wiki/Read-only_memory
[teste automático de inicialização]: https://en.wikipedia.org/wiki/Power-on_self-test
No x86, existem dois padrões de firmware: o "Basic Input/Output System" (**[BIOS]**) e o mais novo "Unified Extensible Firmware Interface" (**[UEFI]**). O padrão BIOS é antigo e ultrapassado, mas simples e bem suportado em qualquer máquina x86 desde os anos 1980. UEFI, em contraste, é mais moderno e tem muito mais recursos, mas é mais complexo de configurar (na minha opinião, pelo menos).
[BIOS]: https://en.wikipedia.org/wiki/BIOS
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
Atualmente, fornecemos apenas suporte para BIOS, mas suporte para UEFI também está planejado. Se você gostaria de nos ajudar com isso, confira o [issue no Github](https://github.com/phil-opp/blog_os/issues/349).
### Boot BIOS
Quase todos os sistemas x86 têm suporte para boot BIOS, incluindo máquinas mais novas baseadas em UEFI que usam um BIOS emulado. Isso é ótimo, porque você pode usar a mesma lógica de boot em todas as máquinas do último século. Mas essa ampla compatibilidade é ao mesmo tempo a maior desvantagem do boot BIOS, porque significa que a CPU é colocada em um modo de compatibilidade de 16 bits chamado [modo real] antes do boot, para que bootloaders arcaicos dos anos 1980 ainda funcionem.
Mas vamos começar do início:
Quando você liga um computador, ele carrega o BIOS de uma memória flash especial localizada na placa-mãe. O BIOS executa rotinas de teste automático e inicialização do hardware, então procura por discos inicializáveis. Se ele encontra um, o controle é transferido para seu _bootloader_, que é uma porção de 512 bytes de código executável armazenado no início do disco. A maioria dos bootloaders é maior que 512 bytes, então os bootloaders são comumente divididos em um primeiro estágio pequeno, que cabe em 512 bytes, e um segundo estágio, que é subsequentemente carregado pelo primeiro estágio.
O bootloader tem que determinar a localização da imagem do kernel no disco e carregá-la na memória. Ele também precisa mudar a CPU do [modo real] de 16 bits primeiro para o [modo protegido] de 32 bits, e então para o [modo longo] de 64 bits, onde registradores de 64 bits e a memória principal completa estão disponíveis. Seu terceiro trabalho é consultar certas informações (como um mapa de memória) do BIOS e passá-las ao kernel do SO.
[modo real]: https://en.wikipedia.org/wiki/Real_mode
[modo protegido]: https://en.wikipedia.org/wiki/Protected_mode
[modo longo]: https://en.wikipedia.org/wiki/Long_mode
[segmentação de memória]: https://en.wikipedia.org/wiki/X86_memory_segmentation
Escrever um bootloader é um pouco trabalhoso, pois requer linguagem assembly e muitos passos pouco intuitivos como "escrever este valor mágico neste registrador do processador". Portanto, não cobrimos a criação de bootloader neste post e em vez disso fornecemos uma ferramenta chamada [bootimage] que anexa automaticamente um bootloader ao seu kernel.
[bootimage]: https://github.com/rust-osdev/bootimage
Se você estiver interessado em construir seu próprio bootloader: Fique ligado, um conjunto de posts sobre este tópico já está planejado!
#### O Padrão Multiboot
Para evitar que todo sistema operacional implemente seu próprio bootloader, que é compatível apenas com um único SO, a [Free Software Foundation] criou um padrão de bootloader aberto chamado [Multiboot] em 1995. O padrão define uma interface entre o bootloader e o sistema operacional, para que qualquer bootloader compatível com Multiboot possa carregar qualquer sistema operacional compatível com Multiboot. A implementação de referência é o [GNU GRUB], que é o bootloader mais popular para sistemas Linux.
[Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation
[Multiboot]: https://wiki.osdev.org/Multiboot
[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB
Para tornar um kernel compatível com Multiboot, basta inserir um chamado [cabeçalho Multiboot] no início do arquivo do kernel. Isso torna muito fácil inicializar um SO a partir do GRUB. No entanto, o GRUB e o padrão Multiboot também têm alguns problemas:
[cabeçalho Multiboot]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
- Eles suportam apenas o modo protegido de 32 bits. Isso significa que você ainda tem que fazer a configuração da CPU para mudar para o modo longo de 64 bits.
- Eles são projetados para tornar o bootloader simples em vez do kernel. Por exemplo, o kernel precisa ser vinculado com um [tamanho de página padrão ajustado], porque o GRUB não consegue encontrar o cabeçalho Multiboot caso contrário. Outro exemplo é que as [informações de boot], que são passadas ao kernel, contêm muitas estruturas dependentes de arquitetura em vez de fornecer abstrações limpas.
- Tanto o GRUB quanto o padrão Multiboot são documentados apenas esparsamente.
- O GRUB precisa estar instalado no sistema host para criar uma imagem de disco inicializável a partir do arquivo do kernel. Isso torna o desenvolvimento no Windows ou Mac mais difícil.
[tamanho de página padrão ajustado]: https://wiki.osdev.org/Multiboot#Multiboot_2
[informações de boot]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
Por causa dessas desvantagens, decidimos não usar o GRUB ou o padrão Multiboot. No entanto, planejamos adicionar suporte Multiboot à nossa ferramenta [bootimage], para que seja possível carregar seu kernel em um sistema GRUB também. Se você estiver interessado em escrever um kernel compatível com Multiboot, confira a [primeira edição] desta série de blog.
[primeira edição]: @/edition-1/_index.md
### UEFI
(Não fornecemos suporte UEFI no momento, mas adoraríamos! Se você gostaria de ajudar, por favor nos diga no [issue do Github](https://github.com/phil-opp/blog_os/issues/349).)
## Um Kernel Mínimo
Agora que sabemos aproximadamente como um computador inicializa, é hora de criar nosso próprio kernel mínimo. Nosso objetivo é criar uma imagem de disco que imprima um "Hello World!" na tela quando inicializada. Fazemos isso estendendo o [binário Rust independente] do post anterior.
Como você deve se lembrar, construímos o binário independente através do `cargo`, mas dependendo do sistema operacional, precisávamos de nomes de ponto de entrada e flags de compilação diferentes. Isso ocorre porque o `cargo` compila para o _sistema host_ por padrão, ou seja, o sistema em que você está executando. Isso não é algo que queremos para nosso kernel, porque um kernel que executa em cima de, por exemplo, Windows, não faz muito sentido. Em vez disso, queremos compilar para um _sistema alvo_ claramente definido.
### Instalando o Rust Nightly
O Rust tem três canais de lançamento: _stable_, _beta_ e _nightly_. O Livro do Rust explica a diferença entre esses canais muito bem, então dê uma olhada [aqui](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). Para construir um sistema operacional, precisaremos de alguns recursos experimentais que estão disponíveis apenas no canal nightly, então precisamos instalar uma versão nightly do Rust.
Para gerenciar instalações do Rust, eu recomendo fortemente o [rustup]. Ele permite instalar compiladores nightly, beta e stable lado a lado e facilita a atualização deles. Com rustup, você pode usar um compilador nightly para o diretório atual executando `rustup override set nightly`. Alternativamente, você pode adicionar um arquivo chamado `rust-toolchain` com o conteúdo `nightly` ao diretório raiz do projeto. Você pode verificar que tem uma versão nightly instalada executando `rustc --version`: O número da versão deve conter `-nightly` no final.
[rustup]: https://www.rustup.rs/
O compilador nightly nos permite optar por vários recursos experimentais usando as chamadas _feature flags_ no topo do nosso arquivo. Por exemplo, poderíamos habilitar a [macro `asm!`] experimental para assembly inline adicionando `#![feature(asm)]` no topo do nosso `main.rs`. Note que tais recursos experimentais são completamente instáveis, o que significa que versões futuras do Rust podem alterá-los ou removê-los sem aviso prévio. Por esta razão, só os usaremos se absolutamente necessário.
[macro `asm!`]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
### Especificação de Alvo
O Cargo suporta diferentes sistemas alvo através do parâmetro `--target`. O alvo é descrito por uma chamada _[target triple]_, que descreve a arquitetura da CPU, o vendor, o sistema operacional e a [ABI]. Por exemplo, o target triple `x86_64-unknown-linux-gnu` descreve um sistema com uma CPU `x86_64`, sem vendor claro, e um sistema operacional Linux com a ABI GNU. O Rust suporta [muitos target triples diferentes][platform-support], incluindo `arm-linux-androideabi` para Android ou [`wasm32-unknown-unknown` para WebAssembly](https://www.hellorust.com/setup/wasm-target/).
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[ABI]: https://stackoverflow.com/a/2456882
[platform-support]: https://forge.rust-lang.org/release/platform-support.html
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
Para nosso sistema alvo, no entanto, precisamos de alguns parâmetros de configuração especiais (por exemplo, nenhum SO subjacente), então nenhum dos [target triples existentes][platform-support] se encaixa. Felizmente, o Rust nos permite definir [nosso próprio alvo][custom-targets] através de um arquivo JSON. Por exemplo, um arquivo JSON que descreve o target `x86_64-unknown-linux-gnu` se parece com isto:
```json
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
A maioria dos campos é exigida pelo LLVM para gerar código para aquela plataforma. Por exemplo, o campo [`data-layout`] define o tamanho de vários tipos integer, floating point e pointer. Então há campos que o Rust usa para compilação condicional, como `target-pointer-width`. O terceiro tipo de campo define como a crate deve ser construída. Por exemplo, o campo `pre-link-args` especifica argumentos passados ao [linker].
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
[linker]: https://en.wikipedia.org/wiki/Linker_(computing)
Também visamos sistemas `x86_64` com nosso kernel, então nossa especificação de alvo será muito similar à acima. Vamos começar criando um arquivo `x86_64-blog_os.json` (escolha qualquer nome que você goste) com o conteúdo comum:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true
}
```
Note que mudamos o SO no `llvm-target` e no campo `os` para `none`, porque executaremos em bare metal.
Adicionamos as seguintes entradas relacionadas à compilação:
```json
"linker-flavor": "ld.lld",
"linker": "rust-lld",
```
Em vez de usar o linker padrão da plataforma (que pode não suportar alvos Linux), usamos o linker multiplataforma [LLD] que vem com o Rust para vincular nosso kernel.
[LLD]: https://lld.llvm.org/
```json
"panic-strategy": "abort",
```
Esta configuração especifica que o alvo não suporta [stack unwinding] no panic, então em vez disso o programa deve abortar diretamente. Isso tem o mesmo efeito que a opção `panic = "abort"` no nosso Cargo.toml, então podemos removê-la de lá. (Note que, em contraste com a opção Cargo.toml, esta opção de alvo também se aplica quando recompilamos a biblioteca `core` mais adiante neste post. Então, mesmo se você preferir manter a opção Cargo.toml, certifique-se de incluir esta opção.)
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
```json
"disable-redzone": true,
```
Estamos escrevendo um kernel, então precisaremos lidar com interrupções em algum momento. Para fazer isso com segurança, temos que desabilitar uma certa otimização do ponteiro de stack chamada _"red zone"_, porque ela causaria corrupção do stack caso contrário. Para mais informações, veja nosso post separado sobre [desabilitando a red zone].
[desabilitando a red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
```json
"features": "-mmx,-sse,+soft-float",
```
O campo `features` habilita/desabilita recursos do alvo. Desabilitamos os recursos `mmx` e `sse` prefixando-os com um menos e habilitamos o recurso `soft-float` prefixando-o com um mais. Note que não deve haver espaços entre flags diferentes, caso contrário o LLVM falha ao interpretar a string de features.
Os recursos `mmx` e `sse` determinam suporte para instruções [Single Instruction Multiple Data (SIMD)], que frequentemente podem acelerar programas significativamente. No entanto, usar os grandes registradores SIMD em kernels de SO leva a problemas de desempenho. A razão é que o kernel precisa restaurar todos os registradores ao seu estado original antes de continuar um programa interrompido. Isso significa que o kernel tem que salvar o estado SIMD completo na memória principal em cada chamada de sistema ou interrupção de hardware. Como o estado SIMD é muito grande (512-1600 bytes) e interrupções podem ocorrer com muita frequência, essas operações adicionais de salvar/restaurar prejudicam consideravelmente o desempenho. Para evitar isso, desabilitamos SIMD para nosso kernel (não para aplicações executando em cima!).
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
Um problema com desabilitar SIMD é que operações de ponto flutuante em `x86_64` exigem registradores SIMD por padrão. Para resolver este problema, adicionamos o recurso `soft-float`, que emula todas as operações de ponto flutuante através de funções de software baseadas em inteiros normais.
Para mais informações, veja nosso post sobre [desabilitando SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
```json
"rustc-abi": "x86-softfloat"
```
Como queremos usar o recurso `soft-float`, também precisamos dizer ao compilador Rust `rustc` que queremos usar a ABI correspondente. Podemos fazer isso definindo o campo `rustc-abi` para `x86-softfloat`.
#### Juntando Tudo
Nosso arquivo de especificação de alvo agora se parece com isto:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float",
"rustc-abi": "x86-softfloat"
}
```
### Construindo nosso Kernel
Compilar para nosso novo alvo usará convenções Linux, já que o linker-flavor ld.lld instrui o llvm a compilar com a flag `-flavor gnu` (para mais opções de linker, veja [a documentação do rustc](https://doc.rust-lang.org/rustc/codegen-options/index.html#linker-flavor)). Isso significa que precisamos de um ponto de entrada chamado `_start` como descrito no [post anterior]:
[post anterior]: @/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md
```rust
// src/main.rs
#![no_std] // não vincule a biblioteca padrão do Rust
#![no_main] // desativar todos os pontos de entrada no nível Rust
use core::panic::PanicInfo;
/// Esta função é chamada em caso de pânico.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[unsafe(no_mangle)] // não altere (mangle) o nome desta função
pub extern "C" fn _start() -> ! {
// essa função é o ponto de entrada, já que o vinculador procura uma função
// denominado `_start` por padrão
loop {}
}
```
Note que o ponto de entrada precisa ser chamado `_start` independentemente do seu SO host.
Agora podemos construir o kernel para nosso novo alvo passando o nome do arquivo JSON como `--target`:
```
> cargo build --target x86_64-blog_os.json
error: `.json` target specs require -Zjson-target-spec
```
Falha! O erro nos diz que especificações de alvo JSON personalizadas são um recurso instável que requer habilitação explícita. Isso ocorre porque o formato dos arquivos JSON de alvo ainda não é considerado estável, então mudanças podem ocorrer em futuras versões do Rust. Consulte a [issue de rastreamento para especificações de alvo JSON personalizadas][json-target-spec-issue] para mais informações.
[json-target-spec-issue]: https://github.com/rust-lang/rust/issues/151528
#### A Opção `json-target-spec`
Para habilitar o suporte para especificações de alvo JSON personalizadas, precisamos criar um arquivo de [configuração cargo] local em `.cargo/config.toml` (a pasta `.cargo` deve estar ao lado da sua pasta `src`) com o seguinte conteúdo:
[configuração cargo]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# em .cargo/config.toml
[unstable]
json-target-spec = true
```
Isso habilita o recurso instável `json-target-spec`, permitindo-nos usar arquivos JSON de alvo personalizados.
Com esta configuração em vigor, vamos tentar construir novamente:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
```
Agora vemos um erro diferente! O erro nos diz que o compilador Rust não consegue mais encontrar a [biblioteca `core`]. Esta biblioteca contém tipos básicos do Rust como `Result`, `Option` e iteradores, e é implicitamente vinculada a todas as crates `no_std`.
[biblioteca `core`]: https://doc.rust-lang.org/nightly/core/index.html
O problema é que a biblioteca core é distribuída junto com o compilador Rust como uma biblioteca _pré-compilada_. Então ela é válida apenas para target triples host suportados (por exemplo, `x86_64-unknown-linux-gnu`) mas não para nosso alvo customizado. Se quisermos compilar código para outros alvos, precisamos recompilar `core` para esses alvos primeiro.
#### A Opção `build-std`
É aí que entra o [recurso `build-std`] do cargo. Ele permite recompilar `core` e outras crates da biblioteca padrão sob demanda, em vez de usar as versões pré-compiladas enviadas com a instalação do Rust. Este recurso é muito novo e ainda não está finalizado, então é marcado como "unstable" e disponível apenas em [compiladores Rust nightly].
[recurso `build-std`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[compiladores Rust nightly]: #instalando-o-rust-nightly
Para usar o recurso, precisamos adicionar o seguinte ao nosso arquivo de [configuração cargo] em `.cargo/config.toml`:
```toml
# em .cargo/config.toml
[unstable]
json-target-spec = true
build-std = ["core", "compiler_builtins"]
```
Isso diz ao cargo que ele deve recompilar as bibliotecas `core` e `compiler_builtins`. Esta última é necessária porque é uma dependência de `core`. Para recompilar essas bibliotecas, o cargo precisa de acesso ao código-fonte do rust, que podemos instalar com `rustup component add rust-src`.
**Nota:** A chave de configuração `unstable.build-std` requer pelo menos o Rust nightly de 15-07-2020.
Depois de definir a chave de configuração `unstable.build-std` e instalar o componente `rust-src`, podemos executar novamente nosso comando de compilação:
```
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
Vemos que `cargo build` agora recompila as bibliotecas `core`, `rustc-std-workspace-core` (uma dependência de `compiler_builtins`) e `compiler_builtins` para nosso alvo customizado.
#### Intrínsecos Relacionados a Memória
O compilador Rust assume que um certo conjunto de funções embutidas está disponível para todos os sistemas. A maioria dessas funções é fornecida pela crate `compiler_builtins` que acabamos de recompilar. No entanto, existem algumas funções relacionadas a memória nessa crate que não são habilitadas por padrão porque normalmente são fornecidas pela biblioteca C no sistema. Essas funções incluem `memset`, que define todos os bytes em um bloco de memória para um valor dado, `memcpy`, que copia um bloco de memória para outro, e `memcmp`, que compara dois blocos de memória. Embora não precisássemos de nenhuma dessas funções para compilar nosso kernel agora, elas serão necessárias assim que adicionarmos mais código a ele (por exemplo, ao copiar structs).
Como não podemos vincular à biblioteca C do sistema operacional, precisamos de uma maneira alternativa de fornecer essas funções ao compilador. Uma possível abordagem para isso poderia ser implementar nossas próprias funções `memset` etc. e aplicar o atributo `#[unsafe(no_mangle)]` a elas (para evitar a renomeação automática durante a compilação). No entanto, isso é perigoso, pois o menor erro na implementação dessas funções pode levar a undefined behavior. Por exemplo, implementar `memcpy` com um loop `for` pode resultar em recursão infinita porque loops `for` implicitamente chamam o método da trait [`IntoIterator::into_iter`], que pode chamar `memcpy` novamente. Então é uma boa ideia reutilizar implementações existentes e bem testadas em vez disso.
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
Felizmente, a crate `compiler_builtins` já contém implementações para todas as funções necessárias, elas estão apenas desabilitadas por padrão para não colidir com as implementações da biblioteca C. Podemos habilitá-las definindo a flag [`build-std-features`] do cargo para `["compiler-builtins-mem"]`. Como a flag `build-std`, esta flag pode ser passada na linha de comando como uma flag `-Z` ou configurada na tabela `unstable` no arquivo `.cargo/config.toml`. Como queremos sempre compilar com esta flag, a opção do arquivo de configuração faz mais sentido para nós:
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
```toml
# em .cargo/config.toml
[unstable]
json-target-spec = true
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
```
(O suporte para o recurso `compiler-builtins-mem` foi [adicionado muito recentemente](https://github.com/rust-lang/rust/pull/77284), então você precisa pelo menos do Rust nightly `2020-09-30` para ele.)
Nos bastidores, esta flag habilita o [recurso `mem`] da crate `compiler_builtins`. O efeito disso é que o atributo `#[unsafe(no_mangle)]` é aplicado às [implementações `memcpy` etc.] da crate, o que as torna disponíveis ao linker.
[recurso `mem`]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
[implementações `memcpy` etc.]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
Com esta mudança, nosso kernel tem implementações válidas para todas as funções exigidas pelo compilador, então ele continuará a compilar mesmo se nosso código ficar mais complexo.
#### Definir um Alvo Padrão
Para evitar passar o parâmetro `--target` em cada invocação de `cargo build`, podemos sobrescrever o alvo padrão. Para fazer isso, adicionamos o seguinte ao nosso arquivo de [configuração cargo] em `.cargo/config.toml`:
[configuração cargo]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# em .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
Isso diz ao `cargo` para usar nosso alvo `x86_64-blog_os.json` quando nenhum argumento `--target` explícito é passado. Isso significa que agora podemos construir nosso kernel com um simples `cargo build`. Para mais informações sobre opções de configuração do cargo, confira a [documentação oficial][configuração cargo].
Agora podemos construir nosso kernel para um alvo bare metal com um simples `cargo build`. No entanto, nosso ponto de entrada `_start`, que será chamado pelo bootloader, ainda está vazio. É hora de mostrar algo na tela a partir dele.
### Imprimindo na Tela
A maneira mais fácil de imprimir texto na tela neste estágio é o [buffer de texto VGA]. É uma área de memória especial mapeada para o hardware VGA que contém o conteúdo exibido na tela. Normalmente consiste em 25 linhas que cada uma contém 80 células de caractere. Cada célula de caractere exibe um caractere ASCII com algumas cores de primeiro plano e fundo. A saída da tela se parece com isto:
[buffer de texto VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode

Discutiremos o layout exato do buffer VGA no próximo post, onde escreveremos um primeiro pequeno driver para ele. Para imprimir "Hello World!", só precisamos saber que o buffer está localizado no endereço `0xb8000` e que cada célula de caractere consiste em um byte ASCII e um byte de cor.
A implementação se parece com isto:
```rust
static HELLO: &[u8] = b"Hello World!";
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
Primeiro, convertemos o inteiro `0xb8000` em um [ponteiro bruto]. Então [iteramos] sobre os bytes da [byte string] [static] `HELLO`. Usamos o método [`enumerate`] para obter adicionalmente uma variável em execução `i`. No corpo do loop for, usamos o método [`offset`] para escrever o byte da string e o byte de cor correspondente (`0xb` é um ciano claro).
[iterar]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html
[static]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
[ponteiro bruto]: https://doc.rust-lang.org/stable/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
Note que há um bloco [`unsafe`] em torno de todas as escritas de memória. A razão é que o compilador Rust não pode provar que os ponteiros brutos que criamos são válidos. Eles poderiam apontar para qualquer lugar e levar à corrupção de dados. Ao colocá-los em um bloco `unsafe`, estamos basicamente dizendo ao compilador que temos absoluta certeza de que as operações são válidas. Note que um bloco `unsafe` não desativa as verificações de segurança do Rust. Ele apenas permite que você faça [cinco coisas adicionais].
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
[cinco coisas adicionais]: https://doc.rust-lang.org/stable/book/ch20-01-unsafe-rust.html#unsafe-superpowers
Quero enfatizar que **esta não é a maneira como queremos fazer as coisas em Rust!** É muito fácil bagunçar ao trabalhar com ponteiros brutos dentro de blocos unsafe. Por exemplo, poderíamos facilmente escrever além do fim do buffer se não tivermos cuidado.
Então queremos minimizar o uso de `unsafe` o máximo possível. O Rust nos dá a capacidade de fazer isso criando abstrações seguras. Por exemplo, poderíamos criar um tipo de buffer VGA que encapsula toda a unsafety e garante que seja _impossível_ fazer algo errado de fora. Desta forma, precisaríamos apenas de quantidades mínimas de código `unsafe` e poderíamos ter certeza de que não violamos [memory safety]. Criaremos tal abstração de buffer VGA segura no próximo post.
[memory safety]: https://en.wikipedia.org/wiki/Memory_safety
## Executando nosso Kernel
Agora que temos um executável que faz algo perceptível, é hora de executá-lo. Primeiro, precisamos transformar nosso kernel compilado em uma imagem de disco inicializável vinculando-o com um bootloader. Então podemos executar a imagem de disco na máquina virtual [QEMU] ou inicializá-la em hardware real usando um pendrive USB.
### Criando uma Bootimage
Para transformar nosso kernel compilado em uma imagem de disco inicializável, precisamos vinculá-lo com um bootloader. Como aprendemos na [seção sobre boot], o bootloader é responsável por inicializar a CPU e carregar nosso kernel.
[seção sobre boot]: #o-processo-de-boot
Em vez de escrever nosso próprio bootloader, que é um projeto por si só, usamos a crate [`bootloader`]. Esta crate implementa um bootloader BIOS básico sem nenhuma dependência C, apenas Rust e assembly inline. Para usá-lo para inicializar nosso kernel, precisamos adicionar uma dependência nele:
[`bootloader`]: https://crates.io/crates/bootloader
```toml
# em Cargo.toml
[dependencies]
bootloader = "0.9"
```
**Nota:** Este post é compatível apenas com `bootloader v0.9`. Versões mais novas usam um sistema de compilação diferente e resultarão em erros de compilação ao seguir este post.
Adicionar o bootloader como uma dependência não é suficiente para realmente criar uma imagem de disco inicializável. O problema é que precisamos vincular nosso kernel com o bootloader após a compilação, mas o cargo não tem suporte para [scripts pós-compilação].
[scripts pós-compilação]: https://github.com/rust-lang/cargo/issues/545
Para resolver este problema, criamos uma ferramenta chamada `bootimage` que primeiro compila o kernel e o bootloader, e então os vincula juntos para criar uma imagem de disco inicializável. Para instalar a ferramenta, vá para seu diretório home (ou qualquer diretório fora do seu projeto cargo) e execute o seguinte comando no seu terminal:
```
cargo install bootimage
```
Para executar `bootimage` e construir o bootloader, você precisa ter o componente rustup `llvm-tools-preview` instalado. Você pode fazer isso executando `rustup component add llvm-tools-preview`.
Depois de instalar `bootimage` e adicionar o componente `llvm-tools-preview`, você pode criar uma imagem de disco inicializável voltando para o diretório do seu projeto cargo e executando:
```
> cargo bootimage
```
Vemos que a ferramenta recompila nosso kernel usando `cargo build`, então automaticamente pegará quaisquer mudanças que você fizer. Depois, ela compila o bootloader, o que pode demorar um pouco. Como todas as dependências de crate, ele é compilado apenas uma vez e então armazenado em cache, então compilações subsequentes serão muito mais rápidas. Finalmente, `bootimage` combina o bootloader e seu kernel em uma imagem de disco inicializável.
Após executar o comando, você deve ver uma imagem de disco inicializável chamada `bootimage-blog_os.bin` no seu diretório `target/x86_64-blog_os/debug`. Você pode inicializá-la em uma máquina virtual ou copiá-la para um pendrive USB para inicializá-la em hardware real. (Note que este não é uma imagem de CD, que tem um formato diferente, então gravá-la em um CD não funciona).
#### Como funciona?
A ferramenta `bootimage` executa os seguintes passos nos bastidores:
- Ela compila nosso kernel para um arquivo [ELF].
- Ela compila a dependência do bootloader como um executável autônomo.
- Ela vincula os bytes do arquivo ELF do kernel ao bootloader.
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
Quando inicializado, o bootloader lê e analisa o arquivo ELF anexado. Ele então mapeia os segmentos do programa para endereços virtuais nas tabelas de página, zera a seção `.bss` e configura um stack. Finalmente, ele lê o endereço do ponto de entrada (nossa função `_start`) e salta para ele.
### Inicializando no QEMU
Agora podemos inicializar a imagem de disco em uma máquina virtual. Para inicializá-la no [QEMU], execute o seguinte comando:
[QEMU]: https://www.qemu.org/
```
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
```
Isso abre uma janela separada que deve se parecer com isto:

Vemos que nosso "Hello World!" está visível na tela.
### Máquina Real
Também é possível escrevê-lo em um pendrive USB e inicializá-lo em uma máquina real, **mas tenha cuidado** para escolher o nome correto do dispositivo, porque **tudo naquele dispositivo será sobrescrito**:
```
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
```
Onde `sdX` é o nome do dispositivo do seu pendrive USB.
Depois de escrever a imagem no pendrive USB, você pode executá-la em hardware real inicializando a partir dele. Você provavelmente precisará usar um menu de boot especial ou alterar a ordem de boot na configuração do BIOS para inicializar a partir do pendrive USB. Note que atualmente não funciona para máquinas UEFI, já que a crate `bootloader` ainda não tem suporte UEFI.
### Usando `cargo run`
Para facilitar a execução do nosso kernel no QEMU, podemos definir a chave de configuração `runner` para o cargo:
```toml
# em .cargo/config.toml
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
```
A tabela `target.'cfg(target_os = "none")'` se aplica a todos os alvos cujo campo `"os"` do arquivo de configuração de alvo está definido como `"none"`. Isso inclui nosso alvo `x86_64-blog_os.json`. A chave `runner` especifica o comando que deve ser invocado para `cargo run`. O comando é executado após uma compilação bem-sucedida com o caminho do executável passado como o primeiro argumento. Veja a [documentação do cargo][configuração cargo] para mais detalhes.
O comando `bootimage runner` é especificamente projetado para ser utilizável como um executável `runner`. Ele vincula o executável dado com a dependência do bootloader do projeto e então lança o QEMU. Veja o [Readme do `bootimage`] para mais detalhes e opções de configuração possíveis.
[Readme do `bootimage`]: https://github.com/rust-osdev/bootimage
Agora podemos usar `cargo run` para compilar nosso kernel e inicializá-lo no QEMU.
## O que vem a seguir?
No próximo post, exploraremos o buffer de texto VGA em mais detalhes e escreveremos uma interface segura para ele. Também adicionaremos suporte para a macro `println`.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/index.ru.md
================================================
+++
title = "Минимально возможное ядро на Rust"
weight = 2
path = "ru/minimal-rust-kernel"
date = 2018-02-10
[extra]
translators = ["MrZloHex"]
+++
В этом посте мы создадим минимальное 64-битное ядро на Rust для архитектуры x86_64. Мы будем отталкиваться от [независимого бинарного файла][freestanding Rust binary] из предыдущего поста для создания загрузочного образа диска, который может что-то выводить на экран.
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.ru.md
Этот блог открыто разрабатывается на [GitHub]. Если у вас возникли какие-либо проблемы или вопросы, пожалуйста, создайте _issue_. Также вы можете оставлять комментарии [в конце страницы][at the bottom]. Полный исходный код для этого поста вы можете найти в репозитории в ветке [`post-02`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
## Последовательность процессов запуска {#the-boot-process}
Когда вы включаете компьютер, он начинает выполнять код микропрограммы, который хранится в [ПЗУ][ROM] материнской платы. Этот код выполняет [самотестирование при включении][power-on self-test], определяет доступную оперативную память и выполняет предварительную инициализацию процессора и аппаратного обеспечения. После этого он ищет загрузочный диск и начинает загрузку ядра операционной системы.
[ROM]: https://en.wikipedia.org/wiki/Read-only_memory
[power-on self-test]: https://en.wikipedia.org/wiki/Power-on_self-test
Для архитектуры x86 существует два стандарта прошивки: “Basic Input/Output System“ ("Базовая система ввода/вывода" **[BIOS]**) и более новый “Unified Extensible Firmware Interface” ("Унифицированный расширяемый интерфейс прошивки" **[UEFI]**). Стандарт BIOS - старый, но простой и хорошо поддерживаемый на любой машине x86 с 1980-х годов. UEFI, напротив, более современный и имеет гораздо больше возможностей, но более сложен в настройке (по крайней мере, на мой взгляд).
[BIOS]: https://en.wikipedia.org/wiki/BIOS
[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface
В данный момент, мы обеспечиваем поддержку только BIOS, но планируется поддержка и UEFI. Если вы хотите помочь нам в этом, обратитесь к [Github issue](https://github.com/phil-opp/blog_os/issues/349).
## Запуск BIOS
Почти все системы x86 имеют поддержку загрузки BIOS, включая более новые машины на базе UEFI, которые используют эмулированный BIOS. Это замечательно, потому что вы можете использовать одну и ту же логику загрузки на всех машинах из прошлых веков. Но такая широкая совместимость одновременно является и самым большим недостатком загрузки BIOS, поскольку это означает, что перед загрузкой процессор переводится в 16-битный режим совместимости под названием [реальный режим], чтобы архаичные загрузчики 1980-х годов все еще работали.
Но давайте начнем с самого начала:
Когда вы включаете компьютер, он загружает BIOS из специальной флэш-памяти, расположенной на материнской плате. BIOS запускает процедуры самодиагностики и инициализации оборудования, затем ищет загрузочные диски. Если он находит такой, управление передается _загрузчику_, который представляет собой 512-байтовую порцию исполняемого кода, хранящуюся в начале диска. Большинство загрузчиков имеют размер более 512 байт, поэтому загрузчики обычно разделяются на небольшой первый этап, который помещается в 512 байт, и второй этап, который впоследствии загружается первым этапом.
Загрузчик должен определить расположение образа ядра на диске и загрузить его в память. Он также должен переключить процессор из 16-битного [реального режима][real mode] сначала в 32-битный [защищенный режим][protected mode], а затем в 64-битный [длинный режим][long mode], где доступны 64-битные регистры и вся основная память. Третья задача - запросить определенную информацию (например, карту памяти) у BIOS и передать ее ядру ОС.
[real mode]: https://en.wikipedia.org/wiki/Real_mode
[protected mode]: https://en.wikipedia.org/wiki/Protected_mode
[long mode]: https://en.wikipedia.org/wiki/Long_mode
[memory segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation
Написание загрузчика немного громоздко, поскольку требует использования языка ассемблера и множества неинтересных действий, таких как "запишите это магическое значение в этот регистр процессора". Поэтому мы не рассматриваем создание загрузчика в этом посте и вместо этого предоставляем инструмент под названием [bootimage], который автоматически добавляет загрузчик к вашему ядру.
[bootimage]: https://github.com/rust-osdev/bootimage
Если вы заинтересованы в создании собственного загрузчика: Оставайтесь с нами, набор постов на эту тему уже запланирован!
#### Стандарт Multiboot
Чтобы избежать того, что каждая операционная система реализует свой собственный загрузчик, который совместим только с одной ОС, [Free Software Foundation] в 1995 году создал открытый стандарт загрузчика под названием [Multiboot]. Стандарт определяет интерфейс между загрузчиком и операционной системой, так что любой совместимый с Multiboot загрузчик может загружать любую совместимую с Multiboot операционную систему. Эталонной реализацией является [GNU GRUB], который является самым популярным загрузчиком для систем Linux.
[Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation
[Multiboot]: https://wiki.osdev.org/Multiboot
[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB
Чтобы сделать ядро совместимым с Multiboot, нужно просто вставить так называемый [Multiboot заголовок][Multiboot header] в начало файла ядра. Это делает загрузку ОС в GRUB очень простой. Однако у GRUB и стандарта Multiboot есть и некоторые проблемы:
[Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format
- Они поддерживают только 32-битный защищенный режим. Это означает, что для перехода на 64-битный длинный режим необходимо выполнить конфигурацию процессора.
- Они предназначены для того, чтобы упростить загрузчик вместо ядра. Например, ядро должно быть связано с [скорректированным размером страницы по умолчанию][adjusted default page size], потому что иначе GRUB не сможет найти заголовок Multiboot. Другой пример - [информация запуска][boot information], которая передается ядру, содержит множество структур, зависящих от архитектуры, вместо того, чтобы предоставлять чистые абстракции.
- И GRUB, и стандарт Multiboot документированы очень скудно.
- GRUB должен быть установлен на хост-системе, чтобы создать загрузочный образ диска из файла ядра. Это усложняет разработку под Windows или Mac.
[adjusted default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2
[boot information]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format
Из-за этих недостатков мы решили не использовать GRUB или стандарт Multiboot. Однако мы планируем добавить поддержку Multiboot в наш инструмент [bootimage], чтобы можно было загружать ваше ядро и на системе GRUB. Если вы заинтересованы в написании ядра, совместимого с Multiboot, ознакомьтесь с [первым выпуском][first edition] этой серии блогов.
[first edition]: @/edition-1/_index.md
### UEFI
(На данный момент мы не предоставляем поддержку UEFI, но мы бы хотели! Если вы хотите помочь, пожалуйста, сообщите нам об этом в [Github issue](https://github.com/phil-opp/blog_os/issues/349).)
## Минимально возможное ядро
Теперь, когда мы примерно знаем, как запускается компьютер, пришло время создать собственное минимально возможное ядро. Наша цель - создать образ диска, который при загрузке выводит на экран "Hello World!". Для этого мы будем используем [Независимый бинарный файл на Rust][freestanding Rust binary] из предыдущего поста.
Как вы помните, мы собирали независимый бинарный файл с помощью `cargo`, но в зависимости от операционной системы нам требовались разные имена точек входа и флаги компиляции. Это потому, что `cargo` по умолчанию компилирует для _хостовой системы_, то есть системы, на которой вы работаете. Это не то, что мы хотим для нашего ядра, потому что ядро, работающее поверх, например, Windows, не имеет особого смысла. Вместо этого мы хотим компилировать для четко определенной _целевой системы_.
### Установка 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 я настоятельно рекомендую [rustup]. Он позволяет устанавливать nightly, beta и stable компиляторы рядом друг с другом и облегчает их обновление. С помощью rustup вы можете использовать nightly компилятор для текущего каталога, выполнив команду `rustup override set nightly`. В качестве альтернативы вы можете добавить файл `rust-toolchain` с содержимым `nightly` в корневой каталог проекта. Вы можете проверить, установлена ли у вас версия nightly, выполнив команду `rustc --version`: Номер версии должен содержать `-nightly` в конце.
[rustup]: https://www.rustup.rs/
Nightly версия компилятора позволяет нам подключать различные экспериментальные возможности с помощью так называемых _флагов_ в верхней части нашего файла. Например, мы можем включить экспериментальный [макрос `asm!``asm!` macro] для встроенного ассемблера, добавив `#![feature(asm)]` в начало нашего `main.rs`. Обратите внимание, что такие экспериментальные возможности совершенно нестабильны, что означает, что будущие версии Rust могут изменить или удалить их без предварительного предупреждения. По этой причине мы будем использовать их только в случае крайней необходимости.
[`asm!` macro]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
### Спецификация целевой платформы
Cargo поддерживает различные целевые системы через параметр `--target`. Цель описывается так называемой тройкой _[target triple]_, которая описывает архитектуру процессора, производителя, операционную систему и [ABI]. Например, тройка целей `x86_64-unknown-linux-gnu` описывает систему с процессором `x86_64`, неизвестным поставщиком и операционной системой Linux с GNU ABI. Rust поддерживает [множество различных целевых троек][platform-support], включая `arm-linux-androideabi` для Android или [`wasm32-unknown-unknown` для WebAssembly](https://www.hellorust.com/setup/wasm-target/).
[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple
[ABI]: https://stackoverflow.com/a/2456882
[platform-support]: https://forge.rust-lang.org/release/platform-support.html
[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html
Однако для нашей целевой системы нам требуются некоторые специальные параметры конфигурации (например, отсутствие базовой ОС), поэтому ни одна из [существующих целевых троек][platform-support] не подходит. К счастью, Rust позволяет нам определить [custom target][custom-targets] через JSON-файл. Например, JSON-файл, описывающий цель `x86_64-unknown-linux-gnu`, выглядит следующим образом:
```json
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
Большинство полей требуется LLVM для генерации кода для данной платформы. Например, поле [`data-layout`] определяет размер различных типов целых чисел, чисел с плавающей точкой и указателей. Затем есть поля, которые Rust использует для условной компиляции, такие как `target-pointer-width`. Третий вид полей определяет, как должен быть собран крейт. Например, поле `pre-link-args` определяет аргументы, передаваемые [компоновщику][linker].
[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout
[linker]: https://en.wikipedia.org/wiki/Linker_(computing)
Для нашего ядра тоже нужна архитектура `x86_64`, поэтому наша спецификация цели будет очень похожа на приведенную выше. Начнем с создания файла `x86_64-blog_os.json` (выберите любое имя, которое вам нравится) с общим содержанием:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true
}
```
Обратите внимание, что мы изменили ОС в поле `llvm-target` и `os` на `none`, потому что мы будем работать на голом железе.
Добавляем дополнительные параметры для сборки ядра:
```json
"linker-flavor": "ld.lld",
"linker": "rust-lld",
```
Вместо того чтобы использовать компоновщик по умолчанию платформы (который может не поддерживать цели Linux), мы используем кроссплатформенный компоновщик [LLD], поставляемый вместе с Rust, для компоновки нашего ядра.
[LLD]: https://lld.llvm.org/
```json
"panic-strategy": "abort",
```
Этот параметр указывает, что цель не поддерживает [раскрутку стека][stack unwinding] при панике, поэтому вместо этого программа должна прерваться напрямую. Это имеет тот же эффект, что и опция `panic = "abort"` в нашем Cargo.toml, поэтому мы можем удалить ее оттуда. (Обратите внимание, что в отличие от опции Cargo.toml, эта опция также будет применяться, когда мы перекомпилируем библиотеку `core` позже в этом посте. Поэтому не забудьте добавить эту опцию, даже если вы предпочтете оставить опцию в Cargo.toml).
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
```json
"disable-redzone": true,
```
Мы пишем ядро, поэтому в какой-то момент нам понадобится обрабатывать прерывания. Чтобы сделать это безопасно, мы должны отключить определенную оптимизацию указателя стека, называемую _"красной зоной"_, поскольку в противном случае она приведет к повреждениям стека. Для получения дополнительной информации см. нашу отдельную статью об [отключении красной зоны][disabling the red zone].
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.ru.md
```json
"features": "-mmx,-sse,+soft-float",
```
Поле `features` включает/выключает функции целевой платформы. Мы отключаем функции `mmx` и `sse`, добавляя к ним минус, и включаем функцию `soft-float`, добавляя к ней плюс. Обратите внимание, что между разными флагами не должно быть пробелов, иначе LLVM не сможет интерпретировать строку features.
Функции `mmx` и `sse` определяют поддержку инструкций [Single Instruction Multiple Data (SIMD)], которые часто могут значительно ускорить работу программ. Однако использование больших регистров SIMD в ядрах ОС приводит к проблемам с производительностью. Причина в том, что ядру необходимо восстановить все регистры в исходное состояние перед продолжением прерванной программы. Это означает, что ядро должно сохранять полное состояние SIMD в основной памяти при каждом системном вызове или аппаратном прерывании. Поскольку состояние SIMD очень велико (512-1600 байт), а прерывания могут происходить очень часто, эти дополнительные операции сохранения/восстановления значительно снижают производительность. Чтобы избежать этого, мы отключили SIMD для нашего ядра (не для приложений, работающих поверх него!).
[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD
Проблема с отключением SIMD заключается в том, что операции с числами с плавающей точкой на `x86_64` по умолчанию требуют регистров SIMD. Чтобы решить эту проблему, мы добавили функцию `soft-float`, которая эмулирует все операции с числами с плавающей точкой через программные функции, основанные на обычных целых числах.
Для получения дополнительной информации см. наш пост об [отключении SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.ru.md).
```json
"rustc-abi": "x86-softfloat"
```
As we want to use the `soft-float` feature, we also need to tell the Rust compiler `rustc` that we want to use the corresponding ABI. We can do that by setting the `rustc-abi` field to `x86-softfloat`.
#### Соединяем все вместе
Наша спецификация целовой платформы выглядит следующим образом:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float",
"rustc-abi": "x86-softfloat"
}
```
### Компиляция ядра
Компиляция для нашей новой целевой платформы будет использовать соглашения Linux (я не совсем уверен почему — предполагаю, что это просто поведение LLVM по умолчанию). Это означает, что нам нужна точка входа с именем `_start`, как описано в [предыдущем посте][previous post]:
[previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.ru.md
```rust
// src/main.rs
#![no_std] // don't link the Rust standard library
#![no_main] // disable all Rust-level entry points
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[unsafe(no_mangle)] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
// this function is the entry point, since the linker looks for a function
// named `_start` by default
loop {}
}
```
Обратите внимание, что точка входа должна называться `_start` независимо от используемой вами ОС.
Теперь мы можем собрать ядро для нашей новой цели, передав имя файла JSON в качестве `--target`:
```
> cargo build --target x86_64-blog_os.json
error: `.json` target specs require -Zjson-target-spec
```
Не получается! Ошибка сообщает нам, что пользовательские спецификации целей JSON являются нестабильной функцией, требующей явной активации. Это связано с тем, что формат файлов JSON целей ещё не считается стабильным, поэтому в будущих версиях Rust могут произойти изменения. Дополнительную информацию см. в [issue отслеживания для пользовательских спецификаций целей JSON][json-target-spec-issue].
[json-target-spec-issue]: https://github.com/rust-lang/rust/issues/151528
### Опция `json-target-spec`
Чтобы включить поддержку пользовательских спецификаций целей JSON, нам нужно создать файл [конфигурации cargo][cargo configuration] по пути `.cargo/config.toml` (папка `.cargo` должна находиться рядом с папкой `src`) со следующим содержимым:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
```
Это включает нестабильную функцию `json-target-spec`, позволяя нам использовать пользовательские файлы целей JSON.
С этой конфигурацией попробуем собрать снова:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
```
Теперь мы видим другую ошибку! Ошибка сообщает нам, что компилятор Rust больше не может найти [библиотеку `core`][`core` library]. Эта библиотека содержит основные типы Rust, такие как `Result`, `Option` и итераторы, и неявно связана со всеми `no_std` модулями.
[`core` library]: https://doc.rust-lang.org/nightly/core/index.html
Проблема в том, что корневая (`core`) библиотека распространяется вместе с компилятором Rust как _прекомпилированная_ библиотека. Поэтому она действительна только для поддерживаемых тройных хостов (например, `x86_64-unknown-linux-gnu`), но не для нашей пользовательской целевой платформы. Если мы хотим скомпилировать код для других целевых платформ, нам нужно сначала перекомпилировать `core` для этих целей.
### Функция `build-std`
Вот тут-то и приходит на помощь функция [`build-std`][`build-std` feature] в cargo. Она позволяет перекомпилировать `core` и другие стандартные библиотеки по требованию, вместо того, чтобы использовать предварительно скомпилированные версии, поставляемые вместе с установкой Rust. Эта функция очень новая и еще не закончена, поэтому она помечена как "нестабильная" и доступна только на [nightly Rust].
[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[nightly Rust]: #installing-rust-nightly
Чтобы использовать эту функцию, нам нужно добавить следующее в файл [конфигурации cargo][cargo configuration] по пути `.cargo/config.toml`:
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
build-std = ["core", "compiler_builtins"]
```
Это говорит cargo, что он должен перекомпилировать библиотеки `core` и `compiler_builtins`. Последняя необходима, поскольку `core` зависит от неё. Чтобы перекомпилировать эти библиотеки, cargo нужен доступ к исходному коду rust, который мы можем установить с помощью команды `rustup component add rust-src`.
**Note:** Ключ конфигурации `unstable.build-std` требует как минимум Rust nightly от 2020-07-15.
```
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
Мы видим, что `cargo build` теперь перекомпилирует библиотеки `core`, `rustc-std-workspace-core` (зависимость от `compiler_builtins`) и `compiler_builtins` для нашей пользовательской целевой платформы.
#### Внутренние функции, работающие с памятью
Компилятор Rust предполагает, что определенный набор встроенных функций доступен для всех систем. Большинство этих функций обеспечивается модулем `compiler_builtins`, который мы только что перекомпилировали. Однако в этом модуле есть некоторые функции, связанные с памятью, которые не включены по умолчанию, потому что они обычно предоставляются библиотекой C в системе. Эти функции включают `memset`, которая устанавливает все байты в блоке памяти в заданное значение, `memcpy`, которая копирует один блок памяти в другой, и `memcmp`, которая сравнивает два блока памяти. Хотя ни одна из этих функций нам сейчас не понадобилась для компиляции нашего ядра, они потребуются, как только мы добавим в него дополнительный код (например, при копировании структур).
Поскольку мы не можем ссылаться на С библиотеку хостовой операционной системы, нам нужен альтернативный способ предоставления этих функций компилятору. Одним из возможных подходов для этого может быть реализация наших собственных функций `memset` и т.д. и применение к ним атрибута `#[unsafe(no_mangle)]` (чтобы избежать автоматического переименования во время компиляции). Однако это опасно, поскольку малейшая ошибка в реализации этих функций может привести к неопределенному поведению. Например, при реализации `memcpy` с помощью цикла `for` вы можете получить бесконечную рекурсию, поскольку циклы `for` неявно вызывают метод трейта [`IntoIterator::into_iter`], который может снова вызвать `memcpy`. Поэтому хорошей идеей будет повторное использование существующих, хорошо протестированных реализаций.
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
К счастью, модуль `compiler_builtins` уже содержит реализации всех необходимых функций, они просто отключены по умолчанию, чтобы не столкнуться с реализациями из С библиотеки. Мы можем включить их, установив флаг cargo [`build-std-features`] на `["compiler-builtins-mem"]`. Как и флаг `build-std`, этот флаг может быть передан в командной строке как флаг `-Z` или настроен в таблице `unstable` в файле `.cargo/config.toml`. Поскольку мы всегда хотим собирать с этим флагом, вариант с конфигурационным файлом имеет для нас больше смысла:
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
```
(Поддержка функции `compiler-builtins-mem` была [добавлена совсем недавно](https://github.com/rust-lang/rust/pull/77284), поэтому для нее вам нужен как минимум Rust nightly `2020-09-30`).
За кулисами этот флаг включает функцию [`mem`][`mem` feature] крейта `compiler_builtins`. Это приводит к тому, что атрибут `#[unsafe(no_mangle)]` применяется к [реализациям `memcpy` и т.п.][`memcpy` etc. implementations] из этого крейта, что делает их доступными для компоновщика.
[`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
[`memcpy` etc. implementations]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
Благодаря этому изменению наше ядро имеет валидные реализации для всех функций, требуемых компилятором, поэтому оно будет продолжать компилироваться, даже если наш код станет сложнее.
#### Переопределение цели по умолчанию
Чтобы избежать передачи параметра `--target` при каждом вызове `cargo build`, мы можем переопределить цель по умолчанию. Для этого мы добавим следующее в наш файл [конфигураций cargo][cargo configuration] по пути `.cargo/config.toml`:
```toml
# in .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
С этой конфигурацией `cargo` будет использовать нашу цель `x86_64-blog_os.json`, если не передан явный аргумент `--target`. Это означает, что теперь мы можем собрать наше ядро с помощью простой `cargo build`. Чтобы узнать больше о параметрах конфигурации cargo, ознакомьтесь с [официальной документацией][cargo configuration].
Теперь мы можем скомпилировать наше ядро под голое железо с помощью простой `cargo build`. Однако наша точка входа `_start`, которая будет вызываться загрузчиком, все еще пуста. Пришло время вывести что-нибудь на экран.
### Вывод на экран
Самым простым способом печати текста на экран на данном этапе является [текстовый буфер VGA][VGA text buffer]. Это специальная область памяти, сопоставленная с аппаратным обеспечением VGA, которая содержит содержимое, отображаемое на экране. Обычно он состоит из 25 строк, каждая из которых содержит 80 символьных ячеек. Каждая символьная ячейка отображает символ ASCII с некоторыми цветами переднего и заднего плана. Вывод на экран выглядит следующим образом:
[VGA text buffer]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode

Точную разметку буфера VGA мы обсудим в следующем посте, где мы напишем первый небольшой драйвер для него. Для печати "Hello World!" нам достаточно знать, что буфер расположен по адресу `0xb8000` и что каждая символьная ячейка состоит из байта ASCII и байта цвета.
Реализация выглядит следующим образом:
```rust
static HELLO: &[u8] = b"Hello World!";
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
Сначала мы приводим целое число `0xb8000` к [сырому указателю][raw pointer]. Затем мы [итерируем][iterate] по байтам [статической][static] [байтовой строки][byte string] `HELLO`. Мы используем метод [`enumerate`], чтобы дополнительно получить бегущую переменную `i`. В теле цикла for мы используем метод [`offset`] для записи байта строки и соответствующего байта цвета (`0xb` - светло-голубой).
[iterate]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html
[static]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals
[raw pointer]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
Обратите внимание, что вокруг всех записей в память стоит блок [`unsafe`]. Причина в том, что компилятор Rust не может доказать, что сырые указатели, которые мы создаем, действительны. Они могут указывать куда угодно и привести к повреждению данных. Помещая их в блок `unsafe`, мы, по сути, говорим компилятору, что абсолютно уверены в правильности операций. Обратите внимание, что блок `unsafe` не отключает проверки безопасности Rust. Он только позволяет вам делать [пять дополнительных вещей][five additional things].
[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html
[five additional things]: https://doc.rust-lang.org/stable/book/ch20-01-unsafe-rust.html#unsafe-superpowers
Я хочу подчеркнуть, что **это не тот способ, которым стоит что-либо делать в Rust!** Очень легко ошибиться при работе с сырыми указателями внутри блоков `unsafe`: например, мы можем легко записать за конец буфера, если не будем осторожны.
Поэтому мы хотим минимизировать использование `unsafe` настолько, насколько это возможно. Rust дает нам возможность сделать это путем создания безопасных абстракций. Например, мы можем создать тип буфера VGA, который инкапсулирует всю небезопасность и гарантирует, что извне _невозможно_ сделать что-либо неправильно. Таким образом, нам понадобится лишь минимальное количество блоков `unsafe` и мы можем быть уверены, что не нарушаем [безопасность памяти][memory safety]. Мы создадим такую безопасную абстракцию буфера VGA в следующем посте.
[memory safety]: https://en.wikipedia.org/wiki/Memory_safety
## Запуск ядра
Теперь, когда у нас есть исполняемый файл, который делает что-то ощутимое, пришло время запустить его. Сначала нам нужно превратить наше скомпилированное ядро в загрузочный образ диска, связав его с загрузчиком. Затем мы можем запустить образ диска в виртуальной машине [QEMU] или загрузить его на реальном оборудовании с помощью USB-носителя.
### Создание загрузочного образа
Чтобы превратить наше скомпилированное ядро в загрузочный образ диска, нам нужно связать его с загрузчиком. Как мы узнали в [разделе о загрузке], загрузчик отвечает за инициализацию процессора и загрузку нашего ядра.
[разделе о загрузке]: #the-boot-process
Вместо того чтобы писать собственный загрузчик, который является самостоятельным проектом, мы используем модуль [`bootloader`]. Этот модуль реализует базовый BIOS-загрузчик без каких-либо C-зависимостей, только Rust и встроенный ассемблер. Чтобы использовать его для загрузки нашего ядра, нам нужно добавить зависимость от него:
[`bootloader`]: https://crates.io/crates/bootloader
```toml
# in Cargo.toml
[dependencies]
bootloader = "0.9"
```
Добавление загрузчика в качестве зависимости недостаточно для создания загрузочного образа диска. Проблема в том, что нам нужно связать наше ядро с загрузчиком после компиляции, но в cargo нет поддержки [скриптов после сборки][post-build scripts].
[post-build scripts]: https://github.com/rust-lang/cargo/issues/545
Для решения этой проблемы мы создали инструмент `bootimage`, который сначала компилирует ядро и загрузчик, а затем соединяет их вместе для создания загрузочного образа диска. Чтобы установить инструмент, выполните следующую команду в терминале:
```
cargo install bootimage
```
Для запуска `bootimage` и сборки загрузчика вам необходимо установить компонент rustup `llvm-tools-preview`. Это можно сделать, выполнив команду `rustup component add llvm-tools-preview`.
После установки `bootimage` и добавления компонента `llvm-tools-preview` мы можем создать образ загрузочного диска, выполнив команду:
```
> cargo bootimage
```
Мы видим, что инструмент перекомпилирует наше ядро с помощью `cargo build`, поэтому он автоматически подхватит все внесенные вами изменения. После этого он компилирует загрузчик, что может занять некоторое время. Как и все зависимости модулей, он собирается только один раз, а затем кэшируется, поэтому последующие сборки будут происходить гораздо быстрее. Наконец, `bootimage` объединяет загрузчик и ваше ядро в загрузочный образ диска.
После выполнения команды вы должны увидеть загрузочный образ диска с именем `bootimage-blog_os.bin` в каталоге `target/x86_64-blog_os/debug`. Вы можете загрузить его в виртуальной машине или скопировать на USB-накопитель, чтобы загрузить его на реальном оборудовании. (Обратите внимание, что это не образ CD, который имеет другой формат, поэтому запись на CD не работает).
#### Как этот работает?
Инструмент `bootimage` выполняет следующие действия за кулисами:
- Компилирует наше ядро в файл [ELF].
- Компилирует зависимость загрузчика как отдельный исполняемый файл.
- Он связывает байты ELF-файла ядра с загрузчиком.
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader
При запуске загрузчик читает и разбирает приложенный файл ELF. Затем он сопоставляет сегменты программы с виртуальными адресами в таблицах страниц, обнуляет секцию `.bss` и устанавливает стек. Наконец, он считывает адрес точки входа (наша функция `_start`) и переходит к ней.
### Запуск через QEMU
Теперь мы можем загрузить образ диска в виртуальной машине. Чтобы загрузить его в [QEMU], выполните следующую команду:
[QEMU]: https://www.qemu.org/
```
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
```
Откроется отдельное окно, которое выглядит следующим образом:

Мы видим, что наш "Hello World!" отображается на экране.
### Настоящая машина
Также можно записать его на USB-накопитель и загрузить на реальной машине:
```
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
```
Где `sdX` - имя устройства вашего USB-накопителя. **Внимательно проверьте**, что вы выбрали правильное имя устройства, потому что все, что находится на этом устройстве, будет перезаписано.
После записи образа на USB-накопитель его можно запустить на реальном оборудовании, загрузившись с него. Для загрузки с USB-накопителя вам, вероятно, потребуется использовать специальное меню загрузки или изменить порядок загрузки в конфигурации BIOS. Обратите внимание, что в настоящее время это не работает на машинах с UEFI, так как модуль `bootloader` пока не имеет поддержки UEFI.
### Использование `cargo run`
Чтобы облегчить запуск нашего ядра в QEMU, мы можем установить ключ конфигурации `runner` для cargo:
```toml
# in .cargo/config.toml
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
```
Таблица `target.'cfg(target_os = "none")'` применяется ко всем целям, которые установили поле `"os"` своего конфигурационного файла цели на `"none"`. Это включает нашу цель `x86_64-blog_os.json`. Ключ `runner` указывает команду, которая должна быть вызвана для `cargo run`. Команда запускается после успешной сборки с путем к исполняемому файлу, переданному в качестве первого аргумента. Более подробную информацию смотрите в [документации по cargo][cargo configuration].
Команда `bootimage runner` специально разработана для использования в качестве исполняемого файла `runner`. Она связывает заданный исполняемый файл с зависимостью загрузчика проекта, а затем запускает QEMU. Более подробную информацию и возможные варианты конфигурации смотрите в [Readme of `bootimage`].
[Readme of `bootimage`]: https://github.com/rust-osdev/bootimage
Теперь мы можем использовать `cargo run` для компиляции нашего ядра и его загрузки в QEMU.
## Что дальше?
В следующем посте мы более подробно рассмотрим текстовый буфер VGA и напишем безопасный интерфейс для него. Мы также добавим поддержку макроса `println`.
================================================
FILE: blog/content/edition-2/posts/02-minimal-rust-kernel/index.zh-CN.md
================================================
+++
title = "最小内核"
weight = 2
path = "zh-CN/minimal-rust-kernel"
date = 2018-02-10
[extra]
# Please update this when updating the translation
translation_based_on_commit = "096c044b4f3697e91d8e30a2e817e567d0ef21a2"
# GitHub usernames of the people that translated this post
translators = ["luojia65", "Rustin-Liu", "liuyuran"]
# GitHub usernames of the people that contributed to this translation
translation_contributors = ["JiangengDong"]
+++
在这篇文章中,我们将基于 **x86架构**(the x86 architecture),使用 Rust 语言,编写一个最小化的 64 位内核。我们将从上一章中构建的[独立式可执行程序][freestanding-rust-binary]开始,构建自己的内核;它将向显示器打印字符串,并能被打包为一个能够引导启动的**磁盘映像**(disk image)。
[freestanding-rust-binary]: @/edition-2/posts/01-freestanding-rust-binary/index.md
此博客在 [GitHub] 上公开开发. 如果您有任何问题或疑问,请在此处打开一个 issue。 您也可以在[底部][at the bottom]发表评论. 这篇文章的完整源代码可以在 [`post-02`] [post branch] 分支中找到。
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
## 引导启动
当我们启动电脑时,主板 [ROM](https://en.wikipedia.org/wiki/Read-only_memory)内存储的**固件**(firmware)将会运行:它将负责电脑的**加电自检**([power-on self test](https://en.wikipedia.org/wiki/Power-on_self-test)),**可用内存**(available RAM)的检测,以及 CPU 和其它硬件的预加载。这之后,它将寻找一个**可引导的存储介质**(bootable disk),并开始引导启动其中的**内核**(kernel)。
x86 架构支持两种固件标准: **BIOS**([Basic Input/Output System](https://en.wikipedia.org/wiki/BIOS))和 **UEFI**([Unified Extensible Firmware Interface](https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface))。其中,BIOS 标准显得陈旧而过时,但实现简单,并为 1980 年代后的所有 x86 设备所支持;相反地,UEFI 更现代化,功能也更全面,但开发和构建更复杂(至少从我的角度看是如此)。
在这篇文章中,我们暂时只提供 BIOS 固件的引导启动方式,但是UEFI支持也已经在计划中了。如果你希望帮助我们推进它,请查阅这份 [Github issue](https://github.com/phil-opp/blog_os/issues/349)。
### BIOS 启动
几乎所有的 x86 硬件系统都支持 BIOS 启动,这也包含新型的、基于 UEFI、用**模拟 BIOS**(emulated BIOS)的方式向后兼容的硬件系统。这可以说是一件好事情,因为无论是上世纪还是现在的硬件系统,你都只需编写同样的引导启动逻辑;但这种兼容性有时也是 BIOS 引导启动最大的缺点,因为这意味着在系统启动前,你的 CPU 必须先进入一个 16 位系统兼容的**实模式**([real mode](https://en.wikipedia.org/wiki/Real_mode)),这样 1980 年代古老的引导固件才能够继续使用。
让我们从头开始,理解一遍 BIOS 启动的过程。
当电脑启动时,主板上特殊的闪存中存储的 BIOS 固件将被加载。BIOS 固件将会加电自检、初始化硬件,然后它将寻找一个可引导的存储介质。如果找到了,那电脑的控制权将被转交给**引导程序**(bootloader):一段存储在存储介质的开头的、512字节长度的程序片段。大多数的引导程序长度都大于512字节——所以通常情况下,引导程序都被切分为一段优先启动、长度不超过512字节、存储在介质开头的**第一阶段引导程序**(first stage bootloader),和一段随后由其加载的、长度可能较长、存储在其它位置的**第二阶段引导程序**(second stage bootloader)。
引导程序必须决定内核的位置,并将内核加载到内存。引导程序还需要将 CPU 从 16 位的实模式,先切换到 32 位的**保护模式**([protected mode](https://en.wikipedia.org/wiki/Protected_mode)),最终切换到 64 位的**长模式**([long mode](https://en.wikipedia.org/wiki/Long_mode)):此时,所有的 64 位寄存器和整个**主内存**(main memory)才能被访问。引导程序的第三个作用,是从 BIOS 查询特定的信息,并将其传递到内核;如查询和传递**内存映射表**(memory map)。
编写一个引导程序并不是一个简单的任务,因为这需要使用汇编语言,而且必须经过许多意图并不明显的步骤——比如,把一些**魔术数字**(magic number)写入某个寄存器。因此,我们不会讲解如何编写自己的引导程序,而是推荐 [bootimage 工具](https://github.com/rust-osdev/bootimage)——它能够自动并且方便地为你的内核准备一个引导程序。
### Multiboot 标准
每个操作系统都实现自己的引导程序,而这只对单个操作系统有效。为了避免这样的僵局,1995 年,**自由软件基金会**([Free Software Foundation](https://en.wikipedia.org/wiki/Free_Software_Foundation))颁布了一个开源的引导程序标准——[Multiboot](https://wiki.osdev.org/Multiboot)。这个标准定义了引导程序和操作系统间的统一接口,所以任何适配 Multiboot 的引导程序,都能用来加载任何同样适配了 Multiboot 的操作系统。[GNU GRUB](https://en.wikipedia.org/wiki/GNU_GRUB) 是一个可供参考的 Multiboot 实现,它也是最热门的Linux系统引导程序之一。
要编写一款适配 Multiboot 的内核,我们只需要在内核文件开头,插入被称作 **Multiboot头**([Multiboot header](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format))的数据片段。这让 GRUB 很容易引导任何操作系统,但是,GRUB 和 Multiboot 标准也有一些可预知的问题:
1. 它们只支持 32 位的保护模式。这意味着,在引导之后,你依然需要配置你的 CPU,让它切换到 64 位的长模式;
2. 它们被设计为精简引导程序,而不是精简内核。举个例子,内核需要以调整过的**默认页长度**([default page size](https://wiki.osdev.org/Multiboot#Multiboot_2))被链接,否则 GRUB 将无法找到内核的 Multiboot 头。另一个例子是**引导信息**([boot information](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format)),这个包含着大量与架构有关的数据,会在引导启动时,被直接传到操作系统,而不会经过一层清晰的抽象;
3. GRUB 和 Multiboot 标准并没有被详细地解释,阅读相关文档需要一定经验;
4. 为了创建一个能够被引导的磁盘映像,我们在开发时必须安装 GRUB:这加大了基于 Windows 或 macOS 开发内核的难度。
出于这些考虑,我们决定不使用 GRUB 或者 Multiboot 标准。然而,Multiboot 支持功能也在 bootimage 工具的开发计划之中,所以从原理上讲,如果选用 bootimage 工具,在未来使用 GRUB 引导你的系统内核是可能的。 如果你对编写一个支持 Mutiboot 标准的内核有兴趣,可以查阅 [初版文档][first edition]。
[first edition]: @/edition-1/_index.md
### UEFI
(截至此时,我们并未提供UEFI相关教程,但我们确实有此意向。如果你愿意提供一些帮助,请在 [Github issue](https://github.com/phil-opp/blog_os/issues/349) 告知我们,不胜感谢。)
## 最小内核
现在我们已经明白电脑是如何启动的,那也是时候编写我们自己的内核了。我们的小目标是,创建一个内核的磁盘映像,它能够在启动时,向屏幕输出一行“Hello World!”;我们的工作将基于上一章构建的[独立式可执行程序][freestanding-rust-binary]。
如果读者还有印象的话,在上一章,我们使用 `cargo` 构建了一个独立的二进制程序;但这个程序依然基于特定的操作系统平台:因平台而异,我们需要定义不同名称的函数,且使用不同的编译指令。这是因为在默认情况下,`cargo` 会为特定的**宿主系统**(host system)构建源码,比如为你正在运行的系统构建源码。这并不是我们想要的,因为我们的内核不应该基于另一个操作系统——我们想要编写的,就是这个操作系统。确切地说,我们想要的是,编译为一个特定的**目标系统**(target system)。
## 安装 Nightly Rust
Rust 语言有三个**发行频道**(release channel),分别是 stable、beta 和 nightly。《Rust 程序设计语言》中对这三个频道的区别解释得很详细,可以前往[这里](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html)看一看。为了搭建一个操作系统,我们需要一些只有 nightly 会提供的实验性功能,所以我们需要安装一个 nightly 版本的 Rust。
要管理安装好的 Rust,我强烈建议使用 [rustup](https://www.rustup.rs/):它允许你同时安装 nightly、beta 和 stable 版本的编译器,而且让更新 Rust 变得容易。你可以输入 `rustup override add nightly` 来选择在当前目录使用 nightly 版本的 Rust。或者,你也可以在项目根目录添加一个名称为 `rust-toolchain`、内容为 `nightly` 的文件。要检查你是否已经安装了一个 nightly,你可以运行 `rustc --version`:返回的版本号末尾应该包含`-nightly`。
Nightly 版本的编译器允许我们在源码的开头插入**特性标签**(feature flag),来自由选择并使用大量实验性的功能。举个例子,要使用实验性的[内联汇编(asm!宏)][asm feature],我们可以在 `main.rs` 的顶部添加 `#![feature(asm)]`。要注意的是,这样的实验性功能**不稳定**(unstable),意味着未来的 Rust 版本可能会修改或移除这些功能,而不会有预先的警告过渡。因此我们只有在绝对必要的时候,才应该使用这些特性。
[asm feature]: https://doc.rust-lang.org/stable/reference/inline-assembly.html
### 目标配置清单
通过 `--target` 参数,`cargo` 支持不同的目标系统。这个目标系统可以使用一个**目标三元组**([target triple](https://clang.llvm.org/docs/CrossCompilation.html#target-triple))来描述,它描述了 CPU 架构、平台供应者、操作系统和**应用程序二进制接口**([Application Binary Interface, ABI](https://stackoverflow.com/a/2456882))。比方说,目标三元组` x86_64-unknown-linux-gnu` 描述一个基于 `x86_64` 架构 CPU 的、没有明确的平台供应者的 linux 系统,它遵循 GNU 风格的 ABI。Rust 支持[许多不同的目标三元组](https://forge.rust-lang.org/release/platform-support.html),包括安卓系统对应的 `arm-linux-androideabi` 和 [WebAssembly使用的wasm32-unknown-unknown](https://www.hellorust.com/setup/wasm-target/)。
为了编写我们的目标系统,并且鉴于我们需要做一些特殊的配置(比如没有依赖的底层操作系统),[已经支持的目标三元组](https://forge.rust-lang.org/release/platform-support.html)都不能满足我们的要求。幸运的是,只需使用一个 JSON 文件,Rust 便允许我们定义自己的目标系统;这个文件常被称作**目标配置清单**(target specification)。比如,一个描述 `x86_64-unknown-linux-gnu` 目标系统的配置清单大概长这样:
```json
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
一个配置清单中包含多个**配置项**(field)。大多数的配置项都是 LLVM 需求的,它们将配置为特定平台生成的代码。打个比方,`data-layout` 配置项定义了不同的整数、浮点数、指针类型的长度;另外,还有一些 Rust 用作条件编译的配置项,如 `target-pointer-width`。还有一些类型的配置项,定义了这个包该如何被编译,例如,`pre-link-args` 配置项指定了应该向**链接器**([linker](https://en.wikipedia.org/wiki/Linker_(computing)))传入的参数。
我们将把我们的内核编译到 `x86_64` 架构,所以我们的配置清单将和上面的例子相似。现在,我们来创建一个名为 `x86_64-blog_os.json` 的文件——当然也可以选用自己喜欢的文件名——里面包含这样的内容:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true
}
```
需要注意的是,因为我们要在**裸机**(bare metal)上运行内核,我们已经修改了 `llvm-target` 的内容,并将 `os` 配置项的值改为 `none`。
我们还需要添加下面与编译相关的配置项:
```json
"linker-flavor": "ld.lld",
"linker": "rust-lld",
```
在这里,我们不使用平台默认提供的链接器,因为它可能不支持 Linux 目标系统。为了链接我们的内核,我们使用跨平台的 **LLD链接器**([LLD linker](https://lld.llvm.org/)),它是和 Rust 一起打包发布的。
```json
"panic-strategy": "abort",
```
这个配置项的意思是,我们的编译目标不支持 panic 时的**栈展开**([stack unwinding](https://www.bogotobogo.com/cplusplus/stackunwinding.php)),所以我们选择直接**在 panic 时中止**(abort on panic)。这和在 `Cargo.toml` 文件中添加 `panic = "abort"` 选项的作用是相同的,所以我们可以不在这里的配置清单中填写这一项。
```json
"disable-redzone": true,
```
我们正在编写一个内核,所以我们迟早要处理中断。要安全地实现这一点,我们必须禁用一个与**红区**(redzone)有关的栈指针优化:因为此时,这个优化可能会导致栈被破坏。如果需要更详细的资料,请查阅我们的一篇关于 [禁用红区][disabling the red zone] 的短文。
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.zh-CN.md
```json
"features": "-mmx,-sse,+soft-float",
```
`features` 配置项被用来启用或禁用某个目标 **CPU 特征**(CPU feature)。通过在它们前面添加`-`号,我们将 `mmx` 和 `sse` 特征禁用;添加前缀`+`号,我们启用了 `soft-float` 特征。
`mmx` 和 `sse` 特征决定了是否支持**单指令多数据流**([Single Instruction Multiple Data,SIMD](https://en.wikipedia.org/wiki/SIMD))相关指令,这些指令常常能显著地提高程序层面的性能。然而,在内核中使用庞大的 SIMD 寄存器,可能会造成较大的性能影响:因为每次程序中断时,内核不得不储存整个庞大的 SIMD 寄存器以备恢复——这意味着,对每个硬件中断或系统调用,完整的 SIMD 状态必须存到主存中。由于 SIMD 状态可能相当大(512~1600 个字节),而中断可能时常发生,这些额外的存储与恢复操作可能显著地影响效率。为解决这个问题,我们对内核禁用 SIMD(但这不意味着禁用内核之上的应用程序的 SIMD 支持)。
禁用 SIMD 产生的一个问题是,`x86_64` 架构的浮点数指针运算默认依赖于 SIMD 寄存器。我们的解决方法是,启用 `soft-float` 特征,它将使用基于整数的软件功能,模拟浮点数指针运算。
为了让读者的印象更清晰,我们撰写了一篇关于 [禁用 SIMD][disabling SIMD] 的短文。
[disabling SIMD]: @/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.zh-CN.md
```json
"rustc-abi": "x86-softfloat"
```
As we want to use the `soft-float` feature, we also need to tell the Rust compiler `rustc` that we want to use the corresponding ABI. We can do that by setting the `rustc-abi` field to `x86-softfloat`.
现在,我们将各个配置项整合在一起。我们的目标配置清单应该长这样:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": 64,
"target-c-int-width": 32,
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float",
"rustc-abi": "x86-softfloat"
}
```
### 编译内核
要编译我们的内核,我们将使用 Linux 系统的编写风格(这可能是 LLVM 的默认风格)。这意味着,我们需要把[前一篇文章][previous post]中编写的入口点重命名为 `_start`:
[previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.md
```rust
// src/main.rs
#![no_std] // 不链接 Rust 标准库
#![no_main] // 禁用所有 Rust 层级的入口点
use core::panic::PanicInfo;
/// 这个函数将在 panic 时被调用
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[unsafe(no_mangle)] // 不重整函数名
pub extern "C" fn _start() -> ! {
// 因为编译器会寻找一个名为 `_start` 的函数,所以这个函数就是入口点
// 默认命名为 `_start`
loop {}
}
```
注意的是,无论你开发使用的是哪类操作系统,你都需要将入口点命名为 `_start`。前一篇文章中编写的 Windows 系统和 macOS 对应的入口点不应该被保留。
通过把 JSON 文件名传入 `--target` 选项,我们现在可以开始编译我们的内核。让我们试试看:
```
> cargo build --target x86_64-blog_os.json
error: `.json` target specs require -Zjson-target-spec
```
毫不意外的编译失败了,错误信息告诉我们自定义 JSON 目标规范是一个不稳定的功能,需要显式启用。这是因为 JSON 目标文件的格式尚未被认为是稳定的,因此在未来的 Rust 版本中可能会发生变化。有关更多信息,请参阅[自定义 JSON 目标规范的跟踪 issue][json-target-spec-issue]。
[json-target-spec-issue]: https://github.com/rust-lang/rust/issues/151528
#### `json-target-spec` 选项
要启用对自定义 JSON 目标规范的支持,我们需要创建一个 [cargo 配置][cargo configuration] 文件,即 `.cargo/config.toml`(`.cargo` 文件夹应该在 `src` 文件夹旁边),并写入以下语句:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
```
这会启用不稳定的 `json-target-spec` 功能,允许我们使用自定义的 JSON 目标文件。
有了这个配置后,让我们再次尝试编译:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
```
现在我们看到了一个不同的错误!错误信息告诉我们编译器没有找到 [`core`][`core` library] 这个crate,它包含了Rust语言中的部分基础类型,如 `Result`、`Option`、迭代器等等,并且它还会隐式链接到 `no_std` 特性里面。
[`core` library]: https://doc.rust-lang.org/nightly/core/index.html
通常状况下,`core` crate以**预编译库**(precompiled library)的形式与 Rust 编译器一同发布——这时,`core` crate只对支持的宿主系统有效,而对我们自定义的目标系统无效。如果我们想为其它系统编译代码,我们需要为这些系统重新编译整个 `core` crate。
#### `build-std` 选项
此时就到了cargo中 [`build-std` 特性][`build-std` feature] 登场的时刻,该特性允许你按照自己的需要重编译 `core` 等标准crate,而不需要使用Rust安装程序内置的预编译版本。 但是该特性是全新的功能,到目前为止尚未完全完成,所以它被标记为 "unstable" 且仅被允许在 [Nightly Rust 编译器][Nightly Rust compilers] 环境下调用。
[`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std
[Nightly Rust compilers]:https://os.phil-opp.com/zh-CN/minimal-rust-kernel/#an-zhuang-nightly-rust
要启用该特性,你需要在 [cargo 配置][cargo configuration] 文件 `.cargo/config.toml` 中添加以下语句:
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
build-std = ["core", "compiler_builtins"]
```
该配置会告知cargo需要重新编译 `core` 和 `compiler_builtins` 这两个crate,其中 `compiler_builtins` 是 `core` 的必要依赖。 另外重编译需要提供源码,我们可以使用 `rustup component add rust-src` 命令来下载它们。
**Note:** 仅 `2020-07-15` 之后的Rust nightly版本支持 `unstable.build-std` 配置项。
在设定 `unstable.build-std` 配置项并安装 `rust-src` 组件之后,我们就可以开始编译了:
```
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
如你所见,在执行 `cargo build` 之后, `core`、`rustc-std-workspace-core` (`compiler_builtins` 的依赖)和 `compiler_builtins` crate被重新编译了。
#### 内存相关函数
目前来说,Rust编译器假定所有内置函数(`built-in functions`)在所有系统内都是存在且可用的。事实上这个前提只对了一半,
绝大多数内置函数都可以被 `compiler_builtins` 提供,而这个crate刚刚已经被我们重编译过了,然而部分内存相关函数是需要操作系统相关的标准C库提供的。
比如,`memset`(该函数可以为一个内存块内的所有比特进行赋值)、`memcpy`(将一个内存块里的数据拷贝到另一个内存块)以及`memcmp`(比较两个内存块的数据)。
好在我们的内核暂时还不需要用到这些函数,但是不要高兴的太早,当我们编写更丰富的功能(比如拷贝数据结构)时就会用到了。
现在我们当然无法提供操作系统相关的标准C库,所以我们需要使用其他办法提供这些东西。一个显而易见的途径就是自己实现 `memset` 这些函数,但不要忘记加入 `#[unsafe(no_mangle)]` 语句,以避免编译时被自动重命名。 当然,这样做很危险,底层函数中最细微的错误也会将程序导向不可预知的未来。比如,你可能在实现 `memcpy` 时使用了一个 `for` 循环,然而 `for` 循环本身又会调用 [`IntoIterator::into_iter`] 这个trait方法,这个方法又会再次调用 `memcpy`,此时一个无限递归就产生了,所以还是使用经过良好测试的既存实现更加可靠。
[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter
幸运的是,`compiler_builtins` 事实上自带了所有相关函数的实现,只是在默认情况下,出于避免和标准C库发生冲突的考量被禁用掉了,此时我们需要将 [`build-std-features`] 配置项设置为 `["compiler-builtins-mem"]` 来启用这个特性。如同 `build-std` 配置项一样,该特性可以使用 `-Z` 参数启用,也可以在 `.cargo/config.toml` 中使用 `unstable` 配置集启用。现在我们的配置文件中的相关部分是这样子的:
[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features
```toml
# in .cargo/config.toml
[unstable]
json-target-spec = true
build-std-features = ["compiler-builtins-mem"]
build-std = ["core", "compiler_builtins"]
```
(`compiler-builtins-mem` 特性是在 [这个PR](https://github.com/rust-lang/rust/pull/77284) 中被引入的,所以你的Rust nightly更新时间必须晚于 `2020-09-30`。)
该参数为 `compiler_builtins` 启用了 [`mem` 特性][`mem` feature],至于具体效果,就是已经在内部通过 `#[unsafe(no_mangle)]` 向链接器提供了 [`memcpy` 等函数的实现][`memcpy` etc. implementations]。
[`mem` feature]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55
[`memcpy` etc. implementations]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69
经过这些修改,我们的内核已经完成了所有编译所必需的函数,那么让我们继续对代码进行完善。
#### 设置默认编译目标
每次调用 `cargo build` 命令都需要传入 `--target` 参数很麻烦吧?其实我们可以复写掉默认值,从而省略这个参数,只需要在 `.cargo/config.toml` 中加入以下 [cargo 配置][cargo configuration]:
[cargo configuration]: https://doc.rust-lang.org/cargo/reference/config.html
```toml
# in .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
```
这个配置会告知 `cargo` 使用 `x86_64-blog_os.json` 这个文件作为默认的 `--target` 参数,此时只输入短短的一句 `cargo build` 就可以编译到指定平台了。如果你对其他配置项感兴趣,亦可以查阅 [官方文档][cargo configuration]。
那么现在我们已经可以用 `cargo build` 完成程序编译了,然而被成功调用的 `_start` 函数的函数体依然是一个空空如也的循环,是时候往屏幕上输出一点什么了。
### 向屏幕打印字符
要做到这一步,最简单的方式是写入 **VGA 字符缓冲区**([VGA text buffer](https://en.wikipedia.org/wiki/VGA-compatible_text_mode)):这是一段映射到 VGA 硬件的特殊内存片段,包含着显示在屏幕上的内容。通常情况下,它能够存储 25 行、80 列共 2000 个**字符单元**(character cell);每个字符单元能够显示一个 ASCII 字符,也能设置这个字符的**前景色**(foreground color)和**背景色**(background color)。输出到屏幕的字符大概长这样:

我们将在下篇文章中详细讨论 VGA 字符缓冲区的内存布局;目前我们只需要知道,这段缓冲区的地址是 `0xb8000`,且每个字符单元包含一个 ASCII 码字节和一个颜色字节。
我们的实现就像这样:
```rust
static HELLO: &[u8] = b"Hello World!";
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
let vga_buffer = 0xb8000 as *mut u8;
for (i, &byte) in HELLO.iter().enumerate() {
unsafe {
*vga_buffer.offset(i as isize * 2) = byte;
*vga_buffer.offset(i as isize * 2 + 1) = 0xb;
}
}
loop {}
}
```
在这段代码中,我们预先定义了一个**字节字符串**(byte string)类型的**静态变量**(static variable),名为 `HELLO`。我们首先将整数 `0xb8000` **转换**(cast)为一个**裸指针**([raw pointer])。这之后,我们迭代 `HELLO` 的每个字节,使用 [enumerate](https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate) 获得一个额外的序号变量 `i`。在 `for` 语句的循环体中,我们使用 [offset](https://doc.rust-lang.org/std/primitive.pointer.html#method.offset) 偏移裸指针,解引用它,来将字符串的每个字节和对应的颜色字节——`0xb` 代表淡青色——写入内存位置。
[raw pointer]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer
要注意的是,所有的裸指针内存操作都被一个 **unsafe 语句块**([unsafe block](https://doc.rust-lang.org/stable/book/second-edition/ch19-01-unsafe-rust.html))包围。这是因为,此时编译器不能确保我们创建的裸指针是有效的;一个裸指针可能指向任何一个你内存位置;直接解引用并写入它,也许会损坏正常的数据。使用 `unsafe` 语句块时,程序员其实在告诉编译器,自己保证语句块内的操作是有效的。事实上,`unsafe` 语句块并不会关闭 Rust 的安全检查机制;它允许你多做的事情[只有四件][unsafe superpowers]。
[unsafe superpowers]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers
使用 `unsafe` 语句块要求程序员有足够的自信,所以必须强调的一点是,**肆意使用 unsafe 语句块并不是 Rust 编程的一贯方式**。在缺乏足够经验的前提下,直接在 `unsafe` 语句块内操作裸指针,非常容易把事情弄得很糟糕;比如,在不注意的情况下,我们很可能会意外地操作缓冲区以外的内存。
在这样的前提下,我们希望最小化 `unsafe` 语句块的使用。使用 Rust 语言,我们能够将不安全操作将包装为一个安全的抽象模块。举个例子,我们可以创建一个 VGA 缓冲区类型,把所有的不安全语句封装起来,来确保从类型外部操作时,无法写出不安全的代码:通过这种方式,我们只需要最少的 `unsafe` 语句块来确保我们不破坏**内存安全**([memory safety](https://en.wikipedia.org/wiki/Memory_safety))。在下一篇文章中,我们将会创建这样的 VGA 缓冲区封装。
## 启动内核
既然我们已经有了一个能够打印字符的可执行程序,是时候把它运行起来试试看了。首先,我们将编译完毕的内核与引导程序链接,来创建一个引导映像;这之后,我们可以在 QEMU 虚拟机中运行它,或者通过 U 盘在真机上运行。
### 创建引导映像
要将可执行程序转换为**可引导的映像**(bootable disk image),我们需要把它和引导程序链接。这里,引导程序将负责初始化 CPU 并加载我们的内核。
编写引导程序并不容易,所以我们不编写自己的引导程序,而是使用已有的 [bootloader](https://crates.io/crates/bootloader) 包;无需依赖于 C 语言,这个包基于 Rust 代码和内联汇编,实现了一个五脏俱全的 BIOS 引导程序。为了用它启动我们的内核,我们需要将它添加为一个依赖项,在 `Cargo.toml` 中添加下面的代码:
```toml
# in Cargo.toml
[dependencies]
bootloader = "0.9"
```
** 注意:** 当前环境仅兼容 `bootloader v0.9` 版本。较新的版本需考虑使用其他的构建工具,否则会导致构建出现未知错误。
只添加引导程序为依赖项,并不足以创建一个可引导的磁盘映像;我们还需要内核编译完成之后,将内核和引导程序组合在一起。然而,截至目前,原生的 cargo 并不支持在编译完成后添加其它步骤(详见[这个 issue](https://github.com/rust-lang/cargo/issues/545))。
为了解决这个问题,我们建议使用 `bootimage` 工具——它将会在内核编译完毕后,将它和引导程序组合在一起,最终创建一个能够引导的磁盘映像。我们可以使用下面的命令来安装这款工具:
```bash
cargo install bootimage
```
为了运行 `bootimage` 以及编译引导程序,我们需要安装 rustup 模块 `llvm-tools-preview`——我们可以使用 `rustup component add llvm-tools-preview` 来安装这个工具。
成功安装 `bootimage` 后,创建一个可引导的磁盘映像就变得相当容易。我们来输入下面的命令:
```bash
> cargo bootimage
```
可以看到的是,`bootimage` 工具开始使用 `cargo build` 编译你的内核,所以它将增量编译我们修改后的源码。在这之后,它会编译内核的引导程序,这可能将花费一定的时间;但和所有其它依赖包相似的是,在首次编译后,产生的二进制文件将被缓存下来——这将显著地加速后续的编译过程。最终,`bootimage` 将把内核和引导程序组合为一个可引导的磁盘映像。
运行这行命令之后,我们应该能在 `target/x86_64-blog_os/debug` 目录内找到我们的映像文件 `bootimage-blog_os.bin`。我们可以在虚拟机内启动它,也可以刻录到 U 盘上以便在真机上启动。(需要注意的是,因为文件格式不同,这里的 bin 文件并不是一个光驱映像,所以将它刻录到光盘不会起作用。)
事实上,在这行命令背后,`bootimage` 工具执行了三个步骤:
1. 编译我们的内核为一个 **ELF**([Executable and Linkable Format](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format))文件;
2. 编译引导程序为独立的可执行文件;
3. 将内核 ELF 文件**按字节拼接**(append by bytes)到引导程序的末端。
当机器启动时,引导程序将会读取并解析拼接在其后的 ELF 文件。这之后,它将把程序片段映射到**分页表**(page table)中的**虚拟地址**(virtual address),清零 **BSS段**(BSS segment),还将创建一个栈。最终它将读取**入口点地址**(entry point address)——我们程序中 `_start` 函数的位置——并跳转到这个位置。
### 在 QEMU 中启动内核
现在我们可以在虚拟机中启动内核了。为了在[QEMU](https://www.qemu.org/) 中启动内核,我们使用下面的命令:
```bash
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
```
然后就会弹出一个独立窗口:

我们可以看到,屏幕窗口已经显示出 “Hello World!” 字符串。祝贺你!
### 在真机上运行内核
我们也可以使用 dd 工具把内核写入 U 盘,以便在真机上启动。可以输入下面的命令:
```bash
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
```
在这里,`sdX` 是U盘的**设备名**([device name](https://en.wikipedia.org/wiki/Device_file))。请注意,**在选择设备名的时候一定要极其小心,因为目标设备上已有的数据将全部被擦除**。
写入到 U 盘之后,你可以在真机上通过引导启动你的系统。视情况而定,你可能需要在 BIOS 中打开特殊的启动菜单,或者调整启动顺序。需要注意的是,`bootloader` 包暂时不支持 UEFI,所以我们并不能在 UEFI 机器上启动。
### 使用 `cargo run`
要让在 QEMU 中运行内核更轻松,我们可以设置在 cargo 配置文件中设置 `runner` 配置项:
```toml
# in .cargo/config.toml
[target.'cfg(target_os = "none")']
runner = "bootimage runner"
```
在这里,`target.'cfg(target_os = "none")'` 筛选了三元组中宿主系统设置为 `"none"` 的所有编译目标——这将包含我们的 `x86_64-blog_os.json` 目标。另外,`runner` 的值规定了运行 `cargo run` 使用的命令;这个命令将在成功编译后执行,而且会传递可执行文件的路径为第一个参数。[官方提供的 cargo 文档](https://doc.rust-lang.org/cargo/reference/config.html)讲述了更多的细节。
命令 `bootimage runner` 由 `bootimage` 包提供,参数格式经过特殊设计,可以用于 `runner` 命令。它将给定的可执行文件与项目的引导程序依赖项链接,然后在 QEMU 中启动它。`bootimage` 包的 [README文档](https://github.com/rust-osdev/bootimage) 提供了更多细节和可以传入的配置参数。
现在我们可以使用 `cargo run` 来编译内核并在 QEMU 中启动了。
## 下篇预告
在下篇文章中,我们将细致地探索 VGA 字符缓冲区,并包装它为一个安全的接口。我们还将基于它实现 `println!` 宏。
================================================
FILE: blog/content/edition-2/posts/03-vga-text-buffer/index.es.md
================================================
+++
title = "Modo de Texto VGA"
weight = 3
path = "es/modo-texto-vga"
date = 2018-02-26
[extra]
chapter = "Fundamentos"
# GitHub usernames of the people that translated this post
translators = ["dobleuber"]
+++
El [modo de texto VGA] es una forma sencilla de imprimir texto en la pantalla. En esta publicación, creamos una interfaz que hace que su uso sea seguro y simple al encapsular toda la inseguridad en un módulo separado. También implementamos soporte para los [macros de formato] de Rust.
[modo de texto VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
[macros de formato]: https://doc.rust-lang.org/std/fmt/#related-macros
Este blog se desarrolla abiertamente en [GitHub]. Si tienes algún problema o pregunta, por favor abre un issue allí. También puedes dejar comentarios [al final]. El código fuente completo para esta publicación se puede encontrar en la rama [`post-03`][rama del post].
[GitHub]: https://github.com/phil-opp/blog_os
[al final]: #comments
[rama del post]: https://github.com/phil-opp/blog_os/tree/post-03
## El Buffer de Texto VGA
Para imprimir un carácter en la pantalla en modo de texto VGA, uno tiene que escribirlo en el buffer de texto del hardware VGA. El buffer de texto VGA es un arreglo bidimensional con típicamente 25 filas y 80 columnas, que se renderiza directamente en la pantalla. Cada entrada del arreglo describe un solo carácter de pantalla a través del siguiente formato:
| Bit(s) | Valor |
| ------ | --------------------- |
| 0-7 | Código de punto ASCII |
| 8-11 | Color de primer plano |
| 12-14 | Color de fondo |
| 15 | Parpadeo |
El primer byte representa el carácter que debe imprimirse en la [codificación ASCII]. Para ser más específicos, no es exactamente ASCII, sino un conjunto de caracteres llamado [_página de códigos 437_] con algunos caracteres adicionales y ligeras modificaciones. Para simplificar, procederemos a llamarlo un carácter ASCII en esta publicación.
[codificación ASCII]: https://en.wikipedia.org/wiki/ASCII
[_página de códigos 437_]: https://en.wikipedia.org/wiki/Code_page_437
El segundo byte define cómo se muestra el carácter. Los primeros cuatro bits definen el color de primer plano, los siguientes tres bits el color de fondo, y el último bit si el carácter debe parpadear. Los siguientes colores están disponibles:
| Número | Color | Número + Bit de Brillo | Color Brillante |
| ------ | ---------- | ---------------------- | --------------- |
| 0x0 | Negro | 0x8 | Gris Oscuro |
| 0x1 | Azul | 0x9 | Azul Claro |
| 0x2 | Verde | 0xa | Verde Claro |
| 0x3 | Cian | 0xb | Cian Claro |
| 0x4 | Rojo | 0xc | Rojo Claro |
| 0x5 | Magenta | 0xd | Magenta Claro |
| 0x6 | Marrón | 0xe | Amarillo |
| 0x7 | Gris Claro | 0xf | Blanco |
Bit 4 es el _bit de brillo_, que convierte, por ejemplo, azul en azul claro. Para el color de fondo, este bit se reutiliza como el bit de parpadeo.
El buffer de texto VGA es accesible a través de [E/S mapeada en memoria] a la dirección `0xb8000`. Esto significa que las lecturas y escrituras a esa dirección no acceden a la RAM, sino que acceden directamente al buffer de texto en el hardware VGA. Esto significa que podemos leer y escribir a través de operaciones de memoria normales a esa dirección.
[E/S mapeada en memoria]: https://en.wikipedia.org/wiki/Memory-mapped_I/O
Ten en cuenta que el hardware mapeado en memoria podría no soportar todas las operaciones normales de RAM. Por ejemplo, un dispositivo podría soportar solo lecturas por byte y devolver basura cuando se lee un `u64`. Afortunadamente, el buffer de texto [soporta lecturas y escrituras normales], por lo que no tenemos que tratarlo de una manera especial.
[soporta lecturas y escrituras normales]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip
## Un Módulo de Rust
Ahora que sabemos cómo funciona el buffer VGA, podemos crear un módulo de Rust para manejar la impresión:
```rust
// en src/main.rs
mod vga_buffer;
```
Para el contenido de este módulo, creamos un nuevo archivo `src/vga_buffer.rs`. Todo el código a continuación va en nuestro nuevo módulo (a menos que se especifique lo contrario).
### Colores
Primero, representamos los diferentes colores usando un enum:
```rust
// en src/vga_buffer.rs
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
```
Usamos un [enum similar a C] aquí para especificar explícitamente el número para cada color. Debido al atributo `repr(u8)`, cada variante del enum se almacena como un `u8`. En realidad, 4 bits serían suficientes, pero Rust no tiene un tipo `u4`.
[enum similar a C]: https://doc.rust-lang.org/rust-by-example/custom_types/enum/c_like.html
Normalmente, el compilador emitiría una advertencia por cada variante no utilizada. Al usar el atributo `#[allow(dead_code)]`, deshabilitamos estas advertencias para el enum `Color`.
Al [derivar] los rasgos [`Copy`], [`Clone`], [`Debug`], [`PartialEq`], y [`Eq`], habilitamos la [semántica de copia] para el tipo y lo hacemos imprimible y comparable.
[derivar]: https://doc.rust-lang.org/rust-by-example/trait/derive.html
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
Para representar un código de color completo que especifique el color de primer plano y de fondo, creamos un [nuevo tipo] sobre `u8`:
[nuevo tipo]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
```rust
// en src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
```
La estructura `ColorCode` contiene el byte de color completo, que incluye el color de primer plano y de fondo. Como antes, derivamos los rasgos `Copy` y `Debug` para él. Para asegurar que `ColorCode` tenga el mismo diseño de datos exacto que un `u8`, usamos el atributo [`repr(transparent)`].
[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent
### Buffer de Texto
Ahora podemos agregar estructuras para representar un carácter de pantalla y el buffer de texto:
```rust
// en src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
```
Dado que el orden de los campos en las estructuras predeterminadas no está definido en Rust, necesitamos el atributo [`repr(C)`]. Garantiza que los campos de la estructura se dispongan exactamente como en una estructura C y, por lo tanto, garantiza el orden correcto de los campos. Para la estructura `Buffer`, usamos [`repr(transparent)`] nuevamente para asegurar que tenga el mismo diseño de memoria que su único campo.
[`repr(C)`]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc
Para escribir en pantalla, ahora creamos un tipo de escritor:
```rust
// en src/vga_buffer.rs
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
```
El escritor siempre escribirá en la última línea y desplazará las líneas hacia arriba cuando una línea esté llena (o en `\n`). El campo `column_position` lleva un seguimiento de la posición actual en la última fila. Los colores de primer plano y de fondo actuales están especificados por `color_code` y una referencia al buffer VGA está almacenada en `buffer`. Ten en cuenta que necesitamos una [vida útil explícita] aquí para decirle al compilador cuánto tiempo es válida la referencia. La vida útil [`'static`] especifica que la referencia es válida durante todo el tiempo de ejecución del programa (lo cual es cierto para el buffer de texto VGA).
[vida útil explícita]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax
[`'static`]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
### Impresión
Ahora podemos usar el `Writer` para modificar los caracteres del buffer. Primero creamos un método para escribir un solo byte ASCII:
```rust
// en src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code,
});
self.column_position += 1;
}
}
}
fn new_line(&mut self) {/* TODO */}
}
```
Si el byte es el byte de [nueva línea] `\n`, el escritor no imprime nada. En su lugar, llama a un método `new_line`, que implementaremos más tarde. Otros bytes se imprimen en la pantalla en el segundo caso de `match`.
[nueva línea]: https://en.wikipedia.org/wiki/Newline
Al imprimir un byte, el escritor verifica si la línea actual está llena. En ese caso, se usa una llamada a `new_line` para envolver la línea. Luego escribe un nuevo `ScreenChar` en el buffer en la posición actual. Finalmente, se avanza la posición de la columna actual.
Para imprimir cadenas completas, podemos convertirlas en bytes e imprimirlas una por una:
```rust
// en src/vga_buffer.rs
impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// byte ASCII imprimible o nueva línea
0x20..=0x7e | b'\n' => self.write_byte(byte),
// no es parte del rango ASCII imprimible
_ => self.write_byte(0xfe),
}
}
}
}
```
El buffer de texto VGA solo soporta ASCII y los bytes adicionales de [página de códigos 437]. Las cadenas de Rust son [UTF-8] por defecto, por lo que podrían contener bytes que no son soportados por el buffer de texto VGA. Usamos un `match` para diferenciar los bytes ASCII imprimibles (una nueva línea o cualquier cosa entre un carácter de espacio y un carácter `~`) y los bytes no imprimibles. Para los bytes no imprimibles, imprimimos un carácter `■`, que tiene el código hexadecimal `0xfe` en el hardware VGA.
[página de códigos 437]: https://en.wikipedia.org/wiki/Code_page_437
[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm
#### ¡Pruébalo!
Para escribir algunos caracteres en la pantalla, puedes crear una función temporal:
```rust
// en src/vga_buffer.rs
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}
```
Primero crea un nuevo Writer que apunta al buffer VGA en `0xb8000`. La sintaxis para esto podría parecer un poco extraña: Primero, convertimos el entero `0xb8000` como un [puntero sin procesar] mutable. Luego lo convertimos en una referencia mutable al desreferenciarlo (a través de `*`) y tomarlo prestado inmediatamente (a través de `&mut`). Esta conversión requiere un [bloque `unsafe`], ya que el compilador no puede garantizar que el puntero sin procesar sea válido.
[puntero sin procesar]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[bloque `unsafe`]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
Luego escribe el byte `b'H'` en él. El prefijo `b` crea un [literal de byte], que representa un carácter ASCII. Al escribir las cadenas `"ello "` y `"Wörld!"`, probamos nuestro método `write_string` y el manejo de caracteres no imprimibles. Para ver la salida, necesitamos llamar a la función `print_something` desde nuestra función `_start`:
```rust
// en src/main.rs
#[no_mangle]
pub extern "C" fn _start() -> ! {
vga_buffer::print_something();
loop {}
}
```
Cuando ejecutamos nuestro proyecto ahora, se debería imprimir un `Hello W■■rld!` en la esquina inferior izquierda de la pantalla en amarillo:
[literal de byte]: https://doc.rust-lang.org/reference/tokens.html#byte-literals

Observa que la `ö` se imprime como dos caracteres `■`. Eso es porque `ö` está representado por dos bytes en [UTF-8], los cuales no caen en el rango ASCII imprimible. De hecho, esta es una propiedad fundamental de UTF-8: los bytes individuales de valores multibyte nunca son ASCII válidos.
### Volátil
Acabamos de ver que nuestro mensaje se imprimió correctamente. Sin embargo, podría no funcionar con futuros compiladores de Rust que optimicen más agresivamente.
El problema es que solo escribimos en el `Buffer` y nunca leemos de él nuevamente. El compilador no sabe que realmente accedemos a la memoria del buffer VGA (en lugar de la RAM normal) y no sabe nada sobre el efecto secundario de que algunos caracteres aparezcan en la pantalla. Por lo tanto, podría decidir que estas escrituras son innecesarias y pueden omitirse. Para evitar esta optimización errónea, necesitamos especificar estas escrituras como _[volátiles]_. Esto le dice al compilador que la escritura tiene efectos secundarios y no debe ser optimizada.
[volátiles]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
Para usar escrituras volátiles para el buffer VGA, usamos la biblioteca [volatile][crate volatile]. Este _crate_ (así es como se llaman los paquetes en el mundo de Rust) proporciona un tipo de envoltura `Volatile` con métodos `read` y `write`. Estos métodos usan internamente las funciones [read_volatile] y [write_volatile] de la biblioteca principal y, por lo tanto, garantizan que las lecturas/escrituras no sean optimizadas.
[crate volatile]: https://docs.rs/volatile
[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html
[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html
Podemos agregar una dependencia en el crate `volatile` agregándolo a la sección `dependencies` de nuestro `Cargo.toml`:
```toml
# en Cargo.toml
[dependencies]
volatile = "0.2.6"
```
Asegúrate de especificar la versión `0.2.6` de `volatile`. Las versiones más nuevas del crate no son compatibles con esta publicación.
`0.2.6` es el número de versión [semántica]. Para más información, consulta la guía [Especificar Dependencias] de la documentación de cargo.
[semántica]: https://semver.org/
[Especificar Dependencias]: https://doc.crates.io/specifying-dependencies.html
Vamos a usarlo para hacer que las escrituras al buffer VGA sean volátiles. Actualizamos nuestro tipo `Buffer` de la siguiente manera:
```rust
// en src/vga_buffer.rs
use volatile::Volatile;
struct Buffer {
chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
En lugar de un `ScreenChar`, ahora estamos usando un `Volatile`. (El tipo `Volatile` es [genérico] y puede envolver (casi) cualquier tipo). Esto asegura que no podamos escribir accidentalmente en él “normalmente”. En su lugar, ahora tenemos que usar el método `write`.
[genérico]: https://doc.rust-lang.org/book/ch10-01-syntax.html
Esto significa que tenemos que actualizar nuestro método `Writer::write_byte`:
```rust
// en src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
...
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code,
});
...
}
}
}
...
}
```
================================================
FILE: blog/content/edition-2/posts/03-vga-text-buffer/index.fa.md
================================================
+++
title = "حالت متن VGA"
weight = 3
path = "fa/vga-text-mode"
date = 2018-02-26
[extra]
# Please update this when updating the translation
translation_based_on_commit = "fb8b03e82d9805473fed16e8795a78a020a6b537"
# GitHub usernames of the people that translated this post
translators = ["hamidrezakp", "MHBahrampour"]
rtl = true
+++
[حالت متن VGA] یک روش ساده برای چاپ متن روی صفحه است. در این پست ، با قرار دادن همه موارد غیر ایمنی در یک ماژول جداگانه ، رابطی ایجاد می کنیم که استفاده از آن را ایمن و ساده می کند. همچنین پشتیبانی از [ماکروی فرمتبندی] راست را پیاده سازی میکنیم.
[حالت متن VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
[ماکروی فرمتبندی]: https://doc.rust-lang.org/std/fmt/#related-macros
این بلاگ بصورت آزاد بر روی [گیتهاب] توسعه داده شده. اگر مشکل یا سوالی دارید، لطفا آنجا یک ایشو باز کنید. همچنین میتوانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را می توانید در شاخه [`post-01`][post branch] پیدا کنید.
[گیتهاب]: https://github.com/phil-opp/blog_os
[در زیر]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
## بافر متن VGA
برای چاپ یک کاراکتر روی صفحه در حالت متن VGA ، باید آن را در بافر متن سخت افزار VGA بنویسید. بافر متن VGA یک آرایه دو بعدی است که به طور معمول 25 ردیف و 80 ستون دارد که مستقیماً به صفحه نمایش داده(رندر) می شود. هر خانه آرایه یک کاراکتر صفحه نمایش را از طریق قالب زیر توصیف می کند:
| Bit(s) | Value |
| ------ | ---------------- |
| 0-7 | ASCII code point |
| 8-11 | Foreground color |
| 12-14 | Background color |
| 15 | Blink |
اولین بایت کاراکتری در [کدگذاری ASCII] را نشان می دهد که باید چاپ شود. اگر بخواهیم دقیق باشیم ، دقیقاً ASCII نیست ، بلکه مجموعه ای از کاراکترها به نام [_کد صفحه 437_] با برخی کاراکتر های اضافی و تغییرات جزئی است. برای سادگی ، ما در این پست آنرا یک کاراکتر ASCII می نامیم.
[کدگذاری ASCII]: https://en.wikipedia.org/wiki/ASCII
[_کد صفحه 437_]: https://en.wikipedia.org/wiki/Code_page_437
بایت دوم نحوه نمایش کاراکتر را مشخص می کند. چهار بیت اول رنگ پیش زمینه را مشخص می کند ، سه بیت بعدی رنگ پس زمینه و بیت آخر اینکه کاراکتر باید چشمک بزند یا نه. رنگ های زیر موجود است:
| Number | Color | Number + Bright Bit | Bright Color |
| ------ | ---------- | ------------------- | ------------ |
| 0x0 | Black | 0x8 | Dark Gray |
| 0x1 | Blue | 0x9 | Light Blue |
| 0x2 | Green | 0xa | Light Green |
| 0x3 | Cyan | 0xb | Light Cyan |
| 0x4 | Red | 0xc | Light Red |
| 0x5 | Magenta | 0xd | Pink |
| 0x6 | Brown | 0xe | Yellow |
| 0x7 | Light Gray | 0xf | White |
بیت 4، بیت روشنایی است ، که به عنوان مثال آبی به آبی روشن تبدیل میکند. برای رنگ پس زمینه ، این بیت به عنوان بیت چشمک مورد استفاده قرار می گیرد.
بافر متن VGA از طریق [ورودی/خروجی حافظهنگاشتی] به آدرس`0xb8000` قابل دسترسی است. این بدان معنی است که خواندن و نوشتن در آن آدرس به RAM دسترسی ندارد ، بلکه مستقیماً دسترسی به بافر متن در سخت افزار VGA دارد. این بدان معنی است که می توانیم آن را از طریق عملیات حافظه عادی در آن آدرس بخوانیم و بنویسیم.
[ورودی/خروجی حافظهنگاشتی]: https://en.wikipedia.org/wiki/Memory-mapped_I/O
توجه داشته باشید که ممکن است سخت افزار حافظهنگاشتی شده از تمام عملیات معمول RAM پشتیبانی نکند. به عنوان مثال ، یک دستگاه ممکن است فقط خواندن بایتی را پشتیبانی کرده و با خواندن `u64` یک مقدار زباله را برگرداند. خوشبختانه بافر متن [از خواندن و نوشتن عادی پشتیبانی می کند] ، بنابراین مجبور نیستیم با آن به روش خاصی برخورد کنیم.
[از خواندن و نوشتن عادی پشتیبانی می کند]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip
## یک ماژول راست
اکنون که از نحوه کار بافر VGA مطلع شدیم ، می توانیم یک ماژول Rust برای مدیریت چاپ ایجاد کنیم:
```rust
// in src/main.rs
mod vga_buffer;
```
برای محتوای این ماژول ما یک فایل جدید `src/vga_buffer.rs` ایجاد می کنیم. همه کدهای زیر وارد ماژول جدید ما می شوند (مگر اینکه طور دیگری مشخص شده باشد).
### رنگ ها
اول ، ما رنگ های مختلف را با استفاده از یک enum نشان می دهیم:
```rust
// in src/vga_buffer.rs
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
```
ما در اینجا از [enum مانند C] برای مشخص کردن صریح عدد برای هر رنگ استفاده می کنیم. به دلیل ویژگی `repr(u8)` هر نوع enum به عنوان یک `u8` ذخیره می شود. در واقع 4 بیت کافی است ، اما Rust نوع `u4` ندارد.
[enum مانند C]: https://doc.rust-lang.org/rust-by-example/custom_types/enum/c_like.html
به طور معمول کامپایلر برای هر نوع استفاده نشده اخطار می دهد. با استفاده از ویژگی `#[allow(dead_code)]` این هشدارها را برای enum `Color` غیرفعال می کنیم.
توسط [deriving] کردن تریتهای [`Copy`], [`Clone`], [`Debug`], [`PartialEq`], و [`Eq`] ما [مفهوم کپی] را برای نوع فعال کرده و آن را قابل پرینت کردن میکنیم.
[deriving]: https://doc.rust-lang.org/rust-by-example/trait/derive.html
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
[`PartialEq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html
[`Eq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html
[مفهوم کپی]: https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types
برای نشان دادن یک کد کامل رنگ که رنگ پیش زمینه و پس زمینه را مشخص می کند ، یک [نوع جدید] بر روی `u8` ایجاد می کنیم:
[نوع جدید]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
```rust
// in src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
```
ساختمان `ColorCode` شامل بایت کامل رنگ است که شامل رنگ پیش زمینه و پس زمینه است. مانند قبل ، ویژگی های `Copy` و` Debug` را برای آن derive می کنیم. برای اطمینان از اینکه `ColorCode` دقیقاً ساختار داده مشابه `u8` دارد ، از ویژگی [`repr(transparent)`] استفاده می کنیم.
[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent
### بافر متن
اکنون می توانیم ساختمانهایی را برای نمایش یک کاراکتر صفحه و بافر متن اضافه کنیم:
```rust
// in src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
از آنجا که ترتیب فیلدهای ساختمانهای پیش فرض در Rust تعریف نشده است ، به ویژگی[`repr(C)`] نیاز داریم. این تضمین می کند که فیلد های ساختمان دقیقاً مانند یک ساختمان C ترسیم شده اند و بنابراین ترتیب درست را تضمین می کند. برای ساختمان `Buffer` ، ما دوباره از [`repr(transparent)`] استفاده می کنیم تا اطمینان حاصل شود که نحوه قرارگیری در حافظه دقیقا همان یک فیلد است.
[`repr(C)`]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc
برای نوشتن در صفحه ، اکنون یک نوع نویسنده ایجاد می کنیم:
```rust
// in src/vga_buffer.rs
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
```
نویسنده همیشه در آخرین خط مینویسد و وقتی خط پر است (یا در `\n`) ، سطرها را به سمت بالا شیفت می دهد. فیلد `column_position` موقعیت فعلی در ردیف آخر را نگهداری می کند. رنگهای پیش زمینه و پس زمینه فعلی توسط `color_code` مشخص شده و یک ارجاع (رفرنس) به بافر VGA در `buffer` ذخیره می شود. توجه داشته باشید که ما در اینجا به [طول عمر مشخصی] نیاز داریم تا به کامپایلر بگوییم تا چه مدت این ارجاع معتبر است. ظول عمر [`'static`] مشخص می کند که ارجاع برای کل مدت زمان اجرای برنامه معتبر باشد (که برای بافر متن VGA درست است).
[طول عمر مشخصی]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax
[`'static`]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
### چاپ کردن
اکنون می توانیم از `Writer` برای تغییر کاراکترهای بافر استفاده کنیم. ابتدا یک متد برای نوشتن یک بایت ASCII ایجاد می کنیم:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}
fn new_line(&mut self) {/* TODO */}
}
```
اگر بایت، بایتِ [خط جدید] `\n` باشد، نویسنده چیزی چاپ نمی کند. در عوض متد `new_line` را فراخوانی می کند که بعداً آن را پیادهسازی خواهیم کرد. بایت های دیگر در حالت دوم match روی صفحه چاپ می شوند.
[خط جدید]: https://en.wikipedia.org/wiki/Newline
هنگام چاپ بایت ، نویسنده بررسی می کند که آیا خط فعلی پر است یا نه. در صورت پُر بودن، برای نوشتن در خط ، باید متد `new_line` صدا زده شود. سپس یک `ScreenChar` جدید در بافر در موقعیت فعلی می نویسد. سرانجام ، موقعیت ستون فعلی یکی افزایش مییابد.
برای چاپ کل رشته ها، می توانیم آنها را به بایت تبدیل کرده و یکی یکی چاپ کنیم:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// printable ASCII byte or newline
0x20..=0x7e | b'\n' => self.write_byte(byte),
// not part of printable ASCII range
_ => self.write_byte(0xfe),
}
}
}
}
```
بافر متن VGA فقط از ASCII و بایت های اضافی [کد صفحه 437] پشتیبانی می کند. رشته های راست به طور پیش فرض [UTF-8] هستند ، بنابراین ممکن است حاوی بایت هایی باشند که توسط بافر متن VGA پشتیبانی نمی شوند. ما از یک match برای تفکیک بایت های قابل چاپ ASCII (یک خط جدید یا هر چیز دیگری بین یک کاراکتر فاصله و یک کاراکتر`~`) و بایت های غیر قابل چاپ استفاده می کنیم. برای بایت های غیر قابل چاپ ، یک کاراکتر `■` چاپ می کنیم که دارای کد شانزدهای (hex) `0xfe` بر روی سخت افزار VGA است.
[کد صفحه 437]: https://en.wikipedia.org/wiki/Code_page_437
[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm
#### امتحاناش کنید!
برای نوشتن چند کاراکتر بر روی صفحه ، می توانید یک تابع موقتی ایجاد کنید:
```rust
// in src/vga_buffer.rs
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}
```
ابتدا یک Writer جدید ایجاد می کند که به بافر VGA در `0xb8000` اشاره دارد. سینتکس این ممکن است کمی عجیب به نظر برسد: اول ، ما عدد صحیح `0xb8000` را به عنوان [اشاره گر خام] قابل تغییر در نظر می گیریم. سپس با dereferencing کردن آن (از طریق "*") و بلافاصله ارجاع مجدد (از طریق `&mut`) آن را به یک مرجع قابل تغییر تبدیل می کنیم. این تبدیل به یک [بلوک `غیرایمن`] احتیاج دارد ، زیرا کامپایلر نمی تواند صحت اشارهگر خام را تضمین کند.
[اشاره گر خام]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[بلوک `غیرایمن`]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
سپس بایت `b'H'` را روی آن می نویسد. پیشوند `b` یک [بایت لیترال] ایجاد می کند ، که بیانگر یک کاراکتر ASCII است. با نوشتن رشته های `"ello "` و `"Wörld!"` ، ما متد `write_string` و واکنش به کاراکترهای غیر قابل چاپ را آزمایش می کنیم. برای دیدن خروجی ، باید تابع `print_something` را از تابع `_start` فراخوانی کنیم:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
vga_buffer::print_something();
loop {}
}
```
اکنون هنگامی که ما پروژه را اجرا می کنیم ، باید یک `Hello W■■rld!` در گوشه سمت چپ _پایین_ صفحه به رنگ زرد چاپ شود:
[بایت لیترال]: https://doc.rust-lang.org/reference/tokens.html#byte-literals

توجه داشته باشید که `ö` به عنوان دو کاراکتر `■` چاپ شده است. به این دلیل که `ö` با دو بایت در [UTF-8] نمایش داده می شود ، که هر دو در محدوده قابل چاپ ASCII قرار نمی گیرند. در حقیقت ، این یک ویژگی اساسی UTF-8 است: هر بایت از مقادیر چند بایتی هرگز ASCII معتبر نیستند.
### فرّار
ما الان دیدیم که پیام ما به درستی چاپ شده است. با این حال ، ممکن است با کامپایلرهای آینده Rust که به صورت تهاجمی تری(aggressively) بهینه می شوند ، کار نکند.
مشکل این است که ما فقط به `Buffer` می نویسیم و هرگز از آن نمیخوانیم. کامپایلر نمی داند که ما واقعاً به حافظه بافر VGA (به جای RAM معمولی) دسترسی پیدا می کنیم و در مورد اثر جانبی آن یعنی نمایش برخی کاراکتر ها روی صفحه چیزی نمی داند. بنابراین ممکن است تصمیم بگیرد که این نوشتن ها غیرضروری هستند و می تواند آن را حذف کند. برای جلوگیری از این بهینه سازی اشتباه ، باید این نوشتن ها را به عنوان _[فرّار]_ مشخص کنیم. این به کامپایلر می گوید که نوشتن عوارض جانبی دارد و نباید بهینه شود.
[فرّار]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
به منظور استفاده از نوشتن های فرار برای بافر VGA ، ما از کتابخانه [volatile][volatile crate] استفاده می کنیم. این _crate_ (بسته ها در جهان Rust اینطور نامیده میشوند) نوع `Volatile` را که یک نوع wrapper هست با متد های `read` و `write` فراهم می کند. این متد ها به طور داخلی از توابع [read_volatile] و [write_volatile] کتابخانه اصلی استفاده می کنند و بنابراین تضمین می کنند که خواندن/ نوشتن با بهینه شدن حذف نمیشوند.
[volatile crate]: https://docs.rs/volatile
[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html
[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html
ما می توانیم وابستگی به کرت (crate) `volatile` را بوسیله اضافه کردن آن به بخش `dependencies` (وابستگی های) `Cargo.toml` اضافه کنیم:
```toml
# in Cargo.toml
[dependencies]
volatile = "0.2.6"
```
`0.2.6` شماره نسخه [معنایی] است. برای اطلاعات بیشتر ، به راهنمای [تعیین وابستگی ها] مستندات کارگو (cargo) مراجعه کنید.
[معنایی]: https://semver.org/
[تعیین وابستگی ها]: https://doc.crates.io/specifying-dependencies.html
بیایید از آن برای نوشتن فرار در بافر VGA استفاده کنیم. نوع `Buffer` خود را به صورت زیر بروزرسانی می کنیم:
```rust
// in src/vga_buffer.rs
use volatile::Volatile;
struct Buffer {
chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
به جای `ScreenChar` ، ما اکنون از `Volatile` استفاده می کنیم. (نوع `Volatile`، [generic] است و می تواند (تقریباً) هر نوع را در خود قرار دهد). این اطمینان می دهد که ما به طور تصادفی نمی توانیم از طریق نوشتن "عادی" در آن بنویسیم. در عوض ، اکنون باید از متد `write` استفاده کنیم.
[generic]: https://doc.rust-lang.org/book/ch10-01-syntax.html
این بدان معنی است که ما باید متد `Writer::write_byte` خود را به روز کنیم:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
...
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code,
});
...
}
}
}
...
}
```
به جای انتساب عادی با استفاده از `=` ، اکنون ما از متد `write` استفاده می کنیم. این تضمین می کند که کامپایلر هرگز این نوشتن را بهینه نخواهد کرد.
### ماکروهای قالببندی
خوب است که از ماکروهای قالب بندی Rust نیز پشتیبانی کنید. به این ترتیب ، می توانیم انواع مختلفی مانند عدد صحیح یا شناور را به راحتی چاپ کنیم. برای پشتیبانی از آنها ، باید تریت [`core::fmt::Write`] را پیاده سازی کنیم. تنها متد مورد نیاز این تریت ،`write_str` است که کاملاً شبیه به متد `write_str` ما است ، فقط با نوع بازگشت `fmt::Result`:
[`core::fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
```rust
// in src/vga_buffer.rs
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
```
`Ok(())` فقط نتیجه `Ok` حاوی نوع `()` است.
اکنون ما می توانیم از ماکروهای قالب بندی داخلی راست یعنی `write!`/`writeln!` استفاده کنیم:
```rust
// in src/vga_buffer.rs
pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}
```
حالا شما باید یک `Hello! The numbers are 42 and 0.3333333333333333` در پایین صفحه ببینید. فراخوانی `write!` یک `Result` را برمی گرداند که در صورت عدم استفاده باعث هشدار می شود ، بنابراین ما تابع [`unwrap`] را روی آن فراخوانی می کنیم که در صورت بروز خطا پنیک می کند. این در مورد ما مشکلی ندارد ، زیرا نوشتن در بافر VGA هرگز شکست نمیخورد.
[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap
### خطوط جدید
در حال حاضر ، ما از خطوط جدید و کاراکتر هایی که دیگر در خط نمی گنجند چشم پوشی می کنیم. درعوض ما می خواهیم هر کاراکتر را یک خط به بالا منتقل کنیم (خط بالا حذف می شود) و دوباره از ابتدای آخرین خط شروع کنیم. برای انجام این کار ، ما یک پیاده سازی برای متد `new_line` در `Writer` اضافه می کنیم:
```rust
// in src/vga_buffer.rs
impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {/* TODO */}
}
```
ما تمام کاراکترهای صفحه را پیمایش می کنیم و هر کاراکتر را یک ردیف به بالا شیفت می دهیم. توجه داشته باشید که علامت گذاری دامنه (`..`) فاقد مقدار حد بالا است. ما همچنین سطر 0 را حذف می کنیم (اول محدوده از "1" شروع می شود) زیرا این سطر است که از صفحه به بیرون شیفت می شود.
برای تکمیل کد `newline` ، متد `clear_row` را اضافه می کنیم:
```rust
// in src/vga_buffer.rs
impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
}
```
این متد با جایگزینی تمام کاراکترها با یک کاراکتر فاصله ، یک سطر را پاک می کند.
## یک رابط گلوبال
برای فراهم کردن یک نویسنده گلوبال که بتواند به عنوان رابط از سایر ماژول ها بدون حمل نمونه `Writer` در اطراف استفاده شود ، سعی می کنیم یک `WRITER` ثابت ایجاد کنیم:
```rust
// in src/vga_buffer.rs
pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
```
با این حال ، اگر سعی کنیم اکنون آن را کامپایل کنیم ، خطاهای زیر رخ می دهد:
```
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/vga_buffer.rs:7:17
|
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0396]: raw pointers cannot be dereferenced in statics
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:13
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
```
برای فهمیدن آنچه در اینجا اتفاق می افتد ، باید بدانیم که ثابت ها(Statics) در زمان کامپایل مقداردهی اولیه می شوند ، برخلاف متغیرهای عادی که در زمان اجرا مقداردهی اولیه می شوند. مولفهای(component) از کامپایلر Rust که چنین عبارات مقداردهی اولیه را ارزیابی می کند ، “[const evaluator]” نامیده می شود. عملکرد آن هنوز محدود است ، اما کارهای گسترده ای برای گسترش آن در حال انجام است ، به عنوان مثال در “[Allow panicking in constants]” RFC.
[const evaluator]: https://rustc-dev-guide.rust-lang.org/const-eval.html
[Allow panicking in constants]: https://github.com/rust-lang/rfcs/pull/2345
مسئله در مورد `ColorCode::new` با استفاده از توابع [`const` functions] قابل حل است ، اما مشکل اساسی اینجاست که Rust's const evaluator قادر به تبدیل اشارهگرهای خام به رفرنس در زمان کامپایل نیست. شاید روزی جواب دهد ، اما تا آن زمان ، ما باید راه حل دیگری پیدا کنیم.
[`const` functions]: https://doc.rust-lang.org/reference/const_eval.html#const-functions
### استاتیکهای تنبل (Lazy Statics)
یکبار مقداردهی اولیه استاتیکها با توابع غیر ثابت یک مشکل رایج در راست است. خوشبختانه ، در حال حاضر راه حل خوبی در کرتی به نام [lazy_static] وجود دارد. این کرت ماکرو `lazy_static!` را فراهم می کند که یک `استاتیک` را با تنبلی مقداردهی اولیه می کند. به جای محاسبه مقدار آن در زمان کامپایل ، `استاتیک` به تنبلی هنگام اولین دسترسی به آن، خود را مقداردهی اولیه میکند. بنابراین ، مقداردهی اولیه در زمان اجرا اتفاق می افتد تا کد مقدار دهی اولیه پیچیده و دلخواه امکان پذیر باشد.
[lazy_static]: https://docs.rs/lazy_static/1.0.1/lazy_static/
بیایید کرت `lazy_static` را به پروژه خود اضافه کنیم:
```toml
# in Cargo.toml
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
```
ما به ویژگی `spin_no_std` نیاز داریم ، زیرا به کتابخانه استاندارد پیوند نمی دهیم.
با استفاده از `lazy_static` ، می توانیم WRITER ثابت خود را بدون مشکل تعریف کنیم:
```rust
// in src/vga_buffer.rs
use lazy_static::lazy_static;
lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}
```
با این حال ، این `WRITER` بسیار بی فایده است زیرا غیر قابل تغییر است. این بدان معنی است که ما نمی توانیم چیزی در آن بنویسیم (از آنجا که همه متد های نوشتن `&mut self` را در ورودی میگیرند). یک راه حل ممکن استفاده از [استاتیک قابل تغییر] است. اما پس از آن هر خواندن و نوشتن آن ناامن (unsafe) است زیرا می تواند به راحتی باعث data race و سایر موارد بد باشد. استفاده از `static mut` بسیار نهی شده است ، حتی پیشنهادهایی برای [حذف آن][remove static mut] وجود داشت. اما گزینه های دیگر چیست؟ ما می توانیم سعی کنیم از یک استاتیک تغییرناپذیر با نوع سلول مانند [RefCell] یا حتی [UnsafeCell] استفاده کنیم که [تغییر پذیری داخلی] را فراهم می کند. اما این انواع [Sync] نیستند (با دلیل کافی) ، بنابراین نمی توانیم از آنها در استاتیک استفاده کنیم.
[استاتیک قابل تغییر]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437
[RefCell]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt
[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html
[تغییر پذیری داخلی]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html
### Spinlocks
برای دستیابی به قابلیت تغییرپذیری داخلی همزمان (synchronized) ، کاربران کتابخانه استاندارد می توانند از [Mutex] استفاده کنند. هنگامی که منبع از قبل قفل شده است ، با مسدود کردن رشته ها ، امکان انحصار متقابل را فراهم می کند. اما هسته اصلی ما هیچ پشتیبانی از مسدود کردن یا حتی مفهومی از نخ ها ندارد ، بنابراین ما هم نمی توانیم از آن استفاده کنیم. با این وجود یک نوع کاملاً پایهای از mutex در علوم کامپیوتر وجود دارد که به هیچ ویژگی سیستم عاملی نیاز ندارد: [spinlock]. به جای مسدود کردن ، نخ ها سعی می کنند آن را بارها و بارها در یک حلقه قفل کنند و بنابراین زمان پردازنده را می سوزانند تا دوباره mutex آزاد شود.
[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html
[spinlock]: https://en.wikipedia.org/wiki/Spinlock
برای استفاده از spinning mutex ، می توانیم [کرت spin] را به عنوان یک وابستگی اضافه کنیم:
[کرت spin]: https://crates.io/crates/spin
```toml
# in Cargo.toml
[dependencies]
spin = "0.5.2"
```
سپس می توانیم از spinning Mutex برای افزودن [تغییر پذیری داخلی] امن به `WRITER` استاتیک خود استفاده کنیم:
```rust
// in src/vga_buffer.rs
use spin::Mutex;
...
lazy_static! {
pub static ref WRITER: Mutex = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
```
اکنون می توانیم تابع `print_something` را حذف کرده و مستقیماً از تابع`_start` خود چاپ کنیم:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
use core::fmt::Write;
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
loop {}
}
```
برای اینکه بتوانیم از توابع آن استفاده کنیم ، باید تریت `fmt::Write` را وارد کنیم.
### ایمنی
توجه داشته باشید که ما فقط یک بلوک ناامن در کد خود داریم که برای ایجاد رفرنس `Buffer` با اشاره به `0xb8000` لازم است. پس از آن ، تمام عملیات ایمن هستند. Rust به طور پیش فرض از بررسی مرزها در دسترسی به آرایه استفاده می کند ، بنابراین نمی توانیم به طور اتفاقی خارج از بافر بنویسیم. بنابراین ، ما شرایط مورد نیاز را در سیستم نوع انجام میدهیم و قادر به ایجاد یک رابط ایمن به خارج هستیم.
### یک ماکروی println
اکنون که یک نویسنده گلوبال داریم ، می توانیم یک ماکرو `println` اضافه کنیم که می تواند از هر کجا در کد استفاده شود. [سینتکس ماکروی] راست کمی عجیب است ، بنابراین ما سعی نمی کنیم ماکرو را از ابتدا بنویسیم. در عوض به سورس [ماکروی `println!`] در کتابخانه استاندارد نگاه می کنیم:
[سینتکس ماکروی]: https://doc.rust-lang.org/nightly/book/ch20-05-macros.html#declarative-macros-for-general-metaprogramming
[ماکروی `println!`]: https://doc.rust-lang.org/nightly/std/macro.println!.html
```rust
#[macro_export]
macro_rules! println {
() => (print!("\n"));
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}
```
ماکروها از طریق یک یا چند قانون تعریف می شوند که شبیه بازوهای `match` هستند. ماکرو `println` دارای دو قانون است: اولین قانون برای فراخوانی های بدون آرگمان است (به عنوان مثال: `println!()`) ، که به `print!("\n")` گسترش می یابد، بنابراین فقط یک خط جدید را چاپ می کند. قانون دوم برای فراخوانی هایی با پارامترهایی مانند `println!("Hello")` یا `println!("Number: {}", 4)` است. همچنین با فراخوانی کل آرگومان ها و یک خط جدید `\n` اضافی در انتها ، به فراخوانی ماکرو `print!` گسترش می یابد.
ویژگی `#[macro_export]` ماکرو را برای کل کرت (نه فقط ماژولی که تعریف شده است) و کرت های خارجی در دسترس قرار می دهد. همچنین ماکرو را در ریشه کرت قرار می دهد ، به این معنی که ما باید ماکرو را به جای `std::macros::println` از طریق `use std::println` وارد کنیم.
[ماکرو `print!`] به این صورت تعریف می شود:
[ماکرو `print!`]: https://doc.rust-lang.org/nightly/std/macro.print!.html
```rust
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
```
ماکرو به فراخوانی [تابع `_print`] در ماژول `io` گسترش می یابد. [متغیر `$crate`] تضمین می کند که ماکرو هنگام گسترش در `std` در زمان استفاده در کرت های دیگر، در خارج از کرت `std` نیز کار می کند.
[ماکرو `format_args`] از آرگمان های داده شده یک نوع [fmt::Arguments] را می سازد که به `_print` ارسال می شود. [تابع `_print`] از کتابخانه استاندارد،`print_to` را فراخوانی می کند ، که بسیار پیچیده است زیرا از دستگاه های مختلف `Stdout` پشتیبانی می کند. ما به این پیچیدگی احتیاج نداریم زیرا فقط می خواهیم در بافر VGA چاپ کنیم.
[تابع `_print`]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698
[متغیر `$crate`]: https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate
[ماکرو `format_args`]: https://doc.rust-lang.org/nightly/std/macro.format_args.html
[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html
برای چاپ در بافر VGA ، ما فقط ماکروهای `println!` و `print!` را کپی می کنیم ، اما آنها را اصلاح می کنیم تا از تابع `_print` خود استفاده کنیم:
```rust
// in src/vga_buffer.rs
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
```
چیزی که ما از تعریف اصلی `println` تغییر دادیم این است که فراخوانی ماکرو `print!` را با پیشوند `$crate` انجام می دهیم. این تضمین می کند که اگر فقط می خواهیم از `println` استفاده کنیم ، نیازی به وارد کردن ماکرو `print!` هم نداشته باشیم.
مانند کتابخانه استاندارد ، ویژگی `#[macro_export]` را به هر دو ماکرو اضافه می کنیم تا در همه جای کرت ما در دسترس باشند. توجه داشته باشید که این ماکروها را در فضای نام ریشه کرت قرار می دهد ، بنابراین وارد کردن آنها از طریق `use crate::vga_buffer::println` کار نمی کند. در عوض ، ما باید `use crate::println` را استفاده کنیم.
تابع `_print` نویسنده (`WRITER`) استاتیک ما را قفل می کند و متد`write_fmt` را روی آن فراخوانی می کند. این متد از تریت `Write` است ، ما باید این تریت را وارد کنیم. اگر چاپ موفقیت آمیز نباشد ، `unwrap()` اضافی در انتها باعث پنیک میشود. اما از آنجا که ما همیشه `Ok` را در `write_str` برمی گردانیم ، این اتفاق نمی افتد.
از آنجا که ماکروها باید بتوانند از خارج از ماژول، `_print` را فراخوانی کنند، تابع باید عمومی (public) باشد. با این حال ، از آنجا که این جزئیات پیاده سازی را خصوصی (private) در نظر می گیریم، [ویژگی `doc(hidden)`] را اضافه می کنیم تا از مستندات تولید شده پنهان شود.
[ویژگی `doc(hidden)`]: https://doc.rust-lang.org/nightly/rustdoc/write-documentation/the-doc-attribute.html#hidden
### Hello World توسط `println`
اکنون می توانیم از `println` در تابع `_start` استفاده کنیم:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() {
println!("Hello World{}", "!");
loop {}
}
```
توجه داشته باشید که ما مجبور نیستیم ماکرو را در تابع اصلی وارد کنیم ، زیرا در حال حاضر در فضای نام ریشه موجود است.
همانطور که انتظار می رفت ، اکنون یک _“Hello World!”_ روی صفحه مشاهده می کنیم:

### چاپ پیام های پنیک
اکنون که ماکرو `println` را داریم ، می توانیم از آن در تابع پنیک برای چاپ پیام و مکان پنیک استفاده کنیم:
```rust
// in main.rs
/// This function is called on panic.
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
```
اکنون وقتی که `panic!("Some panic message");` را در تابع `_start` خود اضافه میکنیم ، خروجی زیر را می گیریم:

بنابراین ما نه تنها میدانیم که یک پنیک رخ داده است ، بلکه پیام پنیک و اینکه در کجای کد رخ داده است را نیز میدانیم.
## خلاصه
در این پست با ساختار بافر متن VGA و نحوه نوشتن آن از طریق نگاشت حافظه در آدرس `0xb8000` آشنا شدیم. ما یک ماژول راست ایجاد کردیم که عدم امنیت نوشتن را در این بافر نگاشت حافظه شده را محصور می کند و یک رابط امن و راحت به خارج ارائه می دهد.
همچنین دیدیم که به لطف کارگو ، اضافه کردن وابستگی به کتابخانه های دیگران چقدر آسان است. دو وابستگی که اضافه کردیم ، `lazy_static` و`spin` ، در توسعه سیستم عامل بسیار مفید هستند و ما در پست های بعدی از آنها در مکان های بیشتری استفاده خواهیم کرد.
## بعدی چیست؟
در پست بعدی نحوه راه اندازی چارچوب تست واحد (Unit Test) راست توضیح داده شده است. سپس از این پست چند تست واحد اساسی برای ماژول بافر VGA ایجاد خواهیم کرد.
================================================
FILE: blog/content/edition-2/posts/03-vga-text-buffer/index.fr.md
================================================
+++
title = "Mode Texte VGA"
weight = 3
path = "fr/vga-text-mode"
date = 2018-02-26
[extra]
chapter = "Bare Bones"
# Please update this when updating the translation
translation_based_on_commit = "211f460251cd332905225c93eb66b1aff9f4aefd"
# GitHub usernames of the people that translated this post
translators = ["YaogoGerard"]
+++
Le [mode texte VGA] est une manière simple d'afficher du texte à l'écran. Dans cet article, nous créons une interface qui rend son utilisation sûre et simple en encapsulant toutes les parties non sûres dans un module séparé. Nous implémentons également le support des [macros de formatage] de Rust.
[mode texte VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
[macros de formatage]: https://doc.rust-lang.org/std/fmt/#related-macros
Ce blog est développé ouvertement sur [GitHub]. Si vous avez des problèmes ou des questions, veuillez ouvrir un ticket là-bas. Vous pouvez également laisser des commentaires [en bas de page]. Le code source complet de cet article se trouve dans la branche [`post-03`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[en bas de page]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
## Le tampon de texte VGA
Pour afficher un caractère à l'écran en mode texte VGA, il faut l'écrire dans le tampon de texte du matériel VGA. Le tampon de texte VGA est un tableau à deux dimensions typiquement de 25 lignes et 80 colonnes, qui est directement rendu à l'écran. Chaque entrée du tableau décrit un caractère à l'écran via le format suivant :
| Bit(s) | Valeur |
| ------ | --------------------------- |
| 0-7 | Point de code ASCII |
| 8-11 | Couleur de premier plan |
| 12-14 | Couleur d'arrière-plan |
| 15 | Clignotement |
Le premier octet représente le caractère qui doit être affiché dans l'[encodage ASCII]. Pour être plus précis, ce n'est pas exactement l'ASCII, mais un jeu de caractères nommé [_page de codes 437_] avec quelques caractères supplémentaires et de légères modifications. Par souci de simplicité, nous continuerons à l'appeler caractère ASCII dans cet article.
[encodage ASCII]: https://en.wikipedia.org/wiki/ASCII
[_page de codes 437_]: https://en.wikipedia.org/wiki/Code_page_437
Le deuxième octet définit comment le caractère est affiché. Les quatre premiers bits définissent la couleur de premier plan, les trois bits suivants la couleur d'arrière-plan, et le dernier bit si le caractère doit clignoter. Les couleurs suivantes sont disponibles :
| Nombre | Couleur | Nombre + Bit de Luminosité | Couleur Claire |
| ------ | ---------------- | -------------------------- | -------------- |
| 0x0 | Noir | 0x8 | Gris Foncé |
| 0x1 | Bleu | 0x9 | Bleu Clair |
| 0x2 | Vert | 0xa | Vert Clair |
| 0x3 | Cyan | 0xb | Cyan Clair |
| 0x4 | Rouge | 0xc | Rouge Clair |
| 0x5 | Magenta | 0xd | Rose |
| 0x6 | Marron | 0xe | Jaune |
| 0x7 | Gris Clair | 0xf | Blanc |
Le bit 4 est le _bit de luminosité_, qui transforme, par exemple, le bleu en bleu clair. Pour la couleur d'arrière-plan, ce bit est réutilisé comme bit de clignotement.
Le tampon de texte VGA est accessible via une [entrée-sortie mappée en mémoire] à l'adresse `0xb8000`. Cela signifie que les lectures et écritures à cette adresse n'accèdent pas à la RAM mais accèdent directement au tampon de texte sur le matériel VGA. Cela signifie que nous pouvons le lire et l'écrire via des opérations mémoire normales à cette adresse.
[entrée-sortie mappée en mémoire]: https://en.wikipedia.org/wiki/Memory-mapped_I/O
Notez que le matériel mappé en mémoire peut ne pas supporter toutes les opérations RAM normales. Par exemple, un périphérique pourrait ne supporter que des lectures octet par octet et renvoyer des données incohérentes si un `u64` est lu. Heureusement, le tampon de texte [supporte les lectures et écritures normales], nous n'avons donc pas à le traiter de manière spéciale.
[supporte les lectures et écritures normales]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip
## Un module Rust
Maintenant que nous savons comment fonctionne le tampon VGA, nous pouvons créer un module Rust pour gérer l'affichage :
```rust
// dans src/main.rs
mod vga_buffer;
```
Pour le contenu de ce module, nous créons un nouveau fichier `src/vga_buffer.rs`. Tout le code ci-dessous va dans notre nouveau module (sauf indication contraire).
### Couleurs
Tout d'abord, nous représentons les différentes couleurs à l'aide d'une énumération :
```rust
// dans src/vga_buffer.rs
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
```
Nous utilisons ici une [énumération de style C] pour spécifier explicitement le numéro de chaque couleur. Grâce à l'attribut `repr(u8)`, chaque variante de l'énumération est stockée sous forme de `u8`. En réalité, 4 bits seraient suffisants, mais Rust n'a pas de type `u4`.
[énumération de style C]: https://doc.rust-lang.org/rust-by-example/custom_types/enum/c_like.html
Normalement, le compilateur émettrait un avertissement pour chaque variante inutilisée. En utilisant l'attribut `#[allow(dead_code)]`, nous désactivons ces avertissements pour l'énumération `Color`.
En [dérivant] les traits [`Copy`], [`Clone`], [`Debug`], [`PartialEq`] et [`Eq`], nous activons la [sémantique de copie] pour le type et le rendons imprimable et comparable.
[dérivant]: https://doc.rust-lang.org/rust-by-example/trait/derive.html
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
[`PartialEq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html
[`Eq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html
[sémantique de copie]: https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types
Pour représenter un code couleur complet qui spécifie les couleurs de premier plan et d'arrière-plan, nous créons un [newtype] au-dessus de `u8` :
[newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
```rust
// dans src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
```
La structure `ColorCode` contient l'octet de couleur complet, contenant les couleurs de premier plan et d'arrière-plan. Comme précédemment, nous dérivons les traits `Copy` et `Debug` pour celle-ci. Pour garantir que `ColorCode` a exactement la même disposition de données qu'un `u8`, nous utilisons l'attribut [`repr(transparent)`].
[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent
### Tampon de texte
Nous pouvons maintenant ajouter des structures pour représenter un caractère d'écran et le tampon de texte :
```rust
// dans src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
Étant donné que l'ordre des champs dans les structures par défaut est indéfini en Rust, nous avons besoin de l'attribut [`repr(C)`]. Il garantit que les champs de la structure sont disposés exactement comme dans une structure C et garantit ainsi l'ordre correct des champs. Pour la structure `Buffer`, nous utilisons à nouveau [`repr(transparent)`] pour nous assurer qu'elle a la même disposition en mémoire que son champ unique.
[`repr(C)`]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc
Pour écrire réellement à l'écran, nous créons maintenant un type writer :
```rust
// dans src/vga_buffer.rs
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
```
Le writer écrira toujours sur la dernière ligne et décalera les lignes vers le haut lorsqu'une ligne est pleine (ou sur `\n`). Le champ `column_position` garde la trace de la position actuelle dans la dernière ligne. Les couleurs actuelles de premier plan et d'arrière-plan sont spécifiées par `color_code` et une référence au tampon VGA est stockée dans `buffer`. Notez que nous avons besoin d'une [durée de vie explicite] ici pour indiquer au compilateur combien de temps la référence est valide. La durée de vie [`'static`] spécifie que la référence est valide pendant toute la durée d'exécution du programme (ce qui est vrai pour le tampon de texte VGA).
[durée de vie explicite]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax
[`'static`]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
### Affichage
Nous pouvons maintenant utiliser le `Writer` pour modifier les caractères du tampon. Tout d'abord, nous créons une méthode pour écrire un seul octet ASCII :
```rust
// dans src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}
fn new_line(&mut self) {/* TODO */}
}
```
Si l'octet est l'octet de [nouvelle ligne] `\n`, le writer n'affiche rien. Au lieu de cela, il appelle une méthode `new_line`, que nous implémenterons plus tard. Les autres octets sont affichés à l'écran dans le deuxième cas `match`.
[nouvelle ligne]: https://en.wikipedia.org/wiki/Newline
Lors de l'affichage d'un octet, le writer vérifie si la ligne actuelle est pleine. Dans ce cas, un appel à `new_line` est utilisé pour passer à la ligne suivante. Ensuite, il écrit un nouveau `ScreenChar` dans le tampon à la position actuelle. Enfin, la position de colonne actuelle est avancée.
Pour afficher des chaînes entières, nous pouvons les convertir en octets et les afficher un par un :
```rust
// dans src/vga_buffer.rs
impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// octet ASCII imprimable ou nouvelle ligne
0x20..=0x7e | b'\n' => self.write_byte(byte),
// ne fait pas partie de la plage ASCII imprimable
_ => self.write_byte(0xfe),
}
}
}
}
```
Le tampon de texte VGA ne prend en charge que l'ASCII et les octets supplémentaires de la [page de codes 437]. Les chaînes Rust sont en [UTF-8] par défaut, elles peuvent donc contenir des octets qui ne sont pas pris en charge par le tampon de texte VGA. Nous utilisons un `match` pour différencier les octets ASCII imprimables (une nouvelle ligne ou tout ce qui se trouve entre un caractère espace et un caractère `~`) et les octets non imprimables. Pour les octets non imprimables, nous affichons un caractère `■`, qui a le code hexadécimal `0xfe` sur le matériel VGA.
[page de codes 437]: https://en.wikipedia.org/wiki/Code_page_437
[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm
#### Essayons !
Pour écrire quelques caractères à l'écran, vous pouvez créer une fonction temporaire :
```rust
// dans src/vga_buffer.rs
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}
```
Elle crée d'abord un nouveau Writer qui pointe vers le tampon VGA à `0xb8000`. La syntaxe pour cela peut sembler un peu étrange : D'abord, nous convertissons l'entier `0xb8000` en [pointeur brut] mutable. Ensuite, nous le convertissons en référence mutable en le déréférençant (via `*`) et en l'empruntant à nouveau immédiatement (via `&mut`). Cette conversion nécessite un [bloc `unsafe`], car le compilateur ne peut pas garantir que le pointeur brut est valide.
[pointeur brut]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[bloc `unsafe`]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
Ensuite, elle écrit l'octet `b'H'`. Le préfixe `b` crée un [littéral d'octet], qui représente un caractère ASCII. En écrivant les chaînes `"ello "` et `"Wörld!"`, nous testons notre méthode `write_string` et la gestion des caractères non imprimables. Pour voir la sortie, nous devons appeler la fonction `print_something` depuis notre fonction `_start` :
```rust
// dans src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
vga_buffer::print_something();
loop {}
}
```
Lorsque nous exécutons notre projet maintenant, un `Hello W■■rld!` devrait être affiché dans le coin _inférieur_ gauche de l'écran en jaune :
[littéral d'octet]: https://doc.rust-lang.org/reference/tokens.html#byte-literals

Remarquez que le `ö` est affiché sous forme de deux caractères `■`. C'est parce que `ö` est représenté par deux octets en [UTF-8], qui ne se trouvent pas tous les deux dans la plage ASCII imprimable. En fait, c'est une propriété fondamentale de l'UTF-8 : les octets individuels des valeurs multi-octets ne sont jamais de l'ASCII valide.
### Volatile
Nous venons de voir que notre message a été affiché correctement. Cependant, cela pourrait ne pas fonctionner avec les futurs compilateurs Rust qui optimisent de manière plus agressive.
Le problème est que nous écrivons uniquement dans le `Buffer` et ne le lisons plus jamais. Le compilateur ne sait pas que nous accédons réellement à la mémoire du tampon VGA (au lieu de la RAM normale) et ne sait rien de l'effet secondaire selon lequel certains caractères apparaissent à l'écran. Il pourrait donc décider que ces écritures sont inutiles et peuvent être omises. Pour éviter cette optimisation erronée, nous devons spécifier que ces écritures sont _[volatile]_. Cela indique au compilateur que l'écriture a des effets secondaires et ne doit pas être optimisée.
[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
Afin d'utiliser des écritures volatiles pour le tampon VGA, nous utilisons la bibliothèque [volatile][volatile crate]. Cette _crate_ (c'est ainsi que les paquets sont appelés dans le monde Rust) fournit un type wrapper `Volatile` avec des méthodes `read` et `write`. Ces méthodes utilisent en interne les fonctions [read_volatile] et [write_volatile] de la bibliothèque core et garantissent ainsi que les lectures/écritures ne sont pas optimisées.
[volatile crate]: https://docs.rs/volatile
[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html
[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html
Nous pouvons ajouter une dépendance à la crate `volatile` en l'ajoutant à la section `dependencies` de notre `Cargo.toml` :
```toml
# dans Cargo.toml
[dependencies]
volatile = "0.2.6"
```
Assurez-vous de spécifier la version `0.2.6` de `volatile`. Les versions plus récentes de la crate ne sont pas compatibles avec cet article.
`0.2.6` est le numéro de version [sémantique]. Pour plus d'informations, consultez le guide [Specifying Dependencies] de la documentation cargo.
[sémantique]: https://semver.org/
[Specifying Dependencies]: https://doc.crates.io/specifying-dependencies.html
Utilisons-la pour rendre les écritures dans le tampon VGA volatiles. Nous mettons à jour notre type `Buffer` comme suit :
```rust
// dans src/vga_buffer.rs
use volatile::Volatile;
struct Buffer {
chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
Au lieu d'un `ScreenChar`, nous utilisons maintenant un `Volatile`. (Le type `Volatile` est [générique] et peut envelopper (presque) n'importe quel type). Cela garantit que nous ne pouvons pas écrire dedans accidentellement de manière "normale". Au lieu de cela, nous devons maintenant utiliser la méthode `write`.
[générique]: https://doc.rust-lang.org/book/ch10-01-syntax.html
Cela signifie que nous devons mettre à jour notre méthode `Writer::write_byte` :
```rust
// dans src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
...
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code,
});
...
}
}
}
...
}
```
Au lieu d'une affectation typique utilisant `=`, nous utilisons maintenant la méthode `write`. Maintenant, nous pouvons garantir que le compilateur n'optimisera jamais cette écriture.
### Macros de formatage
Il serait agréable de prendre en charge les macros de formatage de Rust également. De cette façon, nous pouvons facilement afficher différents types, comme des entiers ou des flottants. Pour les prendre en charge, nous devons implémenter le trait [`core::fmt::Write`]. La seule méthode requise de ce trait est `write_str`, qui ressemble beaucoup à notre méthode `write_string`, juste avec un type de retour `fmt::Result` :
[`core::fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
```rust
// dans src/vga_buffer.rs
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
```
Le `Ok(())` est juste un Result `Ok` contenant le type `()`.
Maintenant, nous pouvons utiliser les macros de formatage intégrées de Rust `write!`/`writeln!` :
```rust
// dans src/vga_buffer.rs
pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}
```
Maintenant, vous devriez voir un `Hello! The numbers are 42 and 0.3333333333333333` en bas de l'écran. L'appel à `write!` renvoie un `Result` qui provoque un avertissement s'il n'est pas utilisé, nous appelons donc la fonction [`unwrap`] dessus, qui panique si une erreur se produit. Ce n'est pas un problème dans notre cas, car les écritures dans le tampon VGA n'échouent jamais.
[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap
### Nouvelles lignes
Pour le moment, nous ignorons simplement les nouvelles lignes et les caractères qui ne rentrent plus dans la ligne. Au lieu de cela, nous voulons déplacer chaque caractère d'une ligne vers le haut (la ligne supérieure est supprimée) et recommencer au début de la dernière ligne. Pour ce faire, nous ajoutons une implémentation pour la méthode `new_line` de `Writer` :
```rust
// dans src/vga_buffer.rs
impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {/* TODO */}
}
```
Nous itérons sur tous les caractères de l'écran et déplaçons chaque caractère d'une ligne vers le haut. Notez que la borne supérieure de la notation de plage (`..`) est exclusive. Nous omettons également la 0ème ligne (la première plage commence à `1`) car c'est la ligne qui est décalée hors de l'écran.
Pour terminer le code de nouvelle ligne, nous ajoutons la méthode `clear_row` :
```rust
// dans src/vga_buffer.rs
impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
}
```
Cette méthode efface une ligne en écrasant tous ses caractères par un caractère espace.
## Une interface globale
Pour fournir un writer global qui peut être utilisé comme interface depuis d'autres modules sans transporter une instance `Writer`, nous essayons de créer un `WRITER` statique :
```rust
// dans src/vga_buffer.rs
pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
```
Cependant, si nous essayons de le compiler maintenant, les erreurs suivantes se produisent :
```
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/vga_buffer.rs:7:17
|
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0396]: raw pointers cannot be dereferenced in statics
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:13
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
```
Pour comprendre ce qui se passe ici, nous devons savoir que les statiques sont initialisés au moment de la compilation, contrairement aux variables normales qui sont initialisées au moment de l'exécution. Le composant du compilateur Rust qui évalue ces expressions d'initialisation est appelé le "[const evaluator]". Sa fonctionnalité est encore limitée, mais il y a un travail en cours pour l'étendre, par exemple dans la RFC "[Allow panicking in constants]".
[const evaluator]: https://rustc-dev-guide.rust-lang.org/const-eval.html
[Allow panicking in constants]: https://github.com/rust-lang/rfcs/pull/2345
Le problème avec `ColorCode::new` serait résoluble en utilisant des [fonctions `const`], mais le problème fondamental ici est que l'évaluateur const de Rust n'est pas capable de convertir les pointeurs bruts en références au moment de la compilation. Peut-être que cela fonctionnera un jour, mais d'ici là, nous devons trouver une autre solution.
[fonctions `const`]: https://doc.rust-lang.org/reference/const_eval.html#const-functions
### Lazy Statics
L'initialisation unique de statiques avec des fonctions non-const est un problème courant en Rust. Heureusement, il existe déjà une bonne solution dans une crate nommée [lazy_static]. Cette crate fournit une macro `lazy_static!` qui définit un `static` initialisé paresseusement. Au lieu de calculer sa valeur au moment de la compilation, le `static` s'initialise paresseusement lorsqu'il est accédé pour la première fois. Ainsi, l'initialisation se produit au moment de l'exécution, de sorte qu'un code d'initialisation arbitrairement complexe est possible.
[lazy_static]: https://docs.rs/lazy_static/1.0.1/lazy_static/
Ajoutons la crate `lazy_static` à notre projet :
```toml
# dans Cargo.toml
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
```
Nous avons besoin de la fonctionnalité `spin_no_std`, car nous ne lions pas la bibliothèque standard.
Avec `lazy_static`, nous pouvons définir notre `WRITER` statique sans problème :
```rust
// dans src/vga_buffer.rs
use lazy_static::lazy_static;
lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}
```
Cependant, ce `WRITER` est assez inutile car il est immuable. Cela signifie que nous ne pouvons rien y écrire (puisque toutes les méthodes d'écriture prennent `&mut self`). Une solution possible serait d'utiliser un [static mutable]. Mais alors chaque lecture et écriture serait unsafe car cela pourrait facilement introduire des courses de données et d'autres mauvaises choses. L'utilisation de `static mut` est fortement déconseillée. Il y a même eu des propositions pour [le supprimer][remove static mut]. Mais quelles sont les alternatives ? Nous pourrions essayer d'utiliser un static immuable avec un type de cellule comme [RefCell] ou même [UnsafeCell] qui fournit une [mutabilité intérieure]. Mais ces types ne sont pas [Sync] (pour de bonnes raisons), nous ne pouvons donc pas les utiliser dans des statiques.
[static mutable]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437
[RefCell]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt
[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html
[mutabilité intérieure]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html
### Spinlocks
Pour obtenir une mutabilité intérieure synchronisée, les utilisateurs de la bibliothèque standard peuvent utiliser [Mutex]. Il fournit une exclusion mutuelle en bloquant les threads lorsque la ressource est déjà verrouillée. Mais notre noyau de base n'a aucun support de blocage ni même de concept de threads, nous ne pouvons donc pas l'utiliser non plus. Cependant, il existe un type de mutex très basique en informatique qui ne nécessite aucune fonctionnalité du système d'exploitation : le [spinlock]. Au lieu de bloquer, les threads essaient simplement de le verrouiller encore et encore dans une boucle serrée, brûlant ainsi du temps CPU jusqu'à ce que le mutex soit à nouveau libre.
[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html
[spinlock]: https://en.wikipedia.org/wiki/Spinlock
Pour utiliser un mutex tournant, nous pouvons ajouter la [crate spin] comme dépendance :
[crate spin]: https://crates.io/crates/spin
```toml
# dans Cargo.toml
[dependencies]
spin = "0.5.2"
```
Ensuite, nous pouvons utiliser le mutex tournant pour ajouter une [mutabilité intérieure] sûre à notre `WRITER` statique :
```rust
// dans src/vga_buffer.rs
use spin::Mutex;
...
lazy_static! {
pub static ref WRITER: Mutex = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
```
Maintenant, nous pouvons supprimer la fonction `print_something` et afficher directement depuis notre fonction `_start` :
```rust
// dans src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
use core::fmt::Write;
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
loop {}
}
```
Nous devons importer le trait `fmt::Write` pour pouvoir utiliser ses fonctions.
### Sécurité
Notez que nous n'avons qu'un seul bloc unsafe dans notre code, qui est nécessaire pour créer une référence `Buffer` pointant vers `0xb8000`. Ensuite, toutes les opérations sont sûres. Rust utilise la vérification des limites pour les accès aux tableaux par défaut, nous ne pouvons donc pas écrire accidentellement en dehors du tampon. Ainsi, nous avons encodé les conditions requises dans le système de types et sommes capables de fournir une interface sûre vers l'extérieur.
### Une macro println
Maintenant que nous avons un writer global, nous pouvons ajouter une macro `println` qui peut être utilisée n'importe où dans la base de code. La [syntaxe de macro] de Rust est un peu étrange, nous n'essaierons donc pas d'écrire une macro à partir de zéro. Au lieu de cela, nous regardons la source de la [macro `println!`] dans la bibliothèque standard :
[syntaxe de macro]: https://doc.rust-lang.org/nightly/book/ch20-05-macros.html#declarative-macros-for-general-metaprogramming
[macro `println!`]: https://doc.rust-lang.org/nightly/std/macro.println!.html
```rust
#[macro_export]
macro_rules! println {
() => (print!("\n"));
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}
```
Les macros sont définies par une ou plusieurs règles, similaires aux branches `match`. La macro `println` a deux règles : La première règle est pour les invocations sans arguments, par exemple `println!()`, qui est développée en `print!("\n")` et affiche donc juste une nouvelle ligne. La deuxième règle est pour les invocations avec des paramètres tels que `println!("Hello")` ou `println!("Number: {}", 4)`. Elle est également développée en une invocation de la macro `print!`, passant tous les arguments et une nouvelle ligne supplémentaire `\n` à la fin.
L'attribut `#[macro_export]` rend la macro disponible pour toute la crate (pas seulement le module dans lequel elle est définie) et les crates externes. Il place également la macro à la racine de la crate, ce qui signifie que nous devons importer la macro via `use std::println` au lieu de `std::macros::println`.
La [macro `print!`] est définie comme :
[macro `print!`]: https://doc.rust-lang.org/nightly/std/macro.print!.html
```rust
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
```
La macro se développe en un appel de la [fonction `_print`] dans le module `io`. La [variable `$crate`] garantit que la macro fonctionne également en dehors de la crate `std` en se développant en `std` lorsqu'elle est utilisée dans d'autres crates.
La [macro `format_args`] construit un type [fmt::Arguments] à partir des arguments passés, qui est transmis à `_print`. La [fonction `_print`] de libstd appelle `print_to`, qui est assez compliquée car elle prend en charge différents périphériques `Stdout`. Nous n'avons pas besoin de cette complexité car nous voulons simplement afficher sur le tampon VGA.
[fonction `_print`]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698
[variable `$crate`]: https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate
[macro `format_args`]: https://doc.rust-lang.org/nightly/std/macro.format_args.html
[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html
Pour afficher sur le tampon VGA, nous copions simplement les macros `println!` et `print!`, mais les modifions pour utiliser notre propre fonction `_print` :
```rust
// dans src/vga_buffer.rs
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
```
Une chose que nous avons changée par rapport à la définition originale de `println` est que nous avons préfixé les invocations de la macro `print!` avec `$crate` également. Cela garantit que nous n'avons pas besoin d'importer la macro `print!` aussi si nous voulons seulement utiliser `println`.
Comme dans la bibliothèque standard, nous ajoutons l'attribut `#[macro_export]` aux deux macros pour les rendre disponibles partout dans notre crate. Notez que cela place les macros dans l'espace de noms racine de la crate, donc les importer via `use crate::vga_buffer::println` ne fonctionne pas. Au lieu de cela, nous devons faire `use crate::println`.
La fonction `_print` verrouille notre `WRITER` statique et appelle la méthode `write_fmt` dessus. Cette méthode provient du trait `Write`, que nous devons importer. Le `unwrap()` supplémentaire à la fin panique si l'affichage n'est pas réussi. Mais puisque nous retournons toujours `Ok` dans `write_str`, cela ne devrait pas se produire.
Comme les macros doivent pouvoir appeler `_print` depuis l'extérieur du module, la fonction doit être publique. Cependant, puisque nous considérons cela comme un détail d'implémentation privé, nous ajoutons l'[attribut `doc(hidden)`] pour le masquer de la documentation générée.
[attribut `doc(hidden)`]: https://doc.rust-lang.org/nightly/rustdoc/write-documentation/the-doc-attribute.html#hidden
### Hello World en utilisant `println`
Maintenant, nous pouvons utiliser `println` dans notre fonction `_start` :
```rust
// dans src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
loop {}
}
```
Notez que nous n'avons pas besoin d'importer la macro dans la fonction main, car elle vit déjà dans l'espace de noms racine.
Comme prévu, nous voyons maintenant un _"Hello World!"_ à l'écran :

### Affichage des messages de panique
Maintenant que nous avons une macro `println`, nous pouvons l'utiliser dans notre fonction de panique pour afficher le message de panique et l'emplacement de la panique :
```rust
// dans main.rs
/// Cette fonction est appelée en cas de panique.
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
```
Lorsque nous insérons maintenant `panic!("Some panic message");` dans notre fonction `_start`, nous obtenons la sortie suivante :

Nous savons donc non seulement qu'une panique s'est produite, mais aussi le message de panique et où dans le code cela s'est produit.
## Résumé
Dans cet article, nous avons appris la structure du tampon de texte VGA et comment il peut être écrit via le mappage mémoire à l'adresse `0xb8000`. Nous avons créé un module Rust qui encapsule le caractère unsafe de l'écriture dans ce tampon mappé en mémoire et présente une interface sûre et pratique vers l'extérieur.
Grâce à cargo, nous avons également vu à quel point il est facile d'ajouter des dépendances à des bibliothèques tierces. Les deux dépendances que nous avons ajoutées, `lazy_static` et `spin`, sont très utiles dans le développement d'OS et nous les utiliserons dans plus d'endroits dans les futurs articles.
## Et ensuite ?
Le prochain article explique comment configurer le framework de tests unitaires intégré de Rust. Nous créerons ensuite quelques tests unitaires de base pour le module de tampon VGA de cet article.
================================================
FILE: blog/content/edition-2/posts/03-vga-text-buffer/index.ja.md
================================================
+++
title = "VGAテキストモード"
weight = 3
path = "ja/vga-text-mode"
date = 2018-02-26
[extra]
# Please update this when updating the translation
translation_based_on_commit = "bd6fbcb1c36705b2c474d7fcee387bfea1210851"
# GitHub usernames of the people that translated this post
translators = ["swnakamura", "JohnTitor"]
+++
[VGAテキストモード][VGA text mode]は画面にテキストを出力するシンプルな方法です。この記事では、すべてのunsafeな要素を別のモジュールにカプセル化することで、それを安全かつシンプルに扱えるようにするインターフェースを作ります。また、Rustの[フォーマッティングマクロ][formatting macros]のサポートも実装します。
[VGA text mode]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
[formatting macros]: https://doc.rust-lang.org/std/fmt/#related-macros
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-03` ブランチ][post branch]にあります。
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
## VGAテキストバッファ
VGAテキストモードにおいて、文字を画面に出力するには、VGAハードウェアのテキストバッファにそれを書き込まないといけません。VGAテキストバッファは、普通25行と80列からなる2次元配列で、画面に直接書き出されます。それぞれの配列の要素は画面上の一つの文字を以下の形式で表現しています:
| ビット | 値 |
| ------ | -------------------------- |
| 0-7 | ASCII コードポイント |
| 8-11 | フォアグラウンド(前景)色 |
| 12-14 | バックグラウンド(背景)色 |
| 15 | 点滅 |
最初の1バイトは、出力されるべき文字を[ASCIIエンコーディング][ASCII encoding]で表します。正確に言うと、完全にASCIIではなく、[コードページ437][_code page 437_]という、いくつか文字が追加され、軽微な修正のなされたものです。簡単のため、この記事ではASCII文字と呼ぶことにします。
[ASCII encoding]: https://ja.wikipedia.org/wiki/ASCII
[_code page 437_]: https://ja.wikipedia.org/wiki/コードページ437
2つ目のバイトはその文字がどのように出力されるのかを定義します。最初の4ビットが前景色(訳注:文字自体の色)を、次の3ビットが背景色を、最後のビットがその文字が点滅するのかを決めます。以下の色を使うことができます:
| 数字 | 色 | 数字 + Bright Bit | Bright 色 |
| ---- | ------------ | ----------------- | ----------------------------------------------------------- |
| 0x0 | 黒 | 0x8 | 暗いグレー |
| 0x1 | 青 | 0x9 | 明るい青 |
| 0x2 | 緑 | 0xa | 明るい緑 |
| 0x3 | シアン | 0xb | 明るいシアン |
| 0x4 | 赤 | 0xc | 明るい赤 |
| 0x5 | マゼンタ | 0xd | ピンク |
| 0x6 | 茶色 | 0xe | 黄色 |
| 0x7 | 明るいグレー | 0xf | 白 |
4ビット目は **bright bit** で、これは(1になっているとき)たとえば青を明るい青に変えます。背景色については、このビットは点滅ビットとして再利用されています。
VGAテキストバッファはアドレス`0xb8000`に[memory-mapped I/O][memory-mapped I/O]を通じてアクセスできます。これは、このアドレスへの読み書きをしても、RAMではなく直接VGAハードウェアのテキストバッファにアクセスするということを意味します。つまり、このアドレスに対する通常のメモリ操作を通じて、テキストバッファを読み書きできるのです。
[memory-mapped I/O]: https://ja.wikipedia.org/wiki/メモリマップドI/O
メモリマップされたハードウェアは通常のRAM操作すべてをサポートしてはいないかもしれないということに注意してください。たとえば、デバイスはバイトずつの読み取りしかサポートしておらず、`u64`が読まれるとゴミデータを返すかもしれません。ありがたいことに、テキストバッファを特別なやり方で取り扱う必要がないよう、テキストバッファは[通常の読み書きをサポートしています][supports normal reads and writes]。
[supports normal reads and writes]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip
## Rustのモジュール
VGAバッファが動く仕組みを学んだので、さっそく画面出力を扱うRustのモジュールを作っていきます。
```rust
// in src/main.rs
mod vga_buffer;
```
このモジュールの中身のために、新しい`src/vga_buffer.rs`というファイルを作ります。このファイル以下のコードは、(そうならないよう指定されない限り)すべてこの新しいモジュールの中に入ります。
### 色
まず、様々な色をenumを使って表しましょう:
```rust
// in src/vga_buffer.rs
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
```
ここでは、それぞれの色の数を指定するのに[C言語ライクなenum][C-like enum]を使っています。`repr(u8)`属性のため、それぞれのenumのヴァリアントは`u8`として格納されています。実際には4ビットでも十分なのですが、Rustには`u4`型はありませんので。
[C-like enum]: https://doc.rust-jp.rs/rust-by-example-ja/custom_types/enum/c_like.html
通常、コンパイラは使われていないヴァリアントそれぞれに対して警告を発します。`#[allow(dead_code)]`属性を使うことで`Color` enumに対するそれらの警告を消すことができます。
[`Copy`]、[`Clone`]、[`Debug`]、[`PartialEq`]、および [`Eq`]を[derive][deriving]することによって、この型の[コピーセマンティクス][copy semantics]を有効化し、この型を出力することと比較することを可能にします。
[deriving]: https://doc.rust-jp.rs/rust-by-example-ja/trait/derive.html
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
[`PartialEq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html
[`Eq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html
[copy semantics]: https://doc.rust-jp.rs/book-ja/appendix-03-derivable-traits.html#値を複製するcloneとcopy
前景と背景の色を指定する完全なカラーコードを表現するために、`u8`の上に[ニュータイプ][newtype]を作ります。
[newtype]: https://doc.rust-jp.rs/rust-by-example-ja/generics/new_types.html
```rust
// in src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
```
`ColorCode`構造体は前景色と背景色を持つので、完全なカラーコードを持ちます。前と同じように、`Copy`と`Debug`トレイトをこれにderiveします。`ColorCode`が`u8`と全く同じデータ構造を持つようにするために、[`repr(transparent)`]属性(訳注:翻訳当時、リンク先未訳)を使います。
[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent
### テキストバッファ
次に、画面上の文字とテキストバッファをそれぞれ表す構造体を追加していきます。
```rust
// in src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
Rustにおいて、デフォルトの構造体におけるフィールドの並べ方は未定義なので、[`repr(C)`]属性が必要になります。これは、構造体のフィールドがCの構造体と全く同じように並べられることを保証してくれるので、フィールドの並べ方が正しいと保証してくれるのです。`Buffer`構造体については、[`repr(transparent)`]をもう一度使うことで、その唯一のフィールドと同じメモリレイアウトを持つようにしています。
[`repr(C)`]: https://doc.rust-jp.rs/rust-nomicon-ja/other-reprs.html#reprc
実際に画面に書き出すため、writer型を作ります。
```rust
// in src/vga_buffer.rs
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
```
writerは常に最後の行に書き、行が一杯になったとき(もしくは`\n`を受け取った時)は1行上に送ります。`column_position`フィールドは、最後の行における現在の位置を持ちます。現在の前景および背景色は`color_code`によって指定されており、VGAバッファへの参照は`buffer`に格納されています。ここで、コンパイラにどのくらいの間参照が有効であるのかを教えるために[明示的なライフタイム][explicit lifetime]が必要になることに注意してください。[`'static`]ライフタイムは、その参照がプログラムの実行中ずっと有効であることを指定しています(これはVGAバッファについて正しいです)。
[explicit lifetime]: https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#ライフタイム注釈記法
[`'static`]: https://doc.rust-jp.rs/book-ja/ch10-03-lifetime-syntax.html#静的ライフタイム
### 出力する
では`Writer`を使ってバッファの文字を変更しましょう。まず一つのASCII文字を書くメソッドを作ります:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}
fn new_line(&mut self) {/* TODO */}
}
```
(引数の)バイトが[改行コード][newline]のバイトすなわち`\n`の場合は、writerは何も出力しません。代わりに、あとで実装する`new_line`メソッドを呼びます。他のバイトは、2つ目のマッチケースにおいて画面に出力されます。
[newline]: https://ja.wikipedia.org/wiki/%E6%94%B9%E8%A1%8C%E3%82%B3%E3%83%BC%E3%83%89
バイトを出力する時、writerは現在の行がいっぱいかをチェックします。その場合、行を折り返すために先に`new_line`の呼び出しが必要です。その後で現在の場所のバッファに新しい`ScreenChar`を書き込みます。最後に、現在の列の位置を進めます。
文字列全体を出力するには、バイト列に変換しひとつひとつ出力すればよいです:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// 出力可能なASCIIバイトか、改行コード
0x20..=0x7e | b'\n' => self.write_byte(byte),
// 出力可能なASCIIバイトではない
_ => self.write_byte(0xfe),
}
}
}
}
```
VGAテキストバッファはASCIIおよび[コードページ437][code page 437]にある追加のバイトのみをサポートしています。Rustの文字列はデフォルトでは[UTF-8]なのでVGAテキストバッファにはサポートされていないバイトを含んでいる可能性があります。matchを使って出力可能なASCIIバイト(改行コードか、空白文字から`~`文字の間のすべての文字)と出力不可能なバイトを分けています。出力不可能なバイトについては、文字`■`を出力します(これはVGAハードウェアにおいて16進コード`0xfe`を持っています)。
[code page 437]: https://ja.wikipedia.org/wiki/コードページ437
[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm
#### やってみよう!
適当な文字を画面に書き出すために、一時的に使う関数を作ってみましょう。
```rust
// in src/vga_buffer.rs
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}
```
この関数はまず、VGAバッファの`0xb8000`を指す新しいwriterを作ります。このための構文はやや奇妙に思われるかもしれません:まず、整数`0xb8000`を可変な[生ポインタ][raw pointer]にキャストします。次にこれを(`*`を使って)参照外しすることで可変な参照に変え、即座にそれを(`&mut`を使って)再び借用します。コンパイラはこの生ポインタが有効であることを保証できないので、この変換には[`unsafe`ブロック][`unsafe` block]が必要となります。
[raw pointer]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html#生ポインタを参照外しする
[`unsafe` block]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html
つぎに、この関数はそれにバイト`b'H'`を書きます。`b`というプレフィックスは、ASCII文字を表す[バイトリテラル][byte literal]を作ります。文字列`"ello "`と`"Wörld!"`を書くことで、私達の`write_string`関数と出力不可能な文字の処理をテストできます。出力を見るためには、`print_something`関数を`_start`関数から呼び出さなければなりません:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
vga_buffer::print_something();
loop {}
}
```
ここで、私達のプロジェクトを実行したら、`Hello W■■rld!`が画面の左 **下** に黄色で出力されるはずです。
[byte literal]: https://doc.rust-lang.org/reference/tokens.html#byte-literals

`ö`は2つの`■`という文字として出力されていることに注目してください。これは、`ö`は[UTF-8]において2つのバイトで表され、それらはどちらも出力可能なASCIIの範囲に収まっていないためです。実は、これはUTF-8の基本的な特性です:マルチバイト値のそれぞれのバイトは、絶対に有効なASCIIではないのです。
### Volatile
メッセージが正しく出力されるのを確認できました。しかし、より強力に最適化をする将来のRustコンパイラでは、これはうまく行かないかもしれません。
問題なのは、私達は`Buffer`に書き込むけれども、それから読み取ることはないということです。コンパイラは私達が実際には(通常のRAMの代わりに)VGAバッファメモリにアクセスしていることを知らないので、文字が画面に出力されるという副作用も全く知りません。なので、それらの書き込みは不要で省略可能と判断するかもしれません。この誤った最適化を回避するためには、それらの書き込みを **[volatile]** であると指定する必要があります。これは、この書き込みには副作用があり、最適化により取り除かれるべきではないとコンパイラに命令します。
[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
VGAバッファへのvolatileな書き込みをするために、[volatile][volatile crate]ライブラリを使います。この **クレート**(Rustではパッケージのことをこう呼びます)は、`read`と`write`というメソッドを持つ`Volatile`というラッパー型を提供します。これらのメソッドは、内部的にcoreライブラリの[read_volatile]と[write_volatile]関数を使い、読み込み・書き込みが最適化により取り除かれないことを保証します。
[volatile crate]: https://docs.rs/volatile
[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html
[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html
`Cargo.toml`の`dependencies`セクションに`volatile`クレートを追加することで、このクレートへの依存関係を追加できます。
```toml
# in Cargo.toml
[dependencies]
volatile = "0.2.6"
```
`0.2.6`は[セマンティック][semantic]バージョン番号です。詳しくは、cargoドキュメントの[依存関係の指定][Specifying Dependencies]を見てください。
[semantic]: https://semver.org/lang/ja/
[Specifying Dependencies]: https://doc.crates.io/specifying-dependencies.html
これを使って、VGAバッファへの書き込みをvolatileにしてみましょう。`Buffer`型を以下のように変更します:
```rust
// in src/vga_buffer.rs
use volatile::Volatile;
struct Buffer {
chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
`ScreenChar`の代わりに、`Volatile`を使っています(`Volatile`型は[ジェネリック][generic]であり(ほぼ)すべての型をラップできます)。これにより、間違って「普通の」書き込みをこれに対して行わないようにできます。これからは、代わりに`write`メソッドを使わなければいけません。
[generic]: https://doc.rust-lang.org/book/ch10-01-syntax.html
つまり、`Writer::write_byte`メソッドを更新しなければいけません:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
...
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code,
});
...
}
}
}
...
}
```
`=`を使った通常の代入の代わりに`write`メソッドを使っています。これにより、コンパイラがこの書き込みを最適化して取り除いてしまわないことが保証されます。
### フォーマットマクロ
Rustのフォーマットマクロもサポートすると良さそうです。そうすると、整数や浮動小数点数といった様々な型を簡単に出力できます。それらをサポートするためには、[`core::fmt::Write`]トレイトを実装する必要があります。このトレイトに必要なメソッドは`write_str`だけです。これは私達の`write_string`によく似ており、戻り値の型が`fmt::Result`であるだけです:
[`core::fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
```rust
// in src/vga_buffer.rs
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
```
`Ok(())`は、`()`型を持つ`Ok`、というだけです。
Rustの組み込みの`write!`/`writeln!`フォーマットマクロが使えるようになりました。
```rust
// in src/vga_buffer.rs
pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}
```
このようにすると、画面の下端に`Hello! The numbers are 42 and 0.3333333333333333`が見えるはずです。`write!`の呼び出しは`Result`を返し、これは放置されると警告を出すので、[`unwrap`]関数(エラーの際パニックします)をこれに呼び出しています。VGAバッファへの書き込みは絶対に失敗しないので、この場合これは問題ではありません。
[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap
### 改行
現在、改行や、行に収まらない文字は無視しています。その代わりに、すべての文字を一行上に持っていき(一番上の行は消去されます)、前の行の最初から始めるようにしたいです。これをするために、`Writer`の`new_line`というメソッドの実装を追加します。
```rust
// in src/vga_buffer.rs
impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {/* TODO */}
}
```
すべての画面の文字をイテレートし、それぞれの文字を一行上に動かします。範囲記法 (`..`) は上端を含まないことに注意してください。また、0行目はシフトしたら画面から除かれるので、この行についても省いています(最初の範囲は`1`から始まっています)。
newlineのプログラムを完成させるには、`clear_row`メソッドを追加すればよいです:
```rust
// in src/vga_buffer.rs
impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
}
```
このメソッドはすべての文字を空白文字で書き換えることによって行をクリアしてくれます。
## 大域的なインターフェース
`Writer`のインスタンスを動かさずとも他のモジュールからインターフェースとして使える、大域的なwriterを提供するために、静的な`WRITER`を作りましょう:
```rust
// in src/vga_buffer.rs
pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
```
しかし、これをコンパイルしようとすると、次のエラーが起こります:
```
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
(エラー[E0015]: static内における呼び出しは、定数関数、タプル構造体、タプルヴァリアントに限定されています)
--> src/vga_buffer.rs:7:17
|
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0396]: raw pointers cannot be dereferenced in statics
(エラー[E0396]: 生ポインタはstatic内では参照外しできません)
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
| (定数内での生ポインタの参照外し)
error[E0017]: references in statics may only refer to immutable values
(エラー[E0017]: static内における参照が参照してよいのは不変変数だけです)
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
| (staticは不変変数を必要とします)
error[E0017]: references in statics may only refer to immutable values
(エラー[E0017]: static内における参照が参照してよいのは不変変数だけです)
--> src/vga_buffer.rs:8:13
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
| (staticは不変変数を必要とします)
```
何が起こっているかを理解するには、実行時に初期化される通常の変数とは対照的に、静的変数はコンパイル時に初期化されるということを知らないといけません。この初期化表現を評価するRustコンパイラのコンポーネントを"[const evaluator]"といいます。この機能はまだ限定的ですが、「[定数内でpanicできるようにする][Allow panicking in constants]」RFCのように、この機能を拡張する作業が現在も進行しています。
[const evaluator]: https://rustc-dev-guide.rust-lang.org/const-eval.html
[Allow panicking in constants]: https://github.com/rust-lang/rfcs/pull/2345
`ColorCode::new`に関する問題は[`const`関数][`const` functions]を使って解決できるかもしれませんが、ここでの根本的な問題は、Rustのconst evaluatorがコンパイル時に生ポインタを参照へと変えることができないということです。いつかうまく行くようになるのかもしれませんが、その時までは、別の方法を行わなければなりません。
[`const` functions]: https://doc.rust-lang.org/reference/const_eval.html#const-functions
### 怠けた静的変数
定数でない関数で一度だけ静的変数を初期化したい、というのはRustにおいてよくある問題です。嬉しいことに、[lazy_static]というクレートにすでに良い解決方法が存在します。このクレートは、初期化が後回しにされる`static`を定義する`lazy_static!`マクロを提供します。その値をコンパイル時に計算する代わりに、この`static`は最初にアクセスされたときに初めて初期化します。したがって、初期化は実行時に起こるので、どんなに複雑な初期化プログラムも可能ということです。
**訳注:** lazyは、普通「遅延(評価)」などと訳されます。「怠けているので、アクセスされるギリギリまで評価されない」という英語のイメージを伝えたかったので上のように訳してみました。
[lazy_static]: https://docs.rs/lazy_static/1.0.1/lazy_static/
私達のプロジェクトに`lazy_static`クレートを追加しましょう:
```toml
# in Cargo.toml
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
```
標準ライブラリをリンクしないので、`spin_no_std`機能が必要です。
`lazy_static`を使えば、静的な`WRITER`が問題なく定義できます:
```rust
// in src/vga_buffer.rs
use lazy_static::lazy_static;
lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}
```
しかし、この`WRITER`は不変なので、全く使い物になりません。なぜならこれは、この`WRITER`に何も書き込めないということを意味するからです(私達のすべての書き込みメソッドは`&mut self`を取るからです)。ひとつの解決策には、[可変で静的な変数][mutable static]を使うということがあります。しかし、そうすると、あらゆる読み書きが容易にデータ競合やその他の良くないことを引き起こしてしまうので、それらがすべてunsafeになってしまいます。`static mut`を使うことも、[それを削除しようという提案][remove static mut]すらあることを考えると、できる限り避けたいです。しかし他に方法はあるのでしょうか?不変静的変数を[RefCell]や、果ては[UnsafeCell]のような、[内部可変性][interior mutability]を提供するcell型と一緒に使うという事も考えられます。しかし、それらの型は(ちゃんとした理由があって)[Sync]ではないので、静的変数で使うことはできません。
[mutable static]: https://doc.rust-jp.rs/book-ja/ch19-01-unsafe-rust.html#可変で静的な変数にアクセスしたり変更する
[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437
[RefCell]: https://doc.rust-jp.rs/book-ja/ch15-05-interior-mutability.html#refcelltで実行時に借用を追いかける
[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html
[interior mutability]: https://doc.rust-jp.rs/book-ja/ch15-05-interior-mutability.html
[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html
### スピンロック
同期された内部可変性を得るためには、標準ライブラリを使えるなら[Mutex]を使うことができます。これは、リソースがすでにロックされていた場合、スレッドをブロックすることにより相互排他性を提供します。しかし、私達の初歩的なカーネルにはブロックの機能はもちろんのこと、スレッドの概念すらないので、これも使うことはできません。しかし、コンピュータサイエンスの世界には、OSを必要としない非常に単純なmutexが存在するのです:それが[スピンロック][spinlock]です。スピンロックを使うと、ブロックする代わりに、スレッドは単純にリソースを何度も何度もロックしようとすることで、mutexが開放されるまでの間CPU時間を使い尽くします。
[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html
[spinlock]: https://ja.wikipedia.org/wiki/スピンロック
スピンロックによるmutexを使うには、[spinクレート][spin crate]への依存を追加すればよいです:
[spin crate]: https://crates.io/crates/spin
```toml
# in Cargo.toml
[dependencies]
spin = "0.5.2"
```
すると、スピンを使ったMutexを使うことができ、静的な`WRITER`に安全な[内部可変性][interior mutability]を追加できます。
```rust
// in src/vga_buffer.rs
use spin::Mutex;
...
lazy_static! {
pub static ref WRITER: Mutex = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
```
`print_something`関数を消して、`_start`関数から直接出力しましょう:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
use core::fmt::Write;
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
loop {}
}
```
`fmt::Write`トレイトの関数を使うためには、このトレイトをインポートする必要があります。
### 安全性
コードにはunsafeブロックが一つ(`0xb8000`を指す参照`Buffer`を作るために必要なもの)しかないことに注目してください。その後は、すべての命令が安全です。Rustは配列アクセスにはデフォルトで境界チェックを行うので、間違ってバッファの外に書き込んでしまうことはありえません。よって、必要とされる条件を型システムにすべて組み込んだので、安全なインターフェースを外部に提供できます。
### printlnマクロ
大域的なwriterを手に入れたので、プログラムのどこでも使える`println`マクロを追加できます。Rustの[マクロの構文][macro syntax]はすこしややこしいので、一からマクロを書くことはしません。代わりに、標準ライブラリで[`println!`マクロ][`println!` macro]のソースを見てみます:
[macro syntax]: https://doc.rust-lang.org/nightly/book/ch20-05-macros.html#declarative-macros-for-general-metaprogramming
[`println!` macro]: https://doc.rust-lang.org/nightly/std/macro.println!.html
```rust
#[macro_export]
macro_rules! println {
() => (print!("\n"));
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}
```
マクロは1つ以上のルールを使って定義されます(`match`アームと似ていますね)。`println`には2つのルールがあります:1つ目は引数なし呼び出し(例えば `println!()`)のためのもので、これは`print!("\n")`に展開され、よってただ改行を出力するだけになります。2つ目のルールはパラメータ付きの呼び出し(例えば`println!("Hello")`や `println!("Number: {}", 4)`)のためのものです。これも`print!`マクロの呼び出しへと展開され、すべての引数に加え、改行`\n`を最後に追加して渡します。
`#[macro_export]`属性はマクロを(その定義されたモジュールだけではなく)クレート全体および外部クレートで使えるようにします。また、これはマクロをクレートルートに置くため、`std::macros::println`の代わりに`use std::println`を使ってマクロをインポートしないといけないということを意味します。
[`print!`マクロ][`print!` macro]は以下のように定義されています:
[`print!` macro]: https://doc.rust-lang.org/nightly/std/macro.print!.html
```rust
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
```
このマクロは`io`モジュール内の[`_print`関数][`_print` function]の呼び出しへと展開しています。[`$crate`という変数][`$crate` variable]は、他のクレートで使われた際、`std`へと展開することによって、マクロが`std`クレートの外側で使われたとしてもうまく動くようにしてくれます。
[`format_args`マクロ][`format_args` macro]が与えられた引数から[fmt::Arguments]型を作り、これが`_print`へと渡されています。libstdの[`_print`関数]は`print_to`を呼び出すのですが、これは様々な`Stdout`デバイスをサポートいているためかなり煩雑です。ここではただVGAバッファに出力したいだけなので、そのような煩雑な実装は必要ありません。
[`_print` function]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698
[`$crate` variable]: https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate
[`format_args` macro]: https://doc.rust-lang.org/nightly/std/macro.format_args.html
[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html
VGAバッファに出力するには、`println!`マクロと`print!`マクロをコピーし、独自の`_print`関数を使うように修正してやればいいです:
```rust
// in src/vga_buffer.rs
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
```
元の`println`の定義と異なり、`print!`マクロの呼び出しにも`$crate`をつけるようにしています。これにより、`println`だけを使いたいと思ったら`print!`マクロもインポートしなくていいようになります。
標準ライブラリのように、`#[macro_export]`属性を両方のマクロに与え、クレートのどこでも使えるようにします。このようにすると、マクロはクレートの名前空間のルートに置かれるので、`use crate::vga_buffer::println`としてインポートするとうまく行かないことに注意してください。代わりに、 `use crate::println`としなければいけません。
`_print`関数は静的な`WRITER`をロックし、その`write_fmt`メソッドを呼び出します。このメソッドは`Write`トレイトのものなので、このトレイトもインポートしないといけません。最後に追加した`unwrap()`は、画面出力がうまく行かなかったときパニックします。しかし、`write_str`は常に`Ok`を返すようにしているので、これは起きないはずです。
マクロは`_print`をモジュールの外側から呼び出せる必要があるので、この関数は公開されていなければなりません。しかし、これは非公開の実装の詳細であると考え、[`doc(hidden)`属性][`doc(hidden)` attribute]をつけることで、生成されたドキュメントから隠すようにします。
[`doc(hidden)` attribute]: https://doc.rust-lang.org/nightly/rustdoc/write-documentation/the-doc-attribute.html#hidden
### `println`を使ってHello World
こうすることで、`_start`関数で`println`を使えるようになります:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() {
println!("Hello World{}", "!");
loop {}
}
```
マクロはすでに名前空間のルートにいるので、main関数内でマクロをインポートしなくても良いということに注意してください。
期待通り、画面に Hello World! と出ています:

### パニックメッセージを出力する
`println`マクロを手に入れたので、これを私達のパニック関数で使って、パニックメッセージとパニックの場所を出力させることができます:
```rust
// in main.rs
/// この関数はパニック時に呼ばれる。
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
```
`panic!("Some panic message");`という文を`_start`関数に書くと、次の出力を得ます:

つまり、パニックが起こったということだけでなく、パニックメッセージとそれがコードのどこで起こったかまで知ることができます。
## まとめ
この記事では、VGAテキストバッファの構造と、どのようにすれば`0xb8000`番地におけるメモリマッピングを通じてそれに書き込みを行えるかを学びました。このメモリマップされたバッファへの書き込みというunsafeな操作をカプセル化し、安全で便利なインターフェースを外部に提供するRustモジュールを作りました。
また、cargoのおかげでサードパーティのライブラリへの依存関係を簡単に追加できることも分かりました。`lazy_static`と`spin`という2つの依存先は、OS開発においてとても便利であり、今後の記事においても使っていきます。
## 次は?
次の記事ではRustに組み込まれている単体テストフレームワークをセットアップする方法を説明します。その後、この記事のVGAバッファモジュールに対する基本的な単体テストを作ります。
================================================
FILE: blog/content/edition-2/posts/03-vga-text-buffer/index.ko.md
================================================
+++
title = "VGA 텍스트 모드"
weight = 3
path = "ko/vga-text-mode"
date = 2018-02-26
[extra]
# Please update this when updating the translation
translation_based_on_commit = "1c9b5edd6a5a667e282ca56d6103d3ff1fd7cfcb"
# GitHub usernames of the people that translated this post
translators = ["JOE1994", "Quqqu"]
+++
[VGA 텍스트 모드][VGA text mode]를 통해 쉽게 화면에 텍스트를 출력할 수 있습니다. 이 글에서는 안전하지 않은 작업들을 분리된 모듈에 격리해 쉽고 안전하게 VGA 텍스트 모드를 이용할 수 있는 인터페이스를 구현합니다. 또한 Rust의 [서식 정렬 매크로 (formatting macro)][formatting macros]에 대한 지원을 추가할 것입니다.
[VGA text mode]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
[formatting macros]: https://doc.rust-lang.org/std/fmt/#related-macros
이 블로그는 [GitHub 저장소][GitHub]에서 오픈 소스로 개발되고 있으니, 문제나 문의사항이 있다면 저장소의 'Issue' 기능을 이용해 제보해주세요. [페이지 맨 아래][at the bottom]에 댓글을 남기실 수도 있습니다. 이 포스트와 관련된 모든 소스 코드는 저장소의 [`post-03 브랜치`][post branch]에서 확인하실 수 있습니다.
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
## VGA 텍스트 버퍼
VGA 텍스트 모드에서 화면에 문자를 출력하려면 VGA 하드웨어의 텍스트 버퍼에 해당 문자를 저장해야 합니다. VGA 텍스트 버퍼는 보통 25행 80열 크기의 2차원 배열이며, 해당 버퍼에 저장된 값들은 즉시 화면에 렌더링 됩니다. 배열의 각 원소는 화면에 출력될 문자를 아래의 형식으로 표현합니다:
| 비트 | 값 |
| ----- | ----------- |
| 0-7 | ASCII 코드 |
| 8-11 | 전경색 |
| 12-14 | 배경색 |
| 15 | 깜빡임 여부 |
첫 바이트는 [ASCII 인코딩][ASCII encoding]으로 출력될 문자를 나타냅니다. 엄밀히 따지자면 ASCII 인코딩이 아닌, 해당 인코딩에 문자들을 추가하고 살짝 변형한 [_code page 437_] 이라는 인코딩을 이용합니다. 설명을 간소화하기 위해 이하 본문에서는 그냥 ASCII 문자로 지칭하겠습니다.
[ASCII encoding]: https://en.wikipedia.org/wiki/ASCII
[_code page 437_]: https://en.wikipedia.org/wiki/Code_page_437
두 번째 바이트는 표현하는 문자가 어떻게 표시될 것인지를 정의합니다. 두 번째 바이트의 첫 4비트는 전경색을 나타내고, 그 다음 3비트는 배경색을 나타내며, 마지막 비트는 해당 문자가 화면에서 깜빡이도록 할지 결정합니다. 아래의 색상들을 이용할 수 있습니다:
| 숫자 값 | 색상 | 색상 + 밝기 조정 비트 | 밝기 조정 후 최종 색상 |
| ------- | ---------- | --------------------- | ---------------------- |
| 0x0 | Black | 0x8 | Dark Gray |
| 0x1 | Blue | 0x9 | Light Blue |
| 0x2 | Green | 0xa | Light Green |
| 0x3 | Cyan | 0xb | Light Cyan |
| 0x4 | Red | 0xc | Light Red |
| 0x5 | Magenta | 0xd | Pink |
| 0x6 | Brown | 0xe | Yellow |
| 0x7 | Light Gray | 0xf | White |
두 번째 바이트의 네 번째 비트 (_밝기 조정 비트_)를 통해 파란색을 하늘색으로 조정하는 등 색의 밝기를 변경할 수 있습니다. 배경색을 지정하는 3비트 이후의 마지막 비트는 깜빡임 여부를 지정합니다.
[메모리 맵 입출력 (memory-mapped I/O)][memory-mapped I/O]으로 메모리 주소 `0xb8000`을 통해 VGA 텍스트 버퍼에 접근할 수 있습니다. 해당 주소에 읽기/쓰기 작업을 하면 RAM 대신 VGA 텍스트 버퍼에 직접 읽기/쓰기가 적용됩니다.
[memory-mapped I/O]: https://en.wikipedia.org/wiki/Memory-mapped_I/O
메모리 맵 입출력 적용 대상 하드웨어가 일부 RAM 작업을 지원하지 않을 가능성을 염두해야 합니다. 예를 들어, 바이트 단위 읽기만 지원하는 장치로부터 메모리 맵 입출력을 통해 `u64`를 읽어들일 경우 쓰레기 값이 반환될 수도 있습니다. 다행히 텍스트 버퍼는 [일반적인 읽기/쓰기 작업들을 모두 지원하기에][supports normal reads and writes] 읽기/쓰기를 위한 특수 처리가 필요하지 않습니다.
[supports normal reads and writes]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip
## Rust 모듈
이제 VGA 버퍼가 어떻게 작동하는지 알았으니, 버퍼를 이용해 출력하는 것을 담당할 Rust 모듈을 만들어봅시다:
```rust
// in src/main.rs
mod vga_buffer;
```
새로운 모듈 `vga_buffer`를 위해 파일 `src/vga_buffer.rs`을 만듭니다. 이후 나타나는 모든 코드는 이 모듈에 들어갈 내용입니다 (별도의 지시 사항이 붙는 경우 제외).
### 색상
우선 enum을 이용하여 사용 가능한 여러 색상들을 표현합니다:
```rust
// in src/vga_buffer.rs
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
```
각 색상마다 고유 숫자 값을 배정할 수 있도록 우리는 [C언어와 같은 enum][C-like enum]을 사용합니다. `repr(u8)` 속성 때문에 enum의 각 분류 값은 `u8` 타입으로 저장됩니다. 사실 저장 공간은 4 비트만으로도 충분하지만, Rust에는 `u4` 타입이 없습니다.
[C-like enum]: https://doc.rust-lang.org/rust-by-example/custom_types/enum/c_like.html
사용되지 않는 enum 분류 값이 있을 때마다 컴파일러는 불필요한 코드가 있다는 경고 메시지를 출력합니다. 하지만 위처럼 `#[allow(dead_code)]` 속성을 적용하면 `Color` enum에 대해서는 컴파일러가 해당 경고 메시지를 출력하지 않습니다.
`Color` 타입에 [`Copy`], [`Clone`], [`Debug`], [`PartialEq`] 그리고 [`Eq`] 트레이트들을 [구현 (derive)][deriving] 함으로써 `Color` 타입이 [copy semantics] 를 따르도록 하고 또한 `Color` 타입 변수를 출력하거나 두 `Color` 타입 변수를 서로 비교할 수 있도록 합니다.
[deriving]: https://doc.rust-lang.org/rust-by-example/trait/derive.html
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
[`PartialEq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html
[`Eq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html
[copy semantics]: https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types
전경색과 배경색을 모두 표현할 수 있는 색상 코드를 표현하기 위해 `u8` 타입을 감싸는 [newtype]을 선언합니다:
[newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
```rust
// in src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
```
`ColorCode` 구조체는 전경색 및 배경색을 모두 표현하는 색상 바이트 전체의 정보를 지닙니다. 이전처럼 `Copy` 및 `Debug` 트레이트를 구현 (derive) 해줍니다. `ColorCode` 구조체가 메모리 상에서 `u8` 타입과 같은 저장 형태를 가지도록 [`repr(transparent)`] 속성을 적용합니다.
[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent
### 텍스트 버퍼
스크린 상의 문자 및 텍스트 버퍼를 표현하는 구조체들을 아래와 같이 추가합니다:
```rust
// in src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
Rust에서는 구조체 정의 코드에서의 필드 정렬 순서와 메모리 상에서 구조체의 각 필드가 저장되는 순서가 동일하지 않을 수 있습니다. 구조체의 각 필드 정렬 순서가 컴파일 중에 바뀌지 않도록 하려면 [`repr(C)`] 속성이 필요합니다. 이 속성을 사용하면 C언어의 구조체처럼 컴파일러가 구조체 내 각 필드의 정렬 순서를 임의로 조정할 수 없게 되기에, 우리는 메모리 상에서 구조체의 각 필드가 어떤 순서로 저장되는지 확신할 수 있습니다. 또한 `Buffer` 구조체에 [`repr(transparent)`] 속성을 적용하여 메모리 상에서 해당 구조체가 저장되는 형태가 `chars` 필드의 저장 형태와 동일하도록 해줍니다.
[`repr(C)`]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc
이제 아래와 같은 Writer 타입을 만들어 실제로 화면에 출력하는 데에 이용할 것입니다:
```rust
// in src/vga_buffer.rs
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
```
Writer는 언제나 가장 마지막 행에 값을 작성할 것이며, 작성 중인 행이 꽉 차거나 개행문자를 입력받은 경우에는 작성 중이던 행을 마치고 새로운 행으로 넘어갈 것입니다. 전경색 및 배경색은 `color_code`를 통해 표현되고 `buffer`에 VGA 버퍼에 대한 레퍼런스를 저장합니다. `buffer`에 대한 레퍼런스가 유효한 기간을 컴파일러에게 알리기 위해서 [명시적인 lifetime][explicit lifetime]이 필요합니다. [`'static`] lifetime 표기는 VGA 버퍼에 대한 레퍼런스가 프로그램 실행 시간 내내 유효하다는 것을 명시합니다.
[explicit lifetime]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax
[`'static`]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
### 출력하기
이제 `Writer`를 이용하여 VGA 버퍼에 저장된 문자들을 변경할 수 있게 되었습니다. 우선 아래와 같이 하나의 ASCII 바이트를 출력하는 함수를 만듭니다:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}
fn new_line(&mut self) {/* TODO */}
}
```
주어진 바이트 값이 [개행 문자][newline] `\n`일 경우, Writer는 아무것도 출력하지 않고 대신 `new_line` 함수 (아래에서 함께 구현할 예정)를 호출합니다. 다른 바이트 값들은 match문의 두 번째 패턴에 매치되어 화면에 출력됩니다.
[newline]: https://en.wikipedia.org/wiki/Newline
바이트를 출력할 때, Writer는 현재 행이 가득 찼는지 확인합니다. 현재 행이 가득 찬 경우, 개행을 위해 `new_line` 함수를 먼저 호출해야 합니다. 그 후 버퍼에서의 현재 위치에 새로운 `ScreenChar`를 저장합니다. 마지막으로 현재 열 위치 값을 한 칸 올립니다.
위에서 구현한 함수로 문자열의 각 문자를 하나씩 출력함으로써 문자열 전체를 출력할 수도 있습니다:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// 출력 가능한 ASCII 바이트 혹은 개행 문자
0x20..=0x7e | b'\n' => self.write_byte(byte),
// ASCII 코드 범위 밖의 값
_ => self.write_byte(0xfe),
}
}
}
}
```
VGA 텍스트 버퍼는 ASCII 문자 및 [코드 페이지 437][code page 437] 인코딩의 문자들만 지원합니다. Rust의 문자열은 기본 인코딩이 [UTF-8]이기에 VGA 텍스트 버퍼가 지원하지 않는 바이트들을 포함할 수 있습니다. 그렇기에 위 함수에서 `match`문을 통해 VGA 버퍼를 통해 출력 가능한 문자 (개행 문자 및 스페이스 문자와 `~` 문자 사이의 모든 문자)와 그렇지 않은 문자를 구분하여 처리합니다. 출력 불가능한 문자의 경우, VGA 하드웨어에서 16진수 코드 `0xfe`를 가지는 문자 (`■`)을 출력합니다.
[code page 437]: https://en.wikipedia.org/wiki/Code_page_437
[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm
#### 테스트 해봅시다!
간단한 함수를 하나 만들어 화면에 문자들을 출력해봅시다:
```rust
// in src/vga_buffer.rs
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}
```
우선 메모리 주소 `0xb8000`을 가리키는 새로운 Writer 인스턴스를 생성합니다. 이를 구현한 코드가 다소 난해하게 느껴질 수 있으니 단계별로 나누어 설명드리겠습니다: 먼저 정수 `0xb8000`을 읽기/쓰기 모두 가능한 (mutable) [포인터][raw pointer]로 타입 변환합니다. 그 후 `*` 연산자를 통해 이 포인터를 역참조 (dereference) 하고 `&mut`를 통해 즉시 borrow 함으로써 해당 주소에 저장된 값을 변경할 수 있는 레퍼런스 (mutable reference)를 만듭니다. 여기서 Rust 컴파일러는 포인터의 유효성 및 안전성을 보증할 수 없기에, [`unsafe` 블록][`unsafe` block]을 사용해야만 포인터를 레퍼런스로 변환할 수 있습니다.
[raw pointer]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`unsafe` block]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
그 다음 Writer 인스턴스에 바이트 `b'H'`를 적습니다. 접두사 `b`는 ASCII 문자를 나타내는 [바이트 상수 (literal)][byte literal] 를 생성합니다. 문자열 `"ello "`와 `"Wörld!"`를 적음으로써 `write_string` 함수 및 출력 불가능한 문자에 대한 특수 처리가 잘 구현되었는지 테스트 해봅니다. 화면에 메시지가 출력되는지 확인하기 위해 `print_something` 함수를 `_start` 함수에서 호출합니다:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
vga_buffer::print_something();
loop {}
}
```
프로젝트를 실행하면 `Hello W■■rld!` 라는 메시지가 화면 왼쪽 _아래_ 구석에 노란 텍스트로 출력됩니다:
[byte literal]: https://doc.rust-lang.org/reference/tokens.html#byte-literals

문자 `ö` 대신 두 개의 `■` 문자가 출력되었습니다. 문자 `ö`는 [UTF-8] 인코딩에서 두 바이트로 표현되는데, 각각의 바이트가 출력 가능한 ASCII 문자 범위에 있지 않기 때문입니다. 이는 사실 UTF-8 인코딩의 핵심 특징으로, 두 바이트 이상으로 표현되는 문자들의 각 바이트는 유효한 ASCII 값을 가질 수 없습니다.
### Volatile
위에서 화면에 메시지가 출력되는 것을 확인했습니다. 하지만 미래의 Rust 컴파일러가 더 공격적으로 프로그램 최적화를 하게 된다면 메시지가 출력되지 않을 수 있습니다.
여기서 주목해야 할 것은 우리가 `Buffer`에 데이터를 쓰기만 할 뿐 읽지는 않는다는 점입니다. 컴파일러는 우리가 일반 RAM 메모리가 아닌 VGA 버퍼 메모리에 접근한다는 사실을 알지 못하며, 해당 버퍼에 쓰인 값이 화면에 출력되는 현상 (외부에서 관찰 가능한 상태 변화)에 대해서도 이해하지 못합니다. 그렇기에 컴파일러가 VGA 버퍼에 대한 쓰기 작업이 불필요하다고 판단하여 프로그램 최적화 중에 해당 작업들을 삭제할 수도 있습니다. 이를 방지하려면 VGA 버퍼에 대한 쓰기 작업이 _[volatile]_ 하다고 명시함으로써 해당 쓰기 작업이 관찰 가능한 상태 변화 (side effect)를 일으킨다는 것을 컴파일러에게 알려야 합니다.
[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
VGA 버퍼에 volatile한 방식으로 데이터를 쓰기 위해 우리는 [volatile][volatile crate] 크레이트를 사용합니다. 이 _크레이트_ (패키지 형태의 Rust 라이브러리) 는 `Volatile` 이라는 포장 타입 (wrapper type)과 함께 `read` 및 `write` 함수들을 제공합니다. 이 함수들은 내부적으로 Rust 코어 라이브러리의 [read_volatile] 및 [write_volatile] 함수들을 사용함으로써 읽기/쓰기 작업이 프로그램 최적화 중에 제거되지 않게 합니다.
[volatile crate]: https://docs.rs/volatile
[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html
[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html
`Cargo.toml`의 `dependencies` 섹션에 `volatile` 크레이트를 추가합니다:
```toml
# in Cargo.toml
[dependencies]
volatile = "0.2.6"
```
꼭 `volatile` 크레이트의 `0.2.6` 버전을 사용하셔야 합니다. 그 이후 버전의 `volatile` 크레이트는 이 포스트의 코드와 호환되지 않습니다. `0.2.6`은 [semantic] 버전 넘버를 나타내는데, 자세한 내용은 cargo 문서의 [Specifying Dependencies] 챕터를 확인해주세요.
[semantic]: https://semver.org/
[Specifying Dependencies]: https://doc.crates.io/specifying-dependencies.html
이제 이 크레이트를 써서 VGA 버퍼에 대한 쓰기 작업이 volatile 하도록 만들 것입니다. `Buffer` 타입을 정의하는 코드를 아래처럼 수정해주세요:
```rust
// in src/vga_buffer.rs
use volatile::Volatile;
struct Buffer {
chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
`ScreenChar` 대신 `Volatile`를 사용합니다. (`Volatile` 타입은 [제네릭 (generic)][generic] 타입이며 거의 모든 타입을 감쌀 수 있습니다). 이로써 해당 타입에 대해 실수로 “일반” 쓰기 작업을 하는 실수를 방지할 수 있게 되었습니다. 이제 쓰기 작업 구현 시 `write` 함수만을 이용해야 합니다.
[generic]: https://doc.rust-lang.org/book/ch10-01-syntax.html
`Writer::write_byte` 함수가 `write`함수를 사용하도록 아래처럼 변경합니다:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
...
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code,
});
...
}
}
}
...
}
```
일반 대입 연산자 `=` 대신에 `write` 함수를 사용하였기에, 컴파일러는 최적화 단계에 절대로 해당 쓰기 작업을 삭제하지 않을 것입니다.
### 서식 정렬 매크로
`Writer` 타입이 Rust의 서식 정렬 매크로 (formatting macro) 를 지원한다면 정수나 부동 소수점 값 등 다양한 타입의 값들을 편리하고 쉽게 출력할 수 있을 것입니다. `Writer`가 Rust의 서식 정렬 매크로를 지원하려면 [`core::fmt::Write`] 트레이트를 구현해야 합니다. 해당 트레이트를 구현하기 위해서는 `write_str` 함수만 구현하면 되는데, 이 함수는 우리가 위에서 구현한 `write_string` 함수와 거의 유사하나 반환 타입이 `fmt::Result` 타입인 함수입니다:
[`core::fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
```rust
// in src/vga_buffer.rs
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
```
반환 값 `Ok(())` 는 `()` 타입을 감싸는 `Result` 타입의 `Ok` 입니다.
이제 Rust에서 기본적으로 제공되는 서식 정렬 매크로 `write!`/`writeln!`을 사용할 수 있습니다:
```rust
// in src/vga_buffer.rs
pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}
```
화면 맨 아래에 메시지 `Hello! The numbers are 42 and 0.3333333333333333`가 출력될 것입니다. `write!` 매크로는 `Result`를 반환하는데, `Result`가 사용되지 않았다는 오류가 출력되지 않도록 [`unwrap`] 함수를 호출합니다. 반환된 `Result`가 `Err()`일 경우 프로그램이 패닉 (panic) 하겠지만, 우리가 작성한 코드는 VGA 버퍼에 대한 쓰기 후 언제나 `Ok()`를 반환하기에 패닉이 발생하지 않습니다.
[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap
### 개행
현재 행이 꽉 찬 상태에서 입력받은 문자 및 개행 문자에 대해 우리는 아직 아무런 대응을 하지 않습니다. 이러한 경우 현재 행의 모든 문자들을 한 행씩 위로 올려 출력하고 (맨 위 행은 지우고) 비워진 현재 행의 맨 앞 칸에서부터 다시 시작해야 합니다. 아래의 `new_line` 함수를 통해 해당 작업을 구현합니다:
```rust
// in src/vga_buffer.rs
impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {/* TODO */}
}
```
화면에 출력된 각 문자들을 순회하며 전부 한 행씩 위로 올려 출력합니다. 범위를 나타내는 `..` 표기는 범위의 상한 값을 포함하지 않는다는 것을 주의해 주세요. 0번째 행은 화면 밖으로 사라질 행이기에 순회하지 않습니다.
아래의 `clear_row` 함수를 추가하여 개행 문자 처리 코드를 완성합니다:
```rust
// in src/vga_buffer.rs
impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
}
```
이 함수는 한 행의 모든 문자를 스페이스 문자로 덮어쓰는 방식으로 한 행의 내용을 전부 지웁니다.
## 전역 접근 가능한 인터페이스
`Writer` 인스턴스를 이리저리 옮겨다닐 필요가 없도록 전역 접근 가능한 `Writer`를 제공하기 위해 정적 변수 `WRITER`를 만들어 봅시다:
```rust
// in src/vga_buffer.rs
pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
```
컴파일 시 아래의 오류 메시지가 출력될 것입니다:
```
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/vga_buffer.rs:7:17
|
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0396]: raw pointers cannot be dereferenced in statics
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:13
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
```
여기서 오류가 왜 발생했는지 이해하려면 우선 알아야 할 것이 있습니다. 그것은 바로 일반 자동 변수들이 프로그램 실행 시간에 초기화 되는 반면에 정적 (static) 변수들은 컴파일 시간에 초기화된다는 점입니다. Rust 컴파일러의 "[const evaluator]" 컴포넌트가 정적 변수를 컴파일 시간에 초기화합니다. 아직 구현된 기능이 많지는 않지만, 해당 컴포넌트의 기능을 확장하는 작업이 진행 중입니다 (예시: “[Allow panicking in constants]” RFC).
[const evaluator]: https://rustc-dev-guide.rust-lang.org/const-eval.html
[Allow panicking in constants]: https://github.com/rust-lang/rfcs/pull/2345
`ColorCode::new`에 대한 오류는 [`const` 함수][`const` functions]를 이용해 쉽게 해결할 수 있습니다. 더 큰 문제는 바로 Rust의 const evaluator가 컴파일 시간에 raw pointer를 레퍼런스로 전환하지 못한다는 것입니다. 미래에는 이것이 가능해질 수도 있겠지만, 현재로서는 다른 해법을 찾아야 합니다.
[`const` functions]: https://doc.rust-lang.org/reference/const_eval.html#const-functions
### 정적 변수의 초기화 지연
Rust 개발을 하다 보면 const가 아닌 함수를 이용해 1회에 한해 정적 변수의 값을 설정해야 하는 상황이 자주 발생합니다. [lazy_static] 크레이트의 `lazy_static!` 매크로를 이용하면, 정적 변수의 값을 컴파일 시간에 결정하지 않고 초기화 시점을 해당 프로그램 실행 중 변수에 대한 접근이 처음 일어나는 시점까지 미룰 수 있습니다. 즉, 정적 변수 초기화가 프로그램 실행 시간에 진행되기에 초기 값을 계산할 때 const가 아닌 복잡한 함수들을 사용할 수 있습니다.
[lazy_static]: https://docs.rs/lazy_static/1.0.1/lazy_static/
프로젝트 의존 라이브러리로서 `lazy_static` 크레이트를 추가해줍니다:
```toml
# in Cargo.toml
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
```
우리는 러스트 표준 라이브러리를 링크하지 않기에 `spin_no_std` 기능이 필요합니다.
`lazy_static` 크레이트 덕분에 이제 오류 없이 `WRITER`를 정의할 수 있습니다:
```rust
// in src/vga_buffer.rs
use lazy_static::lazy_static;
lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}
```
현재 `WRITER`는 immutable (읽기 가능, 쓰기 불가능) 하여 실질적인 쓸모가 없습니다. 모든 쓰기 함수들은 첫 인자로 `&mut self`를 받기 때문에 `WRITER`로 어떤 쓰기 작업도 할 수가 없습니다. 이에 대한 해결책으로 [mutable static]은 어떨까요? 이 선택지를 고른다면 모든 읽기 및 쓰기 작업이 데이터 레이스 (data race) 및 기타 위험에 노출되기에 안전을 보장할 수 없게 됩니다. Rust에서 `static mut`는 웬만하면 사용하지 않도록 권장되며, 심지어 [Rust 언어에서 완전히 `static mut`를 제거하자는 제안][remove static mut]이 나오기도 했습니다. 이것 이외에도 대안이 있을까요? [내부 가변성 (interior mutability)][interior mutability]을 제공하는 [RefCell] 혹은 [UnsafeCell] 을 통해 immutable한 정적 변수를 만드는 것은 어떨까요? 이 타입들은 중요한 이유로 [Sync] 트레이트를 구현하지 않기에 정적 변수를 선언할 때에는 사용할 수 없습니다.
[mutable static]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437
[RefCell]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt
[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html
[interior mutability]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html
### 스핀 락 (Spinlocks)
표준 라이브러리의 [Mutex]는 동기화된 내부 가변성 (interior mutability)을 제공합니다. Mutex는 접근하려는 리소스가 잠겼을 때 현재 스레드를 블로킹 (blocking) 하는 것으로 상호 배제 (mutual exclusion)를 구현합니다. 우리의 커널은 스레드 블로킹은 커녕 스레드의 개념조차 구현하지 않기에 [Mutex]를 사용할 수 없습니다. 그 대신 우리에게는 운영체제 기능이 필요 없는 원시적인 [스핀 락 (spinlock)][spinlock]이 있습니다. 스핀 락은 Mutex와 달리 스레드를 블로킹하지 않고, 리소스의 잠김이 풀릴 때까지 반복문에서 계속 리소스 취득을 시도하면서 CPU 시간을 소모합니다.
[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html
[spinlock]: https://en.wikipedia.org/wiki/Spinlock
스핀 락을 사용하기 위해 [spin 크레이트][spin crate] 를 의존 크레이트 목록에 추가합니다:
[spin crate]: https://crates.io/crates/spin
```toml
# in Cargo.toml
[dependencies]
spin = "0.5.2"
```
이제 스핀 락을 이용해 전역 변수 `WRITER`에 안전하게 [내부 가변성 (interior mutability)][interior mutability] 을 구현할 수 있습니다:
```rust
// in src/vga_buffer.rs
use spin::Mutex;
...
lazy_static! {
pub static ref WRITER: Mutex = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
```
`print_something` 함수를 삭제하고 `_start` 함수에서 직접 메시지를 출력할 수 있습니다:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
use core::fmt::Write;
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
loop {}
}
```
`fmt::Write` 트레이트를 가져와야 이 트레이트가 제공하는 함수들을 사용할 수 있습니다.
### 메모리 안전성
우리가 작성한 코드에는 unsafe 블록이 단 하나 존재합니다. 이 unsafe 블록은 주소 `0xb8000`을 가리키는 레퍼런스 `Buffer`를 초기화 하는 로직을 담기 위해 필요합니다. `Buffer`에 대한 초기화 이외 모든 작업들은 안전합니다 (메모리 안전성 측면에서). Rust는 배열의 원소에 접근하는 코드에는 인덱스 값과 배열의 길이를 비교하는 로직을 자동으로 삽입하기에, 버퍼의 정해진 공간 밖에 실수로 데이터를 쓰는 것은 불가능합니다. 타입 시스템에서 요구하는 조건들을 코드에 알맞게 구현함으로써 외부 사용자에게 안전한 인터페이스를 제공할 수 있게 되었습니다.
### println 매크로
전역 변수 `Writer`도 갖추었으니 이제 프로젝트 내 어디서든 사용할 수 있는 `println` 매크로를 추가할 수 있습니다. Rust의 [매크로 문법][macro syntax]은 다소 난해하기에, 우리에게 필요한 매크로를 밑바닥부터 작성하지는 않을 것입니다. 그 대신 표준 라이브러리의 [`println!` 매크로][`println!` macro] 구현 코드를 참조할 것입니다:
[macro syntax]: https://doc.rust-lang.org/nightly/book/ch20-05-macros.html#declarative-macros-for-general-metaprogramming
[`println!` macro]: https://doc.rust-lang.org/nightly/std/macro.println!.html
```rust
#[macro_export]
macro_rules! println {
() => (print!("\n"));
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}
```
매크로는 `match`문의 여러 패턴들을 선언하듯 한 개 이상의 규칙을 통해 정의됩니다. `println` 매크로는 두 개의 규칙을 가집니다: 첫 번째 규칙은 매크로에 아무 인자도 전달되지 않았을 때 (예: `println!()`)에 적용되어 개행 문자를 출력하는 `print!("\n")` 코드를 생성합니다. 두 번째 규칙은 매크로에 여러 인자들이 주어졌을 때 적용됩니다 (예: `println!("Hello")` 혹은 `println!("Number: {}", 4)`). 두 번째 규칙은 주어진 인자들을 그대로 `print!` 매크로에 전달하고 인자 문자열 끝에 개행 문자를 추가한 코드를 생성합니다.
`#[macro_export]` 속성이 적용된 매크로는 외부 크레이트 및 현재 크레이트 내 어디서든 사용 가능해집니다 (기본적으로는 매크로가 정의된 모듈 내에서만 그 매크로를 쓸 수 있습니다). 또한 이 속성이 적용된 매크로는 크레이트의 최고 상위 네임스페이스에 배치되기에, 매크로를 쓰기 위해 가져올 때 `use std::println` 대신 `use std::macros::println`을 적어야 합니다.
[`print!` 매크로][`print!` macro]는 아래와 같이 정의되어 있습니다:
[`print!` macro]: https://doc.rust-lang.org/nightly/std/macro.print!.html
```rust
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
```
이 매크로는 `io` 모듈의 [`print` 함수][`_print` function]를 호출하는 코드로 변환됩니다. [변수 `$crate`][`$crate` variable]가 `std`로 변환되기에 다른 크레이트에서도 이 매크로를 사용할 수 있습니다.
[`format_args` 매크로][`format_args` macro]는 주어진 인자들로부터 [fmt::Arguments] 타입 오브젝트를 만들고, 이 오브젝트가 `_print` 함수에 전달됩니다. 표준 라이브러리의 [`_print` 함수][`_print` function]는 `print_to` 함수를 호출합니다. `print_to` 함수는 다양한 `Stdout` (표준 출력) 장치들을 모두 지원하기에 구현이 제법 복잡합니다. 우리는 VGA 버퍼에 출력하는 것만을 목표로 하기에 굳이 `print_to` 함수의 복잡한 구현을 가져올 필요가 없습니다.
[`_print` function]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698
[`$crate` variable]: https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate
[`format_args` macro]: https://doc.rust-lang.org/nightly/std/macro.format_args.html
[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html
VGA 버퍼에 메시지를 출력하기 위해 `println!` 및 `print!` 매크로 구현 코드를 복사해 온 뒤 우리가 직접 정의한 `_print` 함수를 사용하도록 변경해줍니다:
```rust
// in src/vga_buffer.rs
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
```
기존 `println` 구현에서 `print!` 매크로를 호출하는 코드에 우리는 `$crate` 접두어를 추가했습니다.
이로써 `println` 매크로만 사용하고 싶은 경우에 `print` 매크로를 별도로 import 하지 않아도 됩니다.
표준 라이브러리의 구현과 마찬가지로, 두 매크로에 `#[macro_export]` 속성을 추가하여 크레이트 어디에서나 사용할 수 있도록 합니다. 이 속성이 추가된 두 매크로는 크레이트의 최고 상위 네임스페이스에 배정되기에, `use crate::vga_buffer::println` 대신 `use crate::println`을 사용하여 import 합니다.
`_print` 함수는 정적 변수 `WRITER`를 잠그고 `write_fmt` 함수를 호출합니다. 이 함수는 `Write` 트레이트를 통해 제공되기에, 이 트레이트를 import 해야 합니다. `write_fmt` 함수 호출 이후의 `unwrap()`으로 인해 출력이 실패할 경우 패닉이 발생합니다. 하지만 `write_str` 함수가 언제나 `Ok`를 반환하기에 패닉이 일어날 일은 없습니다.
우리의 매크로들이 모듈 밖에서 `_print` 함수를 호출할 수 있으려면 이 함수를 public 함수로 설정해야 합니다. public 함수이지만 구체적인 구현 방식은 드러나지 않도록 [`doc(hidden)` 속성][`doc(hidden)` attribute]을 추가하여 이 함수가 프로젝트 문서에 노출되지 않게 합니다.
[`doc(hidden)` attribute]: https://doc.rust-lang.org/nightly/rustdoc/write-documentation/the-doc-attribute.html#hidden
### `println`을 이용해 "Hello World" 출력하기
이제 `_start` 함수에서 `println`을 사용할 수 있습니다:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() {
println!("Hello World{}", "!");
loop {}
}
```
`println!` 매크로가 이미 루트 네임스페이스에 배정되었기에, main 함수에서 사용하기 위해 다시 매크로를 import 할 필요가 없습니다.
예상한 대로, 화면에 _“Hello World!”_ 가 출력된 것을 확인할 수 있습니다:

### 패닉 메시지 출력하기
`println` 매크로를 이용하여 `panic` 함수에서도 패닉 메시지 및 패닉이 발생한 코드 위치를 출력할 수 있게 되었습니다:
```rust
// in main.rs
/// This function is called on panic.
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
```
`_start` 함수에 `panic!("Some panic message")` 을 추가한 후 빌드 및 실행하면 아래와 같은 출력 내용을 확인할 수 있을 것입니다:

출력 내용을 통해 패닉 발생 여부, 패닉 메시지 그리고 패닉이 일어난 코드 위치까지도 알 수 있습니다.
## 정리
이 포스트에서는 VGA 텍스트 버퍼의 구조 및 메모리 주소 `0xb8000`로의 메모리 매핑을 통해 어떻게 VGA 텍스트 버퍼에 쓰기 작업을 할 수 있는지에 대해 다뤘습니다. 또한 메모리 매핑 된 버퍼에 대한 쓰기 기능 (안전하지 않은 작업)을 안전하고 편리한 인터페이스로 제공하는 Rust 모듈을 작성했습니다.
또한 cargo를 이용하여 의존 크레이트를 추가하는 것이 얼마나 쉬운지 직접 확인해볼 수 있었습니다.
이번 포스트에서 추가한 의존 크레이트 `lazy_static`과 `spin`은 운영체제 개발에 매우 유용하기에 이후 포스트에서도 자주 사용할 것입니다.
## 다음 단계는 무엇일까요?
다음 포스트에서는 Rust의 자체 유닛 테스트 프레임워크를 설정하는 법에 대해 설명할 것입니다. 그리고 나서 이번 포스트에서 작성한 VGA 버퍼 모듈을 위한 기본적인 유닛 테스트들을 작성할 것입니다.
================================================
FILE: blog/content/edition-2/posts/03-vga-text-buffer/index.md
================================================
+++
title = "VGA Text Mode"
weight = 3
path = "vga-text-mode"
date = 2018-02-26
[extra]
chapter = "Bare Bones"
+++
The [VGA text mode] is a simple way to print text to the screen. In this post, we create an interface that makes its usage safe and simple by encapsulating all unsafety in a separate module. We also implement support for Rust's [formatting macros].
[VGA text mode]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
[formatting macros]: https://doc.rust-lang.org/std/fmt/#related-macros
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-03`][post branch] branch.
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
## The VGA Text Buffer
To print a character to the screen in VGA text mode, one has to write it to the text buffer of the VGA hardware. The VGA text buffer is a two-dimensional array with typically 25 rows and 80 columns, which is directly rendered to the screen. Each array entry describes a single screen character through the following format:
| Bit(s) | Value |
| ------ | ---------------- |
| 0-7 | ASCII code point |
| 8-11 | Foreground color |
| 12-14 | Background color |
| 15 | Blink |
The first byte represents the character that should be printed in the [ASCII encoding]. To be more specific, it isn't exactly ASCII, but a character set named [_code page 437_] with some additional characters and slight modifications. For simplicity, we will proceed to call it an ASCII character in this post.
[ASCII encoding]: https://en.wikipedia.org/wiki/ASCII
[_code page 437_]: https://en.wikipedia.org/wiki/Code_page_437
The second byte defines how the character is displayed. The first four bits define the foreground color, the next three bits the background color, and the last bit whether the character should blink. The following colors are available:
| Number | Color | Number + Bright Bit | Bright Color |
| ------ | ---------- | ------------------- | ------------ |
| 0x0 | Black | 0x8 | Dark Gray |
| 0x1 | Blue | 0x9 | Light Blue |
| 0x2 | Green | 0xa | Light Green |
| 0x3 | Cyan | 0xb | Light Cyan |
| 0x4 | Red | 0xc | Light Red |
| 0x5 | Magenta | 0xd | Pink |
| 0x6 | Brown | 0xe | Yellow |
| 0x7 | Light Gray | 0xf | White |
Bit 4 is the _bright bit_, which turns, for example, blue into light blue. For the background color, this bit is repurposed as the blink bit.
The VGA text buffer is accessible via [memory-mapped I/O] to the address `0xb8000`. This means that reads and writes to that address don't access the RAM but directly access the text buffer on the VGA hardware. This means we can read and write it through normal memory operations to that address.
[memory-mapped I/O]: https://en.wikipedia.org/wiki/Memory-mapped_I/O
Note that memory-mapped hardware might not support all normal RAM operations. For example, a device could only support byte-wise reads and return junk when a `u64` is read. Fortunately, the text buffer [supports normal reads and writes], so we don't have to treat it in a special way.
[supports normal reads and writes]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip
## A Rust Module
Now that we know how the VGA buffer works, we can create a Rust module to handle printing:
```rust
// in src/main.rs
mod vga_buffer;
```
For the content of this module, we create a new `src/vga_buffer.rs` file. All of the code below goes into our new module (unless specified otherwise).
### Colors
First, we represent the different colors using an enum:
```rust
// in src/vga_buffer.rs
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
```
We use a [C-like enum] here to explicitly specify the number for each color. Because of the `repr(u8)` attribute, each enum variant is stored as a `u8`. Actually 4 bits would be sufficient, but Rust doesn't have a `u4` type.
[C-like enum]: https://doc.rust-lang.org/rust-by-example/custom_types/enum/c_like.html
Normally the compiler would issue a warning for each unused variant. By using the `#[allow(dead_code)]` attribute, we disable these warnings for the `Color` enum.
By [deriving] the [`Copy`], [`Clone`], [`Debug`], [`PartialEq`], and [`Eq`] traits, we enable [copy semantics] for the type and make it printable and comparable.
[deriving]: https://doc.rust-lang.org/rust-by-example/trait/derive.html
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
[`PartialEq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html
[`Eq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html
[copy semantics]: https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types
To represent a full color code that specifies foreground and background color, we create a [newtype] on top of `u8`:
[newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
```rust
// in src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
```
The `ColorCode` struct contains the full color byte, containing foreground and background color. Like before, we derive the `Copy` and `Debug` traits for it. To ensure that the `ColorCode` has the exact same data layout as a `u8`, we use the [`repr(transparent)`] attribute.
[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent
### Text Buffer
Now we can add structures to represent a screen character and the text buffer:
```rust
// in src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
Since the field ordering in default structs is undefined in Rust, we need the [`repr(C)`] attribute. It guarantees that the struct's fields are laid out exactly like in a C struct and thus guarantees the correct field ordering. For the `Buffer` struct, we use [`repr(transparent)`] again to ensure that it has the same memory layout as its single field.
[`repr(C)`]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc
To actually write to screen, we now create a writer type:
```rust
// in src/vga_buffer.rs
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
```
The writer will always write to the last line and shift lines up when a line is full (or on `\n`). The `column_position` field keeps track of the current position in the last row. The current foreground and background colors are specified by `color_code` and a reference to the VGA buffer is stored in `buffer`. Note that we need an [explicit lifetime] here to tell the compiler how long the reference is valid. The [`'static`] lifetime specifies that the reference is valid for the whole program run time (which is true for the VGA text buffer).
[explicit lifetime]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax
[`'static`]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
### Printing
Now we can use the `Writer` to modify the buffer's characters. First we create a method to write a single ASCII byte:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}
fn new_line(&mut self) {/* TODO */}
}
```
If the byte is the [newline] byte `\n`, the writer does not print anything. Instead, it calls a `new_line` method, which we'll implement later. Other bytes get printed to the screen in the second `match` case.
[newline]: https://en.wikipedia.org/wiki/Newline
When printing a byte, the writer checks if the current line is full. In that case, a `new_line` call is used to wrap the line. Then it writes a new `ScreenChar` to the buffer at the current position. Finally, the current column position is advanced.
To print whole strings, we can convert them to bytes and print them one-by-one:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// printable ASCII byte or newline
0x20..=0x7e | b'\n' => self.write_byte(byte),
// not part of printable ASCII range
_ => self.write_byte(0xfe),
}
}
}
}
```
The VGA text buffer only supports ASCII and the additional bytes of [code page 437]. Rust strings are [UTF-8] by default, so they might contain bytes that are not supported by the VGA text buffer. We use a `match` to differentiate printable ASCII bytes (a newline or anything in between a space character and a `~` character) and unprintable bytes. For unprintable bytes, we print a `■` character, which has the hex code `0xfe` on the VGA hardware.
[code page 437]: https://en.wikipedia.org/wiki/Code_page_437
[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm
#### Try it out!
To write some characters to the screen, you can create a temporary function:
```rust
// in src/vga_buffer.rs
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}
```
It first creates a new Writer that points to the VGA buffer at `0xb8000`. The syntax for this might seem a bit strange: First, we cast the integer `0xb8000` as a mutable [raw pointer]. Then we convert it to a mutable reference by dereferencing it (through `*`) and immediately borrowing it again (through `&mut`). This conversion requires an [`unsafe` block], since the compiler can't guarantee that the raw pointer is valid.
[raw pointer]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[`unsafe` block]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
Then it writes the byte `b'H'` to it. The `b` prefix creates a [byte literal], which represents an ASCII character. By writing the strings `"ello "` and `"Wörld!"`, we test our `write_string` method and the handling of unprintable characters. To see the output, we need to call the `print_something` function from our `_start` function:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
vga_buffer::print_something();
loop {}
}
```
When we run our project now, a `Hello W■■rld!` should be printed in the _lower_ left corner of the screen in yellow:
[byte literal]: https://doc.rust-lang.org/reference/tokens.html#byte-literals

Notice that the `ö` is printed as two `■` characters. That's because `ö` is represented by two bytes in [UTF-8], which both don't fall into the printable ASCII range. In fact, this is a fundamental property of UTF-8: the individual bytes of multi-byte values are never valid ASCII.
### Volatile
We just saw that our message was printed correctly. However, it might not work with future Rust compilers that optimize more aggressively.
The problem is that we only write to the `Buffer` and never read from it again. The compiler doesn't know that we really access VGA buffer memory (instead of normal RAM) and knows nothing about the side effect that some characters appear on the screen. So it might decide that these writes are unnecessary and can be omitted. To avoid this erroneous optimization, we need to specify these writes as _[volatile]_. This tells the compiler that the write has side effects and should not be optimized away.
[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
In order to use volatile writes for the VGA buffer, we use the [volatile][volatile crate] library. This _crate_ (this is how packages are called in the Rust world) provides a `Volatile` wrapper type with `read` and `write` methods. These methods internally use the [read_volatile] and [write_volatile] functions of the core library and thus guarantee that the reads/writes are not optimized away.
[volatile crate]: https://docs.rs/volatile
[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html
[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html
We can add a dependency on the `volatile` crate by adding it to the `dependencies` section of our `Cargo.toml`:
```toml
# in Cargo.toml
[dependencies]
volatile = "0.2.6"
```
Make sure to specify `volatile` version `0.2.6`. Newer versions of the crate are not compatible with this post.
`0.2.6` is the [semantic] version number. For more information, see the [Specifying Dependencies] guide of the cargo documentation.
[semantic]: https://semver.org/
[Specifying Dependencies]: https://doc.crates.io/specifying-dependencies.html
Let's use it to make writes to the VGA buffer volatile. We update our `Buffer` type as follows:
```rust
// in src/vga_buffer.rs
use volatile::Volatile;
struct Buffer {
chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
Instead of a `ScreenChar`, we're now using a `Volatile`. (The `Volatile` type is [generic] and can wrap (almost) any type). This ensures that we can't accidentally write to it “normally”. Instead, we have to use the `write` method now.
[generic]: https://doc.rust-lang.org/book/ch10-01-syntax.html
This means that we have to update our `Writer::write_byte` method:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
...
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code,
});
...
}
}
}
...
}
```
Instead of a typical assignment using `=`, we're now using the `write` method. Now we can guarantee that the compiler will never optimize away this write.
### Formatting Macros
It would be nice to support Rust's formatting macros, too. That way, we can easily print different types, like integers or floats. To support them, we need to implement the [`core::fmt::Write`] trait. The only required method of this trait is `write_str`, which looks quite similar to our `write_string` method, just with a `fmt::Result` return type:
[`core::fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
```rust
// in src/vga_buffer.rs
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
```
The `Ok(())` is just a `Ok` Result containing the `()` type.
Now we can use Rust's built-in `write!`/`writeln!` formatting macros:
```rust
// in src/vga_buffer.rs
pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}
```
Now you should see a `Hello! The numbers are 42 and 0.3333333333333333` at the bottom of the screen. The `write!` call returns a `Result` which causes a warning if not used, so we call the [`unwrap`] function on it, which panics if an error occurs. This isn't a problem in our case, since writes to the VGA buffer never fail.
[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap
### Newlines
Right now, we just ignore newlines and characters that don't fit into the line anymore. Instead, we want to move every character one line up (the top line gets deleted) and start at the beginning of the last line again. To do this, we add an implementation for the `new_line` method of `Writer`:
```rust
// in src/vga_buffer.rs
impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {/* TODO */}
}
```
We iterate over all the screen characters and move each character one row up. Note that the upper bound of the range notation (`..`) is exclusive. We also omit the 0th row (the first range starts at `1`) because it's the row that is shifted off screen.
To finish the newline code, we add the `clear_row` method:
```rust
// in src/vga_buffer.rs
impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
}
```
This method clears a row by overwriting all of its characters with a space character.
## A Global Interface
To provide a global writer that can be used as an interface from other modules without carrying a `Writer` instance around, we try to create a static `WRITER`:
```rust
// in src/vga_buffer.rs
pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
```
However, if we try to compile it now, the following errors occur:
```
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/vga_buffer.rs:7:17
|
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0396]: raw pointers cannot be dereferenced in statics
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:13
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
```
To understand what's happening here, we need to know that statics are initialized at compile time, in contrast to normal variables that are initialized at run time. The component of the Rust compiler that evaluates such initialization expressions is called the “[const evaluator]”. Its functionality is still limited, but there is ongoing work to expand it, for example in the “[Allow panicking in constants]” RFC.
[const evaluator]: https://rustc-dev-guide.rust-lang.org/const-eval.html
[Allow panicking in constants]: https://github.com/rust-lang/rfcs/pull/2345
The issue with `ColorCode::new` would be solvable by using [`const` functions], but the fundamental problem here is that Rust's const evaluator is not able to convert raw pointers to references at compile time. Maybe it will work someday, but until then, we have to find another solution.
[`const` functions]: https://doc.rust-lang.org/reference/const_eval.html#const-functions
### Lazy Statics
The one-time initialization of statics with non-const functions is a common problem in Rust. Fortunately, there already exists a good solution in a crate named [lazy_static]. This crate provides a `lazy_static!` macro that defines a lazily initialized `static`. Instead of computing its value at compile time, the `static` lazily initializes itself when accessed for the first time. Thus, the initialization happens at runtime, so arbitrarily complex initialization code is possible.
[lazy_static]: https://docs.rs/lazy_static/1.0.1/lazy_static/
Let's add the `lazy_static` crate to our project:
```toml
# in Cargo.toml
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
```
We need the `spin_no_std` feature, since we don't link the standard library.
With `lazy_static`, we can define our static `WRITER` without problems:
```rust
// in src/vga_buffer.rs
use lazy_static::lazy_static;
lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}
```
However, this `WRITER` is pretty useless since it is immutable. This means that we can't write anything to it (since all the write methods take `&mut self`). One possible solution would be to use a [mutable static]. But then every read and write to it would be unsafe since it could easily introduce data races and other bad things. Using `static mut` is highly discouraged. There were even proposals to [remove it][remove static mut]. But what are the alternatives? We could try to use an immutable static with a cell type like [RefCell] or even [UnsafeCell] that provides [interior mutability]. But these types aren't [Sync] \(with good reason), so we can't use them in statics.
[mutable static]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437
[RefCell]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt
[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html
[interior mutability]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html
### Spinlocks
To get synchronized interior mutability, users of the standard library can use [Mutex]. It provides mutual exclusion by blocking threads when the resource is already locked. But our basic kernel does not have any blocking support or even a concept of threads, so we can't use it either. However, there is a really basic kind of mutex in computer science that requires no operating system features: the [spinlock]. Instead of blocking, the threads simply try to lock it again and again in a tight loop, thus burning CPU time until the mutex is free again.
[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html
[spinlock]: https://en.wikipedia.org/wiki/Spinlock
To use a spinning mutex, we can add the [spin crate] as a dependency:
[spin crate]: https://crates.io/crates/spin
```toml
# in Cargo.toml
[dependencies]
spin = "0.5.2"
```
Then we can use the spinning mutex to add safe [interior mutability] to our static `WRITER`:
```rust
// in src/vga_buffer.rs
use spin::Mutex;
...
lazy_static! {
pub static ref WRITER: Mutex = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
```
Now we can delete the `print_something` function and print directly from our `_start` function:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
use core::fmt::Write;
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
loop {}
}
```
We need to import the `fmt::Write` trait in order to be able to use its functions.
### Safety
Note that we only have a single unsafe block in our code, which is needed to create a `Buffer` reference pointing to `0xb8000`. Afterwards, all operations are safe. Rust uses bounds checking for array accesses by default, so we can't accidentally write outside the buffer. Thus, we encoded the required conditions in the type system and are able to provide a safe interface to the outside.
### A println Macro
Now that we have a global writer, we can add a `println` macro that can be used from anywhere in the codebase. Rust's [macro syntax] is a bit strange, so we won't try to write a macro from scratch. Instead, we look at the source of the [`println!` macro] in the standard library:
[macro syntax]: https://doc.rust-lang.org/nightly/book/ch20-05-macros.html#declarative-macros-for-general-metaprogramming
[`println!` macro]: https://doc.rust-lang.org/nightly/std/macro.println!.html
```rust
#[macro_export]
macro_rules! println {
() => (print!("\n"));
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}
```
Macros are defined through one or more rules, similar to `match` arms. The `println` macro has two rules: The first rule is for invocations without arguments, e.g., `println!()`, which is expanded to `print!("\n")` and thus just prints a newline. The second rule is for invocations with parameters such as `println!("Hello")` or `println!("Number: {}", 4)`. It is also expanded to an invocation of the `print!` macro, passing all arguments and an additional newline `\n` at the end.
The `#[macro_export]` attribute makes the macro available to the whole crate (not just the module it is defined in) and external crates. It also places the macro at the crate root, which means we have to import the macro through `use std::println` instead of `std::macros::println`.
The [`print!` macro] is defined as:
[`print!` macro]: https://doc.rust-lang.org/nightly/std/macro.print!.html
```rust
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
```
The macro expands to a call of the [`_print` function] in the `io` module. The [`$crate` variable] ensures that the macro also works from outside the `std` crate by expanding to `std` when it's used in other crates.
The [`format_args` macro] builds a [fmt::Arguments] type from the passed arguments, which is passed to `_print`. The [`_print` function] of libstd calls `print_to`, which is rather complicated because it supports different `Stdout` devices. We don't need that complexity since we just want to print to the VGA buffer.
[`_print` function]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698
[`$crate` variable]: https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate
[`format_args` macro]: https://doc.rust-lang.org/nightly/std/macro.format_args.html
[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html
To print to the VGA buffer, we just copy the `println!` and `print!` macros, but modify them to use our own `_print` function:
```rust
// in src/vga_buffer.rs
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
```
One thing that we changed from the original `println` definition is that we prefixed the invocations of the `print!` macro with `$crate` too. This ensures that we don't need to import the `print!` macro too if we only want to use `println`.
Like in the standard library, we add the `#[macro_export]` attribute to both macros to make them available everywhere in our crate. Note that this places the macros in the root namespace of the crate, so importing them via `use crate::vga_buffer::println` does not work. Instead, we have to do `use crate::println`.
The `_print` function locks our static `WRITER` and calls the `write_fmt` method on it. This method is from the `Write` trait, which we need to import. The additional `unwrap()` at the end panics if printing isn't successful. But since we always return `Ok` in `write_str`, that should not happen.
Since the macros need to be able to call `_print` from outside of the module, the function needs to be public. However, since we consider this a private implementation detail, we add the [`doc(hidden)` attribute] to hide it from the generated documentation.
[`doc(hidden)` attribute]: https://doc.rust-lang.org/nightly/rustdoc/write-documentation/the-doc-attribute.html#hidden
### Hello World using `println`
Now we can use `println` in our `_start` function:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
loop {}
}
```
Note that we don't have to import the macro in the main function, because it already lives in the root namespace.
As expected, we now see a _“Hello World!”_ on the screen:

### Printing Panic Messages
Now that we have a `println` macro, we can use it in our panic function to print the panic message and the location of the panic:
```rust
// in main.rs
/// This function is called on panic.
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
```
When we now insert `panic!("Some panic message");` in our `_start` function, we get the following output:

So we know not only that a panic has occurred, but also the panic message and where in the code it happened.
## Summary
In this post, we learned about the structure of the VGA text buffer and how it can be written through the memory mapping at address `0xb8000`. We created a Rust module that encapsulates the unsafety of writing to this memory-mapped buffer and presents a safe and convenient interface to the outside.
Thanks to cargo, we also saw how easy it is to add dependencies on third-party libraries. The two dependencies that we added, `lazy_static` and `spin`, are very useful in OS development and we will use them in more places in future posts.
## What's next?
The next post explains how to set up Rust's built-in unit test framework. We will then create some basic unit tests for the VGA buffer module from this post.
================================================
FILE: blog/content/edition-2/posts/03-vga-text-buffer/index.pt-BR.md
================================================
+++
title = "Modo de Texto VGA"
weight = 3
path = "pt-BR/vga-text-mode"
date = 2018-02-26
[extra]
chapter = "O Básico"
# Please update this when updating the translation
translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
O [modo de texto VGA] é uma maneira simples de imprimir texto na tela. Neste post, criamos uma interface que torna seu uso seguro e simples ao encapsular toda a unsafety em um módulo separado. Também implementamos suporte para as [macros de formatação] do Rust.
[modo de texto VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
[macros de formatação]: https://doc.rust-lang.org/std/fmt/#related-macros
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-03`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[na parte inferior]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
## O Buffer de Texto VGA
Para imprimir um caractere na tela em modo de texto VGA, é preciso escrevê-lo no buffer de texto do hardware VGA. O buffer de texto VGA é um array bidimensional com tipicamente 25 linhas e 80 colunas, que é renderizado diretamente na tela. Cada entrada do array descreve um único caractere da tela através do seguinte formato:
| Bit(s) | Valor |
| ------ | --------------------- |
| 0-7 | Ponto de código ASCII |
| 8-11 | Cor do primeiro plano |
| 12-14 | Cor do fundo |
| 15 | Piscar |
O primeiro byte representa o caractere que deve ser impresso na [codificação ASCII]. Para ser mais específico, não é exatamente ASCII, mas um conjunto de caracteres chamado [_página de código 437_] com alguns caracteres adicionais e pequenas modificações. Para simplificar, continuaremos chamando-o de caractere ASCII neste post.
[codificação ASCII]: https://en.wikipedia.org/wiki/ASCII
[_página de código 437_]: https://en.wikipedia.org/wiki/Code_page_437
O segundo byte define como o caractere é exibido. Os primeiros quatro bits definem a cor do primeiro plano, os próximos três bits a cor do fundo, e o último bit se o caractere deve piscar. As seguintes cores estão disponíveis:
| Número | Cor | Número + Bit Brilhante | Cor Brilhante |
| ------ | ----------- | ---------------------- | -------------- |
| 0x0 | Preto | 0x8 | Cinza Escuro |
| 0x1 | Azul | 0x9 | Azul Claro |
| 0x2 | Verde | 0xa | Verde Claro |
| 0x3 | Ciano | 0xb | Ciano Claro |
| 0x4 | Vermelho | 0xc | Vermelho Claro |
| 0x5 | Magenta | 0xd | Rosa |
| 0x6 | Marrom | 0xe | Amarelo |
| 0x7 | Cinza Claro | 0xf | Branco |
O bit 4 é o _bit brilhante_, que transforma, por exemplo, azul em azul claro. Para a cor de fundo, este bit é reaproveitado como o bit de piscar.
O buffer de texto VGA é acessível via [I/O mapeado em memória] no endereço `0xb8000`. Isso significa que leituras e escritas naquele endereço não acessam a RAM, mas acessam diretamente o buffer de texto no hardware VGA. Isso significa que podemos lê-lo e escrevê-lo através de operações normais de memória naquele endereço.
[I/O mapeado em memória]: https://en.wikipedia.org/wiki/Memory-mapped_I/O
Note que hardware mapeado em memória pode não suportar todas as operações normais de RAM. Por exemplo, um dispositivo poderia suportar apenas leituras byte a byte e retornar lixo quando um `u64` é lido. Felizmente, o buffer de texto [suporta leituras e escritas normais], então não precisamos tratá-lo de maneira especial.
[suporta leituras e escritas normais]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip
## Um Módulo Rust
Agora que sabemos como o buffer VGA funciona, podemos criar um módulo Rust para lidar com a impressão:
```rust
// em src/main.rs
mod vga_buffer;
```
Para o conteúdo deste módulo, criamos um novo arquivo `src/vga_buffer.rs`. Todo o código abaixo vai para nosso novo módulo (a menos que especificado o contrário).
### Cores
Primeiro, representamos as diferentes cores usando um enum:
```rust
// em src/vga_buffer.rs
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
```
Usamos um [enum estilo C] aqui para especificar explicitamente o número para cada cor. Por causa do atributo `repr(u8)`, cada variante do enum é armazenada como um `u8`. Na verdade, 4 bits seriam suficientes, mas Rust não tem um tipo `u4`.
[enum estilo C]: https://doc.rust-lang.org/rust-by-example/custom_types/enum/c_like.html
Normalmente o compilador emitiria um aviso para cada variante não utilizada. Ao usar o atributo `#[allow(dead_code)]`, desabilitamos esses avisos para o enum `Color`.
Ao [derivar] as traits [`Copy`], [`Clone`], [`Debug`], [`PartialEq`] e [`Eq`], habilitamos [semântica de cópia] para o tipo e o tornamos imprimível e comparável.
[derivar]: https://doc.rust-lang.org/rust-by-example/trait/derive.html
[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html
[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html
[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html
[`PartialEq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html
[`Eq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html
[semântica de cópia]: https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types
Para representar um código de cor completo que especifica as cores de primeiro plano e de fundo, criamos um [newtype] em cima de `u8`:
[newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html
```rust
// em src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
```
A struct `ColorCode` contém o byte de cor completo, contendo as cores de primeiro plano e de fundo. Como antes, derivamos as traits `Copy` e `Debug` para ela. Para garantir que o `ColorCode` tenha exatamente o mesmo layout de dados que um `u8`, usamos o atributo [`repr(transparent)`].
[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent
### Buffer de Texto
Agora podemos adicionar estruturas para representar um caractere da tela e o buffer de texto:
```rust
// em src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
Como a ordenação dos campos em structs padrão é indefinida em Rust, precisamos do atributo [`repr(C)`]. Ele garante que os campos da struct sejam dispostos exatamente como em uma struct C e, portanto, garante a ordenação correta dos campos. Para a struct `Buffer`, usamos [`repr(transparent)`] novamente para garantir que ela tenha o mesmo layout de memória que seu único campo.
[`repr(C)`]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc
Para realmente escrever na tela, agora criamos um tipo writer:
```rust
// em src/vga_buffer.rs
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
```
O writer sempre escreverá na última linha e deslocará as linhas para cima quando uma linha estiver cheia (ou no `\n`). O campo `column_position` acompanha a posição atual na última linha. As cores de primeiro plano e de fundo atuais são especificadas por `color_code` e uma referência ao buffer VGA é armazenada em `buffer`. Note que precisamos de um [lifetime explícito] aqui para dizer ao compilador por quanto tempo a referência é válida. O lifetime [`'static`] especifica que a referência é válida durante toda a execução do programa (o que é verdade para o buffer de texto VGA).
[lifetime explícito]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax
[`'static`]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime
### Impressão
Agora podemos usar o `Writer` para modificar os caracteres do buffer. Primeiro criamos um método para escrever um único byte ASCII:
```rust
// em src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}
fn new_line(&mut self) {/* TODO */}
}
```
Se o byte é o byte de [newline] `\n`, o writer não imprime nada. Em vez disso, ele chama um método `new_line`, que implementaremos mais tarde. Outros bytes são impressos na tela no segundo caso `match`.
[newline]: https://en.wikipedia.org/wiki/Newline
Ao imprimir um byte, o writer verifica se a linha atual está cheia. Nesse caso, uma chamada `new_line` é usada para quebrar a linha. Então ele escreve um novo `ScreenChar` no buffer na posição atual. Finalmente, a posição da coluna atual é avançada.
Para imprimir strings inteiras, podemos convertê-las em bytes e imprimi-los um por um:
```rust
// em src/vga_buffer.rs
impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// byte ASCII imprimível ou newline
0x20..=0x7e | b'\n' => self.write_byte(byte),
// não faz parte da faixa ASCII imprimível
_ => self.write_byte(0xfe),
}
}
}
}
```
O buffer de texto VGA suporta apenas ASCII e os bytes adicionais da [página de código 437]. Strings Rust são [UTF-8] por padrão, então podem conter bytes que não são suportados pelo buffer de texto VGA. Usamos um `match` para diferenciar bytes ASCII imprimíveis (um newline ou qualquer coisa entre um caractere de espaço e um caractere `~`) e bytes não imprimíveis. Para bytes não imprimíveis, imprimimos um caractere `■`, que tem o código hexadecimal `0xfe` no hardware VGA.
[página de código 437]: https://en.wikipedia.org/wiki/Code_page_437
[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm
#### Experimente!
Para escrever alguns caracteres na tela, você pode criar uma função temporária:
```rust
// em src/vga_buffer.rs
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}
```
Primeiro ele cria um novo Writer que aponta para o buffer VGA em `0xb8000`. A sintaxe para isso pode parecer um pouco estranha: Primeiro, convertemos o inteiro `0xb8000` como um [ponteiro bruto] mutável. Então o convertemos em uma referência mutável ao desreferenciá-lo (através de `*`) e imediatamente emprestar novamente (através de `&mut`). Esta conversão requer um [bloco `unsafe`], pois o compilador não pode garantir que o ponteiro bruto é válido.
[ponteiro bruto]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer
[bloco `unsafe`]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html
Então ele escreve o byte `b'H'` nele. O prefixo `b` cria um [byte literal], que representa um caractere ASCII. Ao escrever as strings `"ello "` e `"Wörld!"`, testamos nosso método `write_string` e o tratamento de caracteres não imprimíveis. Para ver a saída, precisamos chamar a função `print_something` da nossa função `_start`:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
vga_buffer::print_something();
loop {}
}
```
Quando executamos nosso projeto agora, um `Hello W■■rld!` deve ser impresso no canto inferior _esquerdo_ da tela em amarelo:
[byte literal]: https://doc.rust-lang.org/reference/tokens.html#byte-literals

Note que o `ö` é impresso como dois caracteres `■`. Isso ocorre porque `ö` é representado por dois bytes em [UTF-8], que ambos não se enquadram na faixa ASCII imprimível. Na verdade, esta é uma propriedade fundamental do UTF-8: os bytes individuais de valores multi-byte nunca são ASCII válido.
### Volatile
Acabamos de ver que nossa mensagem foi impressa corretamente. No entanto, pode não funcionar com futuros compiladores Rust que otimizam de forma mais agressiva.
O problema é que escrevemos apenas no `Buffer` e nunca lemos dele novamente. O compilador não sabe que realmente acessamos memória do buffer VGA (em vez de RAM normal) e não sabe nada sobre o efeito colateral de que alguns caracteres aparecem na tela. Então ele pode decidir que essas escritas são desnecessárias e podem ser omitidas. Para evitar esta otimização errônea, precisamos especificar essas escritas como _[volatile]_. Isso diz ao compilador que a escrita tem efeitos colaterais e não deve ser otimizada.
[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming)
Para usar escritas volatile para o buffer VGA, usamos a biblioteca [volatile][volatile crate]. Esta _crate_ (é assim que os pacotes são chamados no mundo Rust) fornece um tipo wrapper `Volatile` com métodos `read` e `write`. Esses métodos usam internamente as funções [read_volatile] e [write_volatile] da biblioteca core e, portanto, garantem que as leituras/escritas não sejam otimizadas.
[volatile crate]: https://docs.rs/volatile
[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html
[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html
Podemos adicionar uma dependência na crate `volatile` adicionando-a à seção `dependencies` do nosso `Cargo.toml`:
```toml
# em Cargo.toml
[dependencies]
volatile = "0.2.6"
```
Certifique-se de especificar a versão `0.2.6` do `volatile`. Versões mais novas da crate não são compatíveis com este post.
`0.2.6` é o número de versão [semântico]. Para mais informações, veja o guia [Specifying Dependencies] da documentação do cargo.
[semântico]: https://semver.org/
[Specifying Dependencies]: https://doc.crates.io/specifying-dependencies.html
Vamos usá-lo para tornar as escritas no buffer VGA volatile. Atualizamos nosso tipo `Buffer` da seguinte forma:
```rust
// em src/vga_buffer.rs
use volatile::Volatile;
struct Buffer {
chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
Em vez de um `ScreenChar`, agora estamos usando um `Volatile`. (O tipo `Volatile` é [genérico] e pode envolver (quase) qualquer tipo). Isso garante que não possamos escrever nele "normalmente" acidentalmente. Em vez disso, temos que usar o método `write` agora.
[genérico]: https://doc.rust-lang.org/book/ch10-01-syntax.html
Isso significa que temos que atualizar nosso método `Writer::write_byte`:
```rust
// em src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
...
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code,
});
...
}
}
}
...
}
```
Em vez de uma atribuição típica usando `=`, agora estamos usando o método `write`. Agora podemos garantir que o compilador nunca otimizará esta escrita.
### Macros de Formatação
Seria bom suportar as macros de formatação do Rust também. Dessa forma, podemos facilmente imprimir diferentes tipos, como inteiros ou floats. Para suportá-las, precisamos implementar a trait [`core::fmt::Write`]. O único método necessário desta trait é `write_str`, que se parece bastante com nosso método `write_string`, apenas com um tipo de retorno `fmt::Result`:
[`core::fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
```rust
// em src/vga_buffer.rs
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
```
O `Ok(())` é apenas um Result `Ok` contendo o tipo `()`.
Agora podemos usar as macros de formatação `write!`/`writeln!` embutidas do Rust:
```rust
// em src/vga_buffer.rs
pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}
```
Agora você deve ver um `Hello! The numbers are 42 and 0.3333333333333333` na parte inferior da tela. A chamada `write!` retorna um `Result` que causa um aviso se não usado, então chamamos a função [`unwrap`] nele, que entra em panic se ocorrer um erro. Isso não é um problema no nosso caso, pois escritas no buffer VGA nunca falham.
[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap
### Newlines
Agora, simplesmente ignoramos newlines e caracteres que não cabem mais na linha. Em vez disso, queremos mover cada caractere uma linha para cima (a linha superior é excluída) e começar no início da última linha novamente. Para fazer isso, adicionamos uma implementação para o método `new_line` do `Writer`:
```rust
// em src/vga_buffer.rs
impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {/* TODO */}
}
```
Iteramos sobre todos os caracteres da tela e movemos cada caractere uma linha para cima. Note que o limite superior da notação de intervalo (`..`) é exclusivo. Também omitimos a linha 0 (o primeiro intervalo começa em `1`) porque é a linha que é deslocada para fora da tela.
Para finalizar o código de newline, adicionamos o método `clear_row`:
```rust
// em src/vga_buffer.rs
impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
}
```
Este método limpa uma linha sobrescrevendo todos os seus caracteres com um caractere de espaço.
## Uma Interface Global
Para fornecer um writer global que possa ser usado como uma interface de outros módulos sem carregar uma instância `Writer` por aí, tentamos criar um `WRITER` static:
```rust
// em src/vga_buffer.rs
pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
```
No entanto, se tentarmos compilá-lo agora, os seguintes erros ocorrem:
```
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/vga_buffer.rs:7:17
|
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0396]: raw pointers cannot be dereferenced in statics
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:13
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
```
Para entender o que está acontecendo aqui, precisamos saber que statics são inicializados em tempo de compilação, ao contrário de variáveis normais que são inicializadas em tempo de execução. O componente do compilador Rust que avalia tais expressões de inicialização é chamado de "[const evaluator]". Sua funcionalidade ainda é limitada, mas há trabalho contínuo para expandi-la, por exemplo no RFC "[Allow panicking in constants]".
[const evaluator]: https://rustc-dev-guide.rust-lang.org/const-eval.html
[Allow panicking in constants]: https://github.com/rust-lang/rfcs/pull/2345
O problema com `ColorCode::new` seria solucionável usando [funções `const`], mas o problema fundamental aqui é que o const evaluator do Rust não é capaz de converter ponteiros brutos em referências em tempo de compilação. Talvez funcione algum dia, mas até lá, precisamos encontrar outra solução.
[funções `const`]: https://doc.rust-lang.org/reference/const_eval.html#const-functions
### Lazy Statics
A inicialização única de statics com funções não const é um problema comum em Rust. Felizmente, já existe uma boa solução em uma crate chamada [lazy_static]. Esta crate fornece uma macro `lazy_static!` que define um `static` inicializado lazily. Em vez de calcular seu valor em tempo de compilação, o `static` se inicializa lazily quando é acessado pela primeira vez. Assim, a inicialização acontece em tempo de execução, então código de inicialização arbitrariamente complexo é possível.
[lazy_static]: https://docs.rs/lazy_static/1.0.1/lazy_static/
Vamos adicionar a crate `lazy_static` ao nosso projeto:
```toml
# em Cargo.toml
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
```
Precisamos do recurso `spin_no_std`, já que não vinculamos a biblioteca padrão.
Com `lazy_static`, podemos definir nosso `WRITER` static sem problemas:
```rust
// em src/vga_buffer.rs
use lazy_static::lazy_static;
lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}
```
No entanto, este `WRITER` é praticamente inútil, pois é imutável. Isso significa que não podemos escrever nada nele (já que todos os métodos de escrita recebem `&mut self`). Uma solução possível seria usar um [static mutável]. Mas então cada leitura e escrita nele seria unsafe, pois poderia facilmente introduzir data races e outras coisas ruins. Usar `static mut` é altamente desencorajado. Até houve propostas para [removê-lo][remove static mut]. Mas quais são as alternativas? Poderíamos tentar usar um static imutável com um tipo de célula como [RefCell] ou até [UnsafeCell] que fornece [mutabilidade interior]. Mas esses tipos não são [Sync] \(com boa razão), então não podemos usá-los em statics.
[static mutável]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437
[RefCell]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt
[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html
[mutabilidade interior]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html
### Spinlocks
Para obter mutabilidade interior sincronizada, usuários da biblioteca padrão podem usar [Mutex]. Ele fornece exclusão mútua bloqueando threads quando o recurso já está bloqueado. Mas nosso kernel básico não tem nenhum suporte de bloqueio ou mesmo um conceito de threads, então também não podemos usá-lo. No entanto, há um tipo realmente básico de mutex na ciência da computação que não requer nenhum recurso de sistema operacional: o [spinlock]. Em vez de bloquear, as threads simplesmente tentam bloqueá-lo novamente e novamente em um loop apertado, queimando assim tempo de CPU até que o mutex esteja livre novamente.
[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html
[spinlock]: https://en.wikipedia.org/wiki/Spinlock
Para usar um spinning mutex, podemos adicionar a [crate spin] como uma dependência:
[crate spin]: https://crates.io/crates/spin
```toml
# em Cargo.toml
[dependencies]
spin = "0.5.2"
```
Então podemos usar o spinning mutex para adicionar [mutabilidade interior] segura ao nosso `WRITER` static:
```rust
// em src/vga_buffer.rs
use spin::Mutex;
...
lazy_static! {
pub static ref WRITER: Mutex = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
```
Agora podemos deletar a função `print_something` e imprimir diretamente da nossa função `_start`:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
use core::fmt::Write;
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
loop {}
}
```
Precisamos importar a trait `fmt::Write` para poder usar suas funções.
### Segurança
Note que temos apenas um bloco unsafe no nosso código, que é necessário para criar uma referência `Buffer` apontando para `0xb8000`. Depois disso, todas as operações são seguras. Rust usa verificação de limites para acessos a arrays por padrão, então não podemos escrever acidentalmente fora do buffer. Assim, codificamos as condições necessárias no sistema de tipos e somos capazes de fornecer uma interface segura para o exterior.
### Uma Macro println
Agora que temos um writer global, podemos adicionar uma macro `println` que pode ser usada de qualquer lugar na base de código. A [sintaxe de macro] do Rust é um pouco estranha, então não tentaremos escrever uma macro do zero. Em vez disso, olhamos para o código-fonte da [macro `println!`] na biblioteca padrão:
[sintaxe de macro]: https://doc.rust-lang.org/nightly/book/ch20-05-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming
[macro `println!`]: https://doc.rust-lang.org/nightly/std/macro.println!.html
```rust
#[macro_export]
macro_rules! println {
() => (print!("\n"));
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}
```
Macros são definidas através de uma ou mais regras, semelhantes a braços `match`. A macro `println` tem duas regras: A primeira regra é para invocações sem argumentos, por exemplo, `println!()`, que é expandida para `print!("\n")` e, portanto, apenas imprime um newline. A segunda regra é para invocações com parâmetros como `println!("Hello")` ou `println!("Number: {}", 4)`. Ela também é expandida para uma invocação da macro `print!`, passando todos os argumentos e um newline `\n` adicional no final.
O atributo `#[macro_export]` torna a macro disponível para toda a crate (não apenas o módulo em que é definida) e crates externas. Ele também coloca a macro na raiz da crate, o que significa que temos que importar a macro através de `use std::println` em vez de `std::macros::println`.
A [macro `print!`] é definida como:
[macro `print!`]: https://doc.rust-lang.org/nightly/std/macro.print!.html
```rust
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
```
A macro se expande para uma chamada da [função `_print`] no módulo `io`. A [variável `$crate`] garante que a macro também funcione de fora da crate `std` ao se expandir para `std` quando é usada em outras crates.
A [macro `format_args`] constrói um tipo [fmt::Arguments] dos argumentos passados, que é passado para `_print`. A [função `_print`] da libstd chama `print_to`, que é bastante complicado porque suporta diferentes dispositivos `Stdout`. Não precisamos dessa complexidade, pois só queremos imprimir no buffer VGA.
[função `_print`]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698
[variável `$crate`]: https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate
[macro `format_args`]: https://doc.rust-lang.org/nightly/std/macro.format_args.html
[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html
Para imprimir no buffer VGA, apenas copiamos as macros `println!` e `print!`, mas as modificamos para usar nossa própria função `_print`:
```rust
// em src/vga_buffer.rs
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
```
Uma coisa que mudamos da definição original de `println` é que também prefixamos as invocações da macro `print!` com `$crate`. Isso garante que não precisamos importar a macro `print!` também se quisermos usar apenas `println`.
Como na biblioteca padrão, adicionamos o atributo `#[macro_export]` a ambas as macros para torná-las disponíveis em todo lugar na nossa crate. Note que isso coloca as macros no namespace raiz da crate, então importá-las via `use crate::vga_buffer::println` não funciona. Em vez disso, temos que fazer `use crate::println`.
A função `_print` bloqueia nosso `WRITER` static e chama o método `write_fmt` nele. Este método é da trait `Write`, que precisamos importar. O `unwrap()` adicional no final entra em panic se a impressão não for bem-sucedida. Mas como sempre retornamos `Ok` em `write_str`, isso não deve acontecer.
Como as macros precisam ser capazes de chamar `_print` de fora do módulo, a função precisa ser pública. No entanto, como consideramos isso um detalhe de implementação privado, adicionamos o [atributo `doc(hidden)`] para ocultá-la da documentação gerada.
[atributo `doc(hidden)`]: https://doc.rust-lang.org/nightly/rustdoc/write-documentation/the-doc-attribute.html#hidden
### Hello World usando `println`
Agora podemos usar `println` na nossa função `_start`:
```rust
// em src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
loop {}
}
```
Note que não precisamos importar a macro na função main, porque ela já vive no namespace raiz.
Como esperado, agora vemos um _"Hello World!"_ na tela:

### Imprimindo Mensagens de Panic
Agora que temos uma macro `println`, podemos usá-la na nossa função panic para imprimir a mensagem de panic e a localização do panic:
```rust
// em main.rs
/// Esta função é chamada em caso de pânico.
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
```
Quando agora inserimos `panic!("Some panic message");` na nossa função `_start`, obtemos a seguinte saída:

Então sabemos não apenas que um panic ocorreu, mas também a mensagem de panic e onde no código aconteceu.
## Resumo
Neste post, aprendemos sobre a estrutura do buffer de texto VGA e como ele pode ser escrito através do mapeamento de memória no endereço `0xb8000`. Criamos um módulo Rust que encapsula a unsafety de escrever neste buffer mapeado em memória e apresenta uma interface segura e conveniente para o exterior.
Graças ao cargo, também vimos como é fácil adicionar dependências em bibliotecas de terceiros. As duas dependências que adicionamos, `lazy_static` e `spin`, são muito úteis no desenvolvimento de SO e as usaremos em mais lugares em posts futuros.
## O que vem a seguir?
O próximo post explica como configurar o framework de testes unitários embutido do Rust. Criaremos então alguns testes unitários básicos para o módulo de buffer VGA deste post.
================================================
FILE: blog/content/edition-2/posts/03-vga-text-buffer/index.zh-CN.md
================================================
+++
title = "VGA 字符模式"
weight = 3
path = "zh-CN/vga-text-mode"
date = 2018-02-26
[extra]
# Please update this when updating the translation
translation_based_on_commit = "bd6fbcb1c36705b2c474d7fcee387bfea1210851"
# GitHub usernames of the people that translated this post
translators = ["luojia65", "Rustin-Liu"]
# GitHub usernames of the people that contributed to this translation
translation_contributors = ["liuyuran"]
+++
**VGA 字符模式**([VGA text mode])是打印字符到屏幕的一种简单方式。在这篇文章中,为了包装这个模式为一个安全而简单的接口,我们将包装 unsafe 代码到独立的模块。我们还将实现对 Rust 语言**格式化宏**([formatting macros])的支持。
[VGA text mode]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode
[formatting macros]: https://doc.rust-lang.org/std/fmt/#related-macros
此博客在 [GitHub] 上公开开发. 如果您有任何问题或疑问,请在此处打开一个 issue。 您也可以在[底部][at the bottom]发表评论. 这篇文章的完整源代码可以在 [`post-03`] [post branch] 分支中找到。
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-03
## VGA 字符缓冲区
为了在 VGA 字符模式中向屏幕打印字符,我们必须将它写入硬件提供的 **VGA 字符缓冲区**(VGA text buffer)。通常状况下,VGA 字符缓冲区是一个 25 行、80 列的二维数组,它的内容将被实时渲染到屏幕。这个数组的元素被称作**字符单元**(character cell),它使用下面的格式描述一个屏幕上的字符:
| Bit(s) | Value |
| ------ | ---------------- |
| 0-7 | ASCII code point |
| 8-11 | Foreground color |
| 12-14 | Background color |
| 15 | Blink |
第一个字节表示了应当输出的 [ASCII 编码][ASCII encoding],更加准确的说,类似于 [437 字符编码表][_code page 437_] 中字符对应的编码,但又有细微的不同。 这里为了简化表达,我们在文章里将其简称为ASCII字符。
[ASCII encoding]: https://en.wikipedia.org/wiki/ASCII
[_code page 437_]: https://en.wikipedia.org/wiki/Code_page_437
第二个字节则定义了字符的显示方式,前四个比特定义了前景色,中间三个比特定义了背景色,最后一个比特则定义了该字符是否应该闪烁,以下是可用的颜色列表:
| Number | Color | Number + Bright Bit | Bright Color |
| ------ | ---------- | ------------------- | ------------ |
| 0x0 | Black | 0x8 | Dark Gray |
| 0x1 | Blue | 0x9 | Light Blue |
| 0x2 | Green | 0xa | Light Green |
| 0x3 | Cyan | 0xb | Light Cyan |
| 0x4 | Red | 0xc | Light Red |
| 0x5 | Magenta | 0xd | Pink |
| 0x6 | Brown | 0xe | Yellow |
| 0x7 | Light Gray | 0xf | White |
每个颜色的第四位称为**加亮位**(bright bit),比如blue加亮后就变成了light blue,但对于背景色,这个比特会被用于标记是否闪烁。
要修改 VGA 字符缓冲区,我们可以通过**存储器映射输入输出**([memory-mapped I/O](https://en.wikipedia.org/wiki/Memory-mapped_I/O))的方式,读取或写入地址 `0xb8000`;这意味着,我们可以像操作普通的内存区域一样操作这个地址。
需要注意的是,一些硬件虽然映射到存储器,但可能不会完全支持所有的内存操作:可能会有一些设备支持按 `u8` 字节读取,但在读取 `u64` 时返回无效的数据。幸运的是,字符缓冲区都[支持标准的读写操作](https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip),所以我们不需要用特殊的标准对待它。
## 包装到 Rust 模块
既然我们已经知道 VGA 文字缓冲区如何工作,也是时候创建一个 Rust 模块来处理文字打印了。我们输入这样的代码:
```rust
// in src/main.rs
mod vga_buffer;
```
我们的模块暂时不需要添加子模块,所以我们将它创建为 `src/vga_buffer.rs` 文件。除非另有说明,本文中的代码都保存到这个文件中。
### 颜色
首先,我们使用 Rust 的**枚举**(enum)表示特定的颜色:
```rust
// in src/vga_buffer.rs
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
```
我们使用**类似于 C 语言的枚举**(C-like enum),为每个颜色明确指定一个数字。在这里,每个用 `repr(u8)` 注记标注的枚举类型,都会以一个 `u8` 的形式存储——事实上 4 个二进制位就足够了,但 Rust 语言并不提供 `u4` 类型。
通常来说,编译器会对每个未使用的变量发出**警告**(warning);使用 `#[allow(dead_code)]`,我们可以对 `Color` 枚举类型禁用这个警告。
我们还**生成**([derive])了 [`Copy`](https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html)、[`Clone`](https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html)、[`Debug`](https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html)、[`PartialEq`](https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html) 和 [`Eq`](https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html) 这几个 trait:这让我们的类型遵循**复制语义**([copy semantics]),也让它可以被比较、被调试和打印。
[derive]: https://doc.rust-lang.org/rust-by-example/trait/derive.html
[copy semantics]: https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types
为了描述包含前景色和背景色的、完整的**颜色代码**(color code),我们基于 `u8` 创建一个新类型:
```rust
// in src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
```
这里,`ColorCode` 类型包装了一个完整的颜色代码字节,它包含前景色和背景色信息。和 `Color` 类型类似,我们为它生成 `Copy` 和 `Debug` 等一系列 trait。为了确保 `ColorCode` 和 `u8` 有完全相同的内存布局,我们添加 [repr(transparent) 标记](https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent)。
### 字符缓冲区
现在,我们可以添加更多的结构体,来描述屏幕上的字符和整个字符缓冲区:
```rust
// in src/vga_buffer.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
在内存布局层面,Rust 并不保证按顺序布局成员变量。因此,我们需要使用 `#[repr(C)]` 标记结构体;这将按 C 语言约定的顺序布局它的成员变量,让我们能正确地映射内存片段。对 `Buffer` 类型,我们再次使用 `repr(transparent)`,来确保类型和它的单个成员有相同的内存布局。
为了输出字符到屏幕,我们来创建一个 `Writer` 类型:
```rust
// in src/vga_buffer.rs
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
```
我们将让这个 `Writer` 类型将字符写入屏幕的最后一行,并在一行写满或接收到换行符 `\n` 的时候,将所有的字符向上位移一行。`column_position` 变量将跟踪光标在最后一行的位置。当前字符的前景和背景色将由 `color_code` 变量指定;另外,我们存入一个 VGA 字符缓冲区的可变借用到`buffer`变量中。需要注意的是,这里我们对借用使用**显式生命周期**([explicit lifetime](https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax)),告诉编译器这个借用在何时有效:我们使用 `'static` 生命周期(['static lifetime](https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime)),意味着这个借用应该在整个程序的运行期间有效;这对一个全局有效的 VGA 字符缓冲区来说,是非常合理的。
### 打印字符
现在我们可以使用 `Writer` 类型来更改缓冲区内的字符了。首先,为了写入一个 ASCII 码字节,我们创建这样的函数:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}
fn new_line(&mut self) {/* TODO */}
}
```
如果这个字节是一个**换行符**([line feed](https://en.wikipedia.org/wiki/Newline))字节 `\n`,我们的 `Writer` 不应该打印新字符,相反,它将调用我们稍后会实现的 `new_line` 方法;其它的字节应该将在 `match` 语句的第二个分支中被打印到屏幕上。
当打印字节时,`Writer` 将检查当前行是否已满。如果已满,它将首先调用 `new_line` 方法来将这一行字向上提升,再将一个新的 `ScreenChar` 写入到缓冲区,最终将当前的光标位置前进一位。
要打印整个字符串,我们把它转换为字节并依次输出:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// 可以是能打印的 ASCII 码字节,也可以是换行符
0x20..=0x7e | b'\n' => self.write_byte(byte),
// 不包含在上述范围之内的字节
_ => self.write_byte(0xfe),
}
}
}
}
```
VGA 字符缓冲区只支持 ASCII 码字节和**代码页 437**([Code page 437](https://en.wikipedia.org/wiki/Code_page_437))定义的字节。Rust 语言的字符串默认编码为 [UTF-8](https://www.fileformat.info/info/unicode/utf8.htm),也因此可能包含一些 VGA 字符缓冲区不支持的字节:我们使用 `match` 语句,来区别可打印的 ASCII 码或换行字节,和其它不可打印的字节。对每个不可打印的字节,我们打印一个 `■` 符号;这个符号在 VGA 硬件中被编码为十六进制的 `0xfe`。
我们可以亲自试一试已经编写的代码。为了这样做,我们可以临时编写一个函数:
```rust
// in src/vga_buffer.rs
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}
```
这个函数首先创建一个指向 `0xb8000` 地址VGA缓冲区的 `Writer`。实现这一点,我们需要编写的代码可能看起来有点奇怪:首先,我们把整数 `0xb8000` 强制转换为一个可变的**裸指针**([raw pointer](https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#dereferencing-a-raw-pointer));之后,通过运算符`*`,我们将这个裸指针解引用;最后,我们再通过 `&mut`,再次获得它的可变借用。这些转换需要 **`unsafe` 语句块**([unsafe block](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html)),因为编译器并不能保证这个裸指针是有效的。
然后它将字节 `b'H'` 写入缓冲区内. 前缀 `b` 创建了一个字节常量([byte literal](https://doc.rust-lang.org/reference/tokens.html#byte-literals)),表示单个 ASCII 码字符;通过尝试写入 `"ello "` 和 `"Wörld!"`,我们可以测试 `write_string` 方法和其后对无法打印字符的处理逻辑。为了观察输出,我们需要在 `_start` 函数中调用 `print_something` 方法:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
vga_buffer::print_something();
loop {}
}
```
编译运行后,黄色的 `Hello W■■rld!` 字符串将会被打印在屏幕的左下角:

需要注意的是,`ö` 字符被打印为两个 `■` 字符。这是因为在 [UTF-8](https://www.fileformat.info/info/unicode/utf8.htm) 编码下,字符 `ö` 是由两个字节表述的——而这两个字节并不处在可打印的 ASCII 码字节范围之内。事实上,这是 UTF-8 编码的基本特点之一:**如果一个字符占用多个字节,那么每个组成它的独立字节都不是有效的 ASCII 码字节**(the individual bytes of multi-byte values are never valid ASCII)。
### 易失操作
我们刚才看到,自己想要输出的信息被正确地打印到屏幕上。然而,未来 Rust 编译器更暴力的优化可能让这段代码不按预期工作。
产生问题的原因在于,我们只向 `Buffer` 写入,却不再从它读出数据。此时,编译器不知道我们事实上已经在操作 VGA 缓冲区内存,而不是在操作普通的 RAM——因此也不知道产生的**副效应**(side effect),即会有几个字符显示在屏幕上。这时,编译器也许会认为这些写入操作都没有必要,甚至会选择忽略这些操作!所以,为了避免这些并不正确的优化,这些写入操作应当被指定为[易失操作](https://en.wikipedia.org/wiki/Volatile_(computer_programming))。这将告诉编译器,这些写入可能会产生副效应,不应该被优化掉。
为了在我们的 VGA 缓冲区中使用易失的写入操作,我们使用 [volatile](https://docs.rs/volatile) 库。这个**包**(crate)提供一个名为 `Volatile` 的**包装类型**(wrapping type)和它的 `read`、`write` 方法;这些方法包装了 `core::ptr` 内的 [read_volatile](https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html) 和 [write_volatile](https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html) 函数,从而保证读操作或写操作不会被编译器优化。
要添加 `volatile` 包为项目的**依赖项**(dependency),我们可以在 `Cargo.toml` 文件的 `dependencies` 中添加下面的代码:
```toml
# in Cargo.toml
[dependencies]
volatile = "0.2.6"
```
`0.2.6` 表示一个**语义版本号**([semantic version number](https://semver.org/)),在 cargo 文档的[《指定依赖项》章节](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html)可以找到与它相关的使用指南。
现在,我们使用它来完成 VGA 缓冲区的 volatile 写入操作。我们将 `Buffer` 类型的定义修改为下列代码:
```rust
// in src/vga_buffer.rs
use volatile::Volatile;
struct Buffer {
chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
```
在这里,我们不使用 `ScreenChar` ,而选择使用 `Volatile` ——在这里,`Volatile` 类型是一个**泛型**([generic](https://doc.rust-lang.org/book/ch10-01-syntax.html)),可以包装几乎所有的类型——这确保了我们不会通过普通的写入操作,意外地向它写入数据;我们转而使用提供的 `write` 方法。
这意味着,我们必须要修改我们的 `Writer::write_byte` 方法:
```rust
// in src/vga_buffer.rs
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
...
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code: color_code,
});
...
}
}
}
...
}
```
正如代码所示,我们不再使用普通的 `=` 赋值,而使用了 `write` 方法:这能确保编译器不再优化这个写入操作。
### 格式化宏
支持 Rust 提供的**格式化宏**(formatting macros)也是一个很好的思路。通过这种途径,我们可以轻松地打印不同类型的变量,如整数或浮点数。为了支持它们,我们需要实现 [`core::fmt::Write`](https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html) trait;要实现它,唯一需要提供的方法是 `write_str`,它和我们先前编写的 `write_string` 方法差别不大,只是返回值类型变成了 `fmt::Result`:
```rust
// in src/vga_buffer.rs
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
```
这里,`Ok(())` 属于 `Result` 枚举类型中的 `Ok`,包含一个值为 `()` 的变量。
现在我们就可以使用 Rust 内置的格式化宏 `write!` 和 `writeln!` 了:
```rust
// in src/vga_buffer.rs
pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}
```
现在,你应该在屏幕下端看到一串 `Hello! The numbers are 42 and 0.3333333333333333`。`write!` 宏返回的 `Result` 类型必须被使用,所以我们调用它的 [`unwrap`](https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap) 方法,它将在错误发生时 panic。这里的情况下应该不会发生这样的问题,因为写入 VGA 字符缓冲区并没有可能失败。
### 换行
在之前的代码中,我们忽略了换行符,因此没有处理超出一行字符的情况。当换行时,我们想要把每个字符向上移动一行——此时最顶上的一行将被删除——然后在最后一行的起始位置继续打印。要做到这一点,我们要为 `Writer` 实现一个新的 `new_line` 方法:
```rust
// in src/vga_buffer.rs
impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {/* TODO */}
}
```
我们遍历每个屏幕上的字符,把每个字符移动到它上方一行的相应位置。这里,`..` 符号是**区间标号**(range notation)的一种;它表示左闭右开的区间,因此不包含它的上界。在外层的枚举中,我们从第 1 行开始,省略了对第 0 行的枚举过程——因为这一行应该被移出屏幕,即它将被下一行的字符覆写。
所以我们实现的 `clear_row` 方法代码如下:
```rust
// in src/vga_buffer.rs
impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
}
```
通过向对应的缓冲区写入空格字符,这个方法能清空一整行的字符位置。
## 全局接口
编写其它模块时,我们希望无需随时拥有 `Writer` 实例,便能使用它的方法。我们尝试创建一个静态的 `WRITER` 变量:
```rust
// in src/vga_buffer.rs
pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
```
我们尝试编译这些代码,却发生了下面的编译错误:
```
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/vga_buffer.rs:7:17
|
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0396]: raw pointers cannot be dereferenced in statics
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:13
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
```
为了明白现在发生了什么,我们需要知道一点:一般的变量在运行时初始化,而静态变量在编译时初始化。Rust编译器规定了一个称为**常量求值器**([const evaluator](https://rustc-dev-guide.rust-lang.org/const-eval.html))的组件,它应该在编译时处理这样的初始化工作。虽然它目前的功能较为有限,但对它的扩展工作进展活跃,比如允许在常量中 panic 的[一篇 RFC 文档](https://github.com/rust-lang/rfcs/pull/2345)。
关于 `ColorCode::new` 的问题应该能使用**常函数**([`const` functions](https://doc.rust-lang.org/reference/const_eval.html#const-functions))解决,但常量求值器还存在不完善之处,它还不能在编译时直接转换裸指针到变量的引用——也许未来这段代码能够工作,但在那之前,我们需要寻找另外的解决方案。
### 延迟初始化
使用非常函数初始化静态变量是 Rust 程序员普遍遇到的问题。幸运的是,有一个叫做 [lazy_static](https://docs.rs/lazy_static/1.0.1/lazy_static/) 的包提供了一个很棒的解决方案:它提供了名为 `lazy_static!` 的宏,定义了一个**延迟初始化**(lazily initialized)的静态变量;这个变量的值将在第一次使用时计算,而非在编译时计算。这时,变量的初始化过程将在运行时执行,任意的初始化代码——无论简单或复杂——都是能够使用的。
现在,我们将 `lazy_static` 包导入到我们的项目:
```toml
# in Cargo.toml
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
```
在这里,由于程序不连接标准库,我们需要启用 `spin_no_std` 特性。
使用 `lazy_static` 我们就可以定义一个不出问题的 `WRITER` 变量:
```rust
// in src/vga_buffer.rs
use lazy_static::lazy_static;
lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}
```
然而,这个 `WRITER` 可能没有什么用途,因为它目前还是**不可变变量**(immutable variable):这意味着我们无法向它写入数据,因为所有与写入数据相关的方法都需要实例的可变引用 `&mut self`。一种解决方案是使用**可变静态**([mutable static](https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable))的变量,但所有对它的读写操作都被规定为不安全的(unsafe)操作,因为这很容易导致数据竞争或发生其它不好的事情——使用 `static mut` 极其不被赞成,甚至有一些提案认为[应该将它删除](https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437)。也有其它的替代方案,比如可以尝试使用比如 [RefCell](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt) 或甚至 [UnsafeCell](https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html) 等类型提供的**内部可变性**([interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html));但这些类型都被设计为非同步类型,即不满足 [Sync](https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html) 约束,所以我们不能在静态变量中使用它们。
### spinlock
要定义同步的内部可变性,我们往往使用标准库提供的互斥锁类 [Mutex](https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html),它通过提供当资源被占用时将线程**阻塞**(block)的**互斥条件**(mutual exclusion)实现这一点;但我们初步的内核代码还没有线程和阻塞的概念,我们将不能使用这个类。不过,我们还有一种较为基础的互斥锁实现方式——**自旋锁**([spinlock](https://en.wikipedia.org/wiki/Spinlock))。自旋锁并不会调用阻塞逻辑,而是在一个小的无限循环中反复尝试获得这个锁,也因此会一直占用 CPU 时间,直到互斥锁被它的占用者释放。
为了使用自旋互斥锁,我们添加 [spin包](https://crates.io/crates/spin) 到项目的依赖项列表:
```toml
# in Cargo.toml
[dependencies]
spin = "0.5.2"
```
现在,我们能够使用自旋的互斥锁,为我们的 `WRITER` 类实现安全的[内部可变性](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html):
```rust
// in src/vga_buffer.rs
use spin::Mutex;
...
lazy_static! {
pub static ref WRITER: Mutex = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
```
现在我们可以删除 `print_something` 函数,尝试直接在 `_start` 函数中打印字符:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
use core::fmt::Write;
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
loop {}
}
```
在这里,我们需要导入名为 `fmt::Write` 的 trait,来使用实现它的类的相应方法。
### 安全性
经过上面的努力后,我们现在的代码只剩一个 unsafe 语句块,它用于创建一个指向 `0xb8000` 地址的 `Buffer` 类型引用;在这步之后,所有的操作都是安全的。Rust 将为每个数组访问检查边界,所以我们不会在不经意间越界到缓冲区之外。因此,我们把需要的条件编码到 Rust 的类型系统,这之后,我们为外界提供的接口就符合内存安全原则了。
### `println!` 宏
现在我们有了一个全局的 `Writer` 实例,我们就可以基于它实现 `println!` 宏,这样它就能被任意地方的代码使用了。Rust 提供的[宏定义语法](https://doc.rust-lang.org/nightly/book/ch20-05-macros.html#declarative-macros-for-general-metaprogramming)需要时间理解,所以我们将不从零开始编写这个宏。我们先看看标准库中 [`println!` 宏的实现源码](https://doc.rust-lang.org/nightly/std/macro.println!.html):
```rust
#[macro_export]
macro_rules! println {
() => (print!("\n"));
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}
```
宏是通过一个或多个**规则**(rule)定义的,这就像 `match` 语句的多个分支。`println!` 宏有两个规则:第一个规则不要求传入参数——就比如 `println!()` ——它将被扩展为 `print!("\n")`,因此只会打印一个新行;第二个要求传入参数——好比 `println!("Rust 能够编写操作系统")` 或 `println!("我学习 Rust 已经{}年了", 3)`——它将使用 `print!` 宏扩展,传入它需求的所有参数,并在输出的字符串最后加入一个换行符 `\n`。
这里,`#[macro_export]` 属性让整个包(crate)和基于它的包都能访问这个宏,而不仅限于定义它的模块(module)。它还将把宏置于包的根模块(crate root)下,这意味着比如我们需要通过 `use std::println` 来导入这个宏,而不是通过 `std::macros::println`。
[`print!` 宏](https://doc.rust-lang.org/nightly/std/macro.print!.html)是这样定义的:
```
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
```
这个宏将扩展为一个对 `io` 模块中 [`_print` 函数](https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698)的调用。[`$crate` 变量](https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate)将在 `std` 包之外被解析为 `std` 包,保证整个宏在 `std` 包之外也可以使用。
[`format_args!` 宏](https://doc.rust-lang.org/nightly/std/macro.format_args.html)将传入的参数搭建为一个 [fmt::Arguments](https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html) 类型,这个类型将被传入 `_print` 函数。`std` 包中的 [`_print` 函数](https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698)将调用复杂的私有函数 `print_to`,来处理对不同 `Stdout` 设备的支持。我们不需要编写这样的复杂函数,因为我们只需要打印到 VGA 字符缓冲区。
要打印到字符缓冲区,我们把 `println!` 和 `print!` 两个宏复制过来,但修改部分代码,让这些宏使用我们定义的 `_print` 函数:
```rust
// in src/vga_buffer.rs
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
```
我们首先修改了 `println!` 宏,在每个使用的 `print!` 宏前面添加了 `$crate` 变量。这样我们在只需要使用 `println!` 时,不必也编写代码导入 `print!` 宏。
就像标准库做的那样,我们为两个宏都添加了 `#[macro_export]` 属性,这样在包的其它地方也可以使用它们。需要注意的是,这将占用包的**根命名空间**(root namespace),所以我们不能通过 `use crate::vga_buffer::println` 来导入它们;我们应该使用 `use crate::println`。
另外,`_print` 函数将占有静态变量 `WRITER` 的锁,并调用它的 `write_fmt` 方法。这个方法是从名为 `Write` 的 trait 中获得的,所以我们需要导入这个 trait。额外的 `unwrap()` 函数将在打印不成功的时候 panic;但既然我们的 `write_str` 总是返回 `Ok`,这种情况不应该发生。
如果这个宏将能在模块外访问,它们也应当能访问 `_print` 函数,因此这个函数必须是公有的(public)。然而,考虑到这是一个私有的实现细节,我们添加一个 [`doc(hidden)` 属性](https://doc.rust-lang.org/nightly/rustdoc/write-documentation/the-doc-attribute.html#hidden),防止它在生成的文档中出现。
### 使用 `println!` 的 Hello World
现在,我们可以在 `_start` 里使用 `println!` 了:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() {
println!("Hello World{}", "!");
loop {}
}
```
要注意的是,我们在入口函数中不需要导入这个宏——因为它已经被置于包的根命名空间了。
运行这段代码,和我们预料的一样,一个 *“Hello World!”* 字符串被打印到了屏幕上:

### 打印 panic 信息
既然我们已经有了 `println!` 宏,我们可以在 panic 处理函数中,使用它打印 panic 信息和 panic 产生的位置:
```rust
// in main.rs
/// 这个函数将在 panic 发生时被调用
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
```
当我们在 `_start` 函数中插入一行 `panic!("Some panic message");` 后,我们得到了这样的输出:

所以,现在我们不仅能知道 panic 已经发生,还能够知道 panic 信息和产生 panic 的代码。
## 小结
这篇文章中,我们学习了 VGA 字符缓冲区的结构,以及如何在 `0xb8000` 的内存映射地址访问它。我们将所有的不安全操作包装为一个 Rust 模块,以便在外界安全地访问它。
我们也发现了——感谢便于使用的 cargo——在 Rust 中使用第三方提供的包是及其容易的。我们添加的两个依赖项,`lazy_static` 和 `spin`,都在操作系统开发中及其有用;我们将在未来的文章中多次使用它们。
## 下篇预告
下一篇文章中,我们将会讲述如何配置 Rust 内置的单元测试框架。我们还将为本文编写的 VGA 缓冲区模块添加基础的单元测试项目。
================================================
FILE: blog/content/edition-2/posts/04-testing/index.es.md
================================================
+++
title = "Pruebas"
weight = 4
path = "es/testing"
date = 2019-04-27
[extra]
chapter = "Fundamentos"
comments_search_term = 1009
# GitHub usernames of the people that translated this post
translators = ["dobleuber"]
+++
Esta publicación explora las pruebas unitarias e integración en ejecutables `no_std`. Utilizaremos el soporte de Rust para marcos de prueba personalizados para ejecutar funciones de prueba dentro de nuestro núcleo. Para reportar los resultados fuera de QEMU, utilizaremos diferentes características de QEMU y la herramienta `bootimage`.
Este blog se desarrolla de manera abierta en [GitHub]. Si tienes algún problema o pregunta, por favor abre un problema allí. También puedes dejar comentarios [en la parte inferior]. El código fuente completo de esta publicación se puede encontrar en la rama [`post-04`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[en la parte inferior]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-04
## Requisitos
Esta publicación reemplaza las publicaciones (_Pruebas Unitarias_) y (_Pruebas de Integración_) (ahora obsoletas). Se asume que has seguido la publicación (_Un Núcleo Rust Mínimo_) después del 2019-04-27. Principalmente, requiere que tengas un archivo `.cargo/config.toml` que [establezca un objetivo predeterminado] y [defina un ejecutable de runner].
[_Pruebas Unitarias_]: @/edition-2/posts/deprecated/04-unit-testing/index.md
[_Pruebas de Integración_]: @/edition-2/posts/deprecated/05-integration-tests/index.md
[_Un Núcleo Rust Mínimo_]: @/edition-2/posts/02-minimal-rust-kernel/index.md
[establezca un objetivo predeterminado]: @/edition-2/posts/02-minimal-rust-kernel/index.md#set-a-default-target
[defina un ejecutable de runner]: @/edition-2/posts/02-minimal-rust-kernel/index.md#using-cargo-run
## Pruebas en Rust
Rust tiene un [marco de prueba incorporado] que es capaz de ejecutar pruebas unitarias sin la necesidad de configurar nada. Solo crea una función que verifique algunos resultados mediante afirmaciones y añade el atributo `#[test]` al encabezado de la función. Luego, `cargo test` encontrará y ejecutará automáticamente todas las funciones de prueba de tu crate.
[marco de prueba incorporado]: https://doc.rust-lang.org/book/ch11-00-testing.html
Desafortunadamente, es un poco más complicado para aplicaciones `no_std` como nuestro núcleo. El problema es que el marco de prueba de Rust utiliza implícitamente la biblioteca incorporada [`test`], que depende de la biblioteca estándar. Esto significa que no podemos usar el marco de prueba predeterminado para nuestro núcleo `#[no_std]`.
[`test`]: https://doc.rust-lang.org/test/index.html
Podemos ver esto cuando intentamos ejecutar `cargo test` en nuestro proyecto:
```
> cargo test
Compiling blog_os v0.1.0 (/…/blog_os)
error[E0463]: can't find crate for `test`
```
Dado que el crate `test` depende de la biblioteca estándar, no está disponible para nuestro objetivo de metal desnudo. Si bien portar el crate `test` a un contexto `#[no_std]` [es posible][utest], es altamente inestable y requiere algunos hacks, como redefinir el macro `panic`.
[utest]: https://github.com/japaric/utest
### Marcos de Prueba Personalizados
Afortunadamente, Rust soporta reemplazar el marco de prueba predeterminado a través de la característica inestable [`custom_test_frameworks`]. Esta característica no requiere bibliotecas externas y, por lo tanto, también funciona en entornos `#[no_std]`. Funciona recopilando todas las funciones anotadas con un atributo `#[test_case]` y luego invocando una función runner especificada por el usuario con la lista de pruebas como argumento. Así, proporciona a la implementación un control máximo sobre el proceso de prueba.
[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html
La desventaja en comparación con el marco de prueba predeterminado es que muchas características avanzadas, como las pruebas [`should_panic`], no están disponibles. En su lugar, depende de la implementación proporcionar tales características sí es necesario. Esto es ideal para nosotros ya que tenemos un entorno de ejecución muy especial en el que las implementaciones predeterminadas de tales características avanzadas probablemente no funcionarían de todos modos. Por ejemplo, el atributo `#[should_panic]` depende de desenrollar la pila para capturar los pánicos, lo cual hemos deshabilitado para nuestro núcleo.
[`should_panic`]: https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic
Para implementar un marco de prueba personalizado para nuestro núcleo, añadimos lo siguiente a nuestro `main.rs`:
```rust
// en src/main.rs
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Fn()]) {
println!("Ejecutando {} pruebas", tests.len());
for test in tests {
test();
}
}
```
Nuestro runner solo imprime un breve mensaje de depuración y luego llama a cada función de prueba en la lista. El tipo de argumento `&[&dyn Fn()]` es un [_slice_] de referencias de [_trait object_] del trait [_Fn()_]. Es básicamente una lista de referencias a tipos que pueden ser llamados como una función. Dado que la función es inútil para ejecuciones que no son de prueba, usamos el atributo `#[cfg(test)]` para incluirlo solo para pruebas.
[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html
[_trait object_]: https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html
[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html
Cuando ejecutamos `cargo test` ahora, vemos que ahora tiene éxito (si no lo tiene, consulta la nota a continuación). Sin embargo, todavía vemos nuestro "¡Hola Mundo!" en lugar del mensaje de nuestro `test_runner`. La razón es que nuestra función `_start` todavía se utiliza como punto de entrada. La característica de marcos de prueba personalizados genera una función `main` que llama a `test_runner`, pero esta función se ignora porque usamos el atributo `#[no_main]` y proporcionamos nuestra propia entrada.
**Nota:** Actualmente hay un error en cargo que conduce a errores de "elemento lang duplicado" en `cargo test` en algunos casos. Ocurre cuando has establecido `panic = "abort"` para un perfil en tu `Cargo.toml`. Intenta eliminarlo, luego `cargo test` debería funcionar. Alternativamente, si eso no funciona, añade `panic-abort-tests = true` a la sección `[unstable]` de tu archivo `.cargo/config.toml`. Consulta el [problema de cargo](https://github.com/rust-lang/cargo/issues/7359) para más información sobre esto.
Para solucionarlo, primero necesitamos cambiar el nombre de la función generada a algo diferente de `main` mediante el atributo `reexport_test_harness_main`. Luego podemos llamar a la función renombrada desde nuestra función `_start`:
```rust
// en src/main.rs
#![reexport_test_harness_main = "test_main"]
#[no_mangle]
pub extern "C" fn _start() -> ! {
println!("¡Hola Mundo{}!", "!");
#[cfg(test)]
test_main();
loop {}
}
```
Establecemos el nombre de la función de entrada del marco de prueba en `test_main` y la llamamos desde nuestro punto de entrada `_start`. Usamos [compilación condicional] para añadir la llamada a `test_main` solo en contextos de prueba porque la función no se genera en una ejecución normal.
Cuando ejecutamos `cargo test` ahora, vemos el mensaje "Ejecutando 0 pruebas" en la pantalla. Ahora estamos listos para crear nuestra primera función de prueba:
```rust
// en src/main.rs
#[test_case]
fn trivial_assertion() {
print!("aserción trivial... ");
assert_eq!(1, 1);
println!("[ok]");
}
```
Cuando ejecutamos `cargo test` ahora, vemos la siguiente salida:
![QEMU imprimiendo "¡Hola Mundo!", "Ejecutando 1 pruebas" y "aserción trivial... [ok]"](qemu-test-runner-output.png)
El slice `tests` pasado a nuestra función `test_runner` ahora contiene una referencia a la función `trivial_assertion`. A partir de la salida `aserción trivial... [ok]` en la pantalla, vemos que la prueba fue llamada y que tuvo éxito.
Después de ejecutar las pruebas, nuestro `test_runner` regresa a la función `test_main`, que a su vez regresa a nuestra función de entrada `_start`. Al final de `_start`, entramos en un bucle infinito porque la función de entrada no puede retornar. Este es un problema, porque queremos que `cargo test` salga después de ejecutar todas las pruebas.
## Salida de QEMU
En este momento, tenemos un bucle infinito al final de nuestra función `_start` y necesitamos cerrar QEMU manualmente en cada ejecución de `cargo test`. Esto es desafortunado porque también queremos ejecutar `cargo test` en scripts sin interacción del usuario. La solución limpia a esto sería implementar una forma adecuada de apagar nuestro OS. Desafortunadamente, esto es relativamente complejo porque requiere implementar soporte para el estándar de gestión de energía [APM] o [ACPI].
[APM]: https://wiki.osdev.org/APM
[ACPI]: https://wiki.osdev.org/ACPI
Afortunadamente, hay una salida: QEMU soporta un dispositivo especial `isa-debug-exit`, que proporciona una forma fácil de salir de QEMU desde el sistema invitado. Para habilitarlo, necesitamos pasar un argumento `-device` a QEMU. Podemos hacerlo añadiendo una clave de configuración `package.metadata.bootimage.test-args` en nuestro `Cargo.toml`:
```toml
# en Cargo.toml
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
```
La aplicación `bootimage runner` agrega los `test-args` al comando predeterminado de QEMU para todos los ejecutables de prueba. Para un `cargo run` normal, los argumentos se ignoran.
Junto con el nombre del dispositivo (`isa-debug-exit`), pasamos los dos parámetros `iobase` y `iosize` que especifican el _puerto de E/S_ a través del cual se puede alcanzar el dispositivo desde nuestro núcleo.
### Puertos de E/S
Hay dos enfoques diferentes para comunicar entre la CPU y el hardware periférico en x86, **E/S mapeada en memoria** y **E/S mapeada en puerto**. Ya hemos utilizado E/S mapeada en memoria para acceder al [buffer de texto VGA] a través de la dirección de memoria `0xb8000`. Esta dirección no está mapeada a RAM, sino a alguna memoria en el dispositivo VGA.
[buffer de texto VGA]: @/edition-2/posts/03-vga-text-buffer/index.md
En contraste, la E/S mapeada en puerto utiliza un bus de E/S separado para la comunicación. Cada periférico conectado tiene uno o más números de puerto. Para comunicarse con dicho puerto de E/S, existen instrucciones especiales de la CPU llamadas `in` y `out`, que toman un número de puerto y un byte de datos (también hay variaciones de estos comandos que permiten enviar un `u16` o `u32`).
El dispositivo `isa-debug-exit` utiliza E/S mapeada en puerto. El parámetro `iobase` especifica en qué dirección de puerto debe residir el dispositivo (`0xf4` es un puerto [generalmente no utilizado][list of x86 I/O ports] en el bus de E/S de x86) y el `iosize` especifica el tamaño de puerto (`0x04` significa cuatro bytes).
[list of x86 I/O ports]: https://wiki.osdev.org/I/O_Ports#The_list
### Usando el Dispositivo de Salida
La funcionalidad del dispositivo `isa-debug-exit` es muy simple. Cuando se escribe un `valor` en el puerto de E/S especificado por `iobase`, provoca que QEMU salga con un [código de salida] `(valor << 1) | 1`. Por lo tanto, cuando escribimos `0` en el puerto, QEMU saldrá con un código de salida `(0 << 1) | 1 = 1`, y cuando escribimos `1` en el puerto, saldrá con un código de salida `(1 << 1) | 1 = 3`.
[código de salida]: https://en.wikipedia.org/wiki/Exit_status
En lugar de invocar manualmente las instrucciones de ensamblaje `in` y `out`, utilizamos las abstracciones provistas por la crate [`x86_64`]. Para añadir una dependencia en esa crate, la añadimos a la sección de `dependencies` en nuestro `Cargo.toml`:
[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/
```toml
# en Cargo.toml
[dependencies]
x86_64 = "0.14.2"
```
Ahora podemos usar el tipo [`Port`] proporcionado por la crate para crear una función `exit_qemu`:
[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html
```rust
// en src/main.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
La función crea un nuevo [`Port`] en `0xf4`, que es el `iobase` del dispositivo `isa-debug-exit`. Luego escribe el código de salida pasado al puerto. Usamos `u32` porque especificamos el `iosize` del dispositivo `isa-debug-exit` como 4 bytes. Ambas operaciones son inseguras porque escribir en un puerto de E/S puede resultar en un comportamiento arbitrario.
Para especificar el código de salida, creamos un enum `QemuExitCode`. La idea es salir con el código de salida de éxito si todas las pruebas tuvieron éxito y con el código de salida de fallo de otro modo. El enum está marcado como `#[repr(u32)]` para representar cada variante como un entero `u32`. Usamos el código de salida `0x10` para éxito y `0x11` para fallo. Los códigos de salida reales no importan mucho, siempre y cuando no choquen con los códigos de salida predeterminados de QEMU. Por ejemplo, usar el código de salida `0` para éxito no es una buena idea porque se convierte en `(0 << 1) | 1 = 1` después de la transformación, que es el código de salida predeterminado cuando QEMU falla al ejecutarse. Así que no podríamos diferenciar un error de QEMU de una ejecución de prueba exitosa.
Ahora podemos actualizar nuestro `test_runner` para salir de QEMU después de que se hayan ejecutado todas las pruebas:
```rust
// en src/main.rs
fn test_runner(tests: &[&dyn Fn()]) {
println!("Ejecutando {} pruebas", tests.len());
for test in tests {
test();
}
/// nuevo
exit_qemu(QemuExitCode::Success);
}
```
Cuando ejecutamos `cargo test` ahora, vemos que QEMU se cierra inmediatamente después de ejecutar las pruebas. El problema es que `cargo test` interpreta la prueba como fallida aunque pasamos nuestro código de salida de éxito:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be
Building bootloader
Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader)
Finished release [optimized + debuginfo] target(s) in 1.07s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4,
iosize=0x04`
error: test failed, to rerun pass '--bin blog_os'
```
El problema es que `cargo test` considera todos los códigos de error que no sean `0` como fallidos.
### Código de salida de éxito
Para solucionar esto, `bootimage` proporciona una clave de configuración `test-success-exit-code` que mapea un código de salida especificado al código de salida `0`:
```toml
# en Cargo.toml
[package.metadata.bootimage]
test-args = […]
test-success-exit-code = 33 # (0x10 << 1) | 1
```
Con esta configuración, `bootimage` mapea nuestro código de salida de éxito al código de salida 0, de modo que `cargo test` reconozca correctamente el caso de éxito y no cuente la prueba como fallida.
Nuestro runner de pruebas ahora cierra automáticamente QEMU y reporta correctamente los resultados de las pruebas. Aún vemos que la ventana de QEMU permanece abierta por un breve período de tiempo, pero no es suficiente para leer los resultados. Sería agradable si pudiéramos imprimir los resultados de las pruebas en la consola en su lugar, para que podamos seguir viéndolos después de que QEMU salga.
## Imprimiendo en la Consola
Para ver la salida de las pruebas en la consola, necesitamos enviar los datos desde nuestro núcleo al sistema host de alguna manera. Hay varias formas de lograr esto, por ejemplo, enviando los datos a través de una interfaz de red TCP. Sin embargo, configurar una pila de red es una tarea bastante compleja, por lo que elegiremos una solución más simple.
### Puerto Serial
Una forma simple de enviar los datos es usar el [puerto serial], un estándar de interfaz antiguo que ya no se encuentra en computadoras modernas. Es fácil de programar y QEMU puede redirigir los bytes enviados a través del serial a la salida estándar del host o a un archivo.
[puerto serial]: https://en.wikipedia.org/wiki/Serial_port
Los chips que implementan una interfaz serial se llaman [UARTs]. Hay [muchos modelos de UART] en x86, pero afortunadamente las únicas diferencias entre ellos son algunas características avanzadas que no necesitamos. Los UART comunes hoy en día son todos compatibles con el [UART 16550], así que utilizaremos ese modelo para nuestro framework de pruebas.
[UARTs]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
[muchos modelos de UART]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#Models
[UART 16550]: https://en.wikipedia.org/wiki/16550_UART
Usaremos la crate [`uart_16550`] para inicializar el UART y enviar datos a través del puerto serial. Para añadirlo como dependencia, actualizamos nuestro `Cargo.toml` y `main.rs`:
[`uart_16550`]: https://docs.rs/uart_16550
```toml
# en Cargo.toml
[dependencies]
uart_16550 = "0.2.0"
```
La crate `uart_16550` contiene una estructura `SerialPort` que representa los registros del UART, pero aún necesitamos construir una instancia de ella nosotros mismos. Para eso, creamos un nuevo módulo `serial` con el siguiente contenido:
```rust
// en src/main.rs
mod serial;
```
```rust
// en src/serial.rs
use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;
lazy_static! {
pub static ref SERIAL1: Mutex = {
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
serial_port.init();
Mutex::new(serial_port)
};
}
```
Al igual que con el [buffer de texto VGA][vga lazy-static], usamos `lazy_static` y un spinlock para crear una instancia `static` de escritor. Usando `lazy_static` podemos asegurarnos de que el método `init` se llame exactamente una vez en su primer uso.
Al igual que el dispositivo `isa-debug-exit`, el UART se programa usando E/S de puerto. Dado que el UART es más complejo, utiliza varios puertos de E/S para programar diferentes registros del dispositivo. La función insegura `SerialPort::new` espera la dirección del primer puerto de E/S del UART como argumento, desde la cual puede calcular las direcciones de todos los puertos necesarios. Estamos pasando la dirección de puerto `0x3F8`, que es el número de puerto estándar para la primera interfaz serial.
[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
Para hacer que el puerto serial sea fácilmente utilizable, añadimos los macros `serial_print!` y `serial_println!`:
```rust
// en src/serial.rs
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
use core::fmt::Write;
SERIAL1.lock().write_fmt(args).expect("Error al imprimir en serial");
}
/// Imprime en el host a través de la interfaz serial.
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => {
$crate::serial::_print(format_args!($($arg)*));
};
}
/// Imprime en el host a través de la interfaz serial, añadiendo una nueva línea.
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
concat!($fmt, "\n"), $($arg)*));
}
```
La implementación es muy similar a la implementación de nuestros macros `print` y `println`. Dado que el tipo `SerialPort` ya implementa el trait [`fmt::Write`], no necesitamos proporcionar nuestra propia implementación.
[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
Ahora podemos imprimir en la interfaz serial en lugar de en el buffer de texto VGA en nuestro código de prueba:
```rust
// en src/main.rs
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Ejecutando {} pruebas", tests.len());
[…]
}
#[test_case]
fn trivial_assertion() {
serial_print!("aserción trivial... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
Ten en cuenta que el macro `serial_println` vive directamente en el espacio de nombres raíz porque usamos el atributo `#[macro_export]`, por lo que importarlo a través de `use crate::serial::serial_println` no funcionará.
### Argumentos de QEMU
Para ver la salida serial de QEMU, necesitamos usar el argumento `-serial` para redirigir la salida a stdout:
```toml
# en Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio"
]
```
Cuando ejecutamos `cargo test` ahora, vemos la salida de las pruebas directamente en la consola:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Ejecutando 1 pruebas
aserción trivial... [ok]
```
Sin embargo, cuando una prueba falla, todavía vemos la salida dentro de QEMU porque nuestro manejador de pánicos todavía usa `println`. Para simular esto, podemos cambiar la afirmación en nuestra prueba de `trivial_assertion` a `assert_eq!(0, 1)`:

Vemos que el mensaje de pánico todavía se imprime en el buffer de VGA, mientras que la otra salida de prueba se imprime en el puerto serial. El mensaje de pánico es bastante útil, así que sería útil verlo también en la consola.
### Imprimir un Mensaje de Error en el Pánico
Para salir de QEMU con un mensaje de error en un pánico, podemos usar [compilación condicional] para usar un manejador de pánicos diferente en modo de prueba:
[compilación condicional]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html
```rust
// en src/main.rs
// nuestro manejador de pánico existente
#[cfg(not(test))] // nuevo atributo
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
// nuestro manejador de pánico en modo de prueba
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
serial_println!("[fallido]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
```
Para nuestro manejador de pánico en las pruebas, usamos `serial_println` en lugar de `println` y luego salimos de QEMU con un código de salida de error. Ten en cuenta que aún necesitamos un bucle infinito después de la llamada a `exit_qemu` porque el compilador no sabe que el dispositivo `isa-debug-exit` provoca una salida del programa.
Ahora QEMU también saldrá para pruebas fallidas e imprimirá un mensaje de error útil en la consola:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Ejecutando 1 pruebas
aserción trivial... [fallido]
Error: panicked at 'assertion failed: `(left == right)`
left: `0`,
right: `1`', src/main.rs:65:5
```
Dado que ahora vemos toda la salida de prueba en la consola, ya no necesitamos la ventana de QEMU que aparece por un corto período de tiempo. Así que podemos ocultarla completamente.
### Ocultando QEMU
Dado que reportamos todos los resultados de las pruebas utilizando el dispositivo `isa-debug-exit` y el puerto serial, ya no necesitamos la ventana de QEMU. Podemos ocultarla pasando el argumento `-display none` a QEMU:
```toml
# en Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio",
"-display", "none"
]
```
Ahora QEMU se ejecuta completamente en segundo plano y no se abre ninguna ventana. Esto no solo es menos molesto, sino que también permite que nuestro framework de pruebas se ejecute en entornos sin una interfaz gráfica, como servicios CI o conexiones [SSH].
[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
### Timeouts
Dado que `cargo test` espera hasta que el runner de pruebas salga, una prueba que nunca retorna puede bloquear el runner de pruebas para siempre. Eso es desafortunado, pero no es un gran problema en la práctica, ya que generalmente es fácil evitar bucles infinitos. En nuestro caso, sin embargo, pueden ocurrir bucles infinitos en varias situaciones:
- El cargador de arranque no logra cargar nuestro núcleo, lo que provoca que el sistema reinicie indefinidamente.
- El firmware BIOS/UEFI no logra cargar el cargador de arranque, lo que provoca el mismo reinicio infinito.
- La CPU entra en una instrucción `loop {}` al final de algunas de nuestras funciones, por ejemplo, porque el dispositivo de salida QEMU no funciona correctamente.
- El hardware provoca un reinicio del sistema, por ejemplo, cuando una excepción de CPU no es capturada (explicado en una publicación futura).
Dado que los bucles infinitos pueden ocurrir en tantas situaciones, la herramienta `bootimage` establece un tiempo de espera de 5 minutos para cada ejecutable de prueba de manera predeterminada. Si la prueba no termina dentro de este tiempo, se marca como fallida y se imprime un error de "Tiempo de espera". Esta función asegura que las pruebas que están atrapadas en un bucle infinito no bloqueen `cargo test` para siempre.
Puedes intentarlo tú mismo añadiendo una instrucción `loop {}` en la prueba `trivial_assertion`. Cuando ejecutes `cargo test`, verás que la prueba se marca como expirado después de 5 minutos. La duración del tiempo de espera es [configurable][bootimage config] a través de una clave `test-timeout` en el Cargo.toml:
[bootimage config]: https://github.com/rust-osdev/bootimage#configuration
```toml
# en Cargo.toml
[package.metadata.bootimage]
test-timeout = 300 # (en segundos)
```
Si no quieres esperar 5 minutos para que la prueba `trivial_assertion` expire, puedes reducir temporalmente el valor anterior.
### Insertar Impresión Automáticamente
Nuestra prueba `trivial_assertion` actualmente necesita imprimir su propia información de estado usando `serial_print!`/`serial_println!`:
```rust
#[test_case]
fn trivial_assertion() {
serial_print!("aserción trivial... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
Añadir manualmente estas declaraciones de impresión para cada prueba que escribimos es engorroso, así que actualicemos nuestro `test_runner` para imprimir estos mensajes automáticamente. Para hacer eso, necesitamos crear un nuevo trait `Testable`:
```rust
// en src/main.rs
pub trait Testable {
fn run(&self) -> ();
}
```
El truco ahora es implementar este trait para todos los tipos `T` que implementan el trait [`Fn()`]:
[`Fn()` trait]: https://doc.rust-lang.org/stable/core/ops/trait.Fn.html
```rust
// en src/main.rs
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
```
Implementamos la función `run` imprimiendo primero el nombre de la función utilizando la función [`any::type_name`] . Esta función se implementa directamente en el compilador y devuelve una descripción de cadena de cada tipo. Para las funciones, el tipo es su nombre, así que esto es exactamente lo que queremos en este caso. El carácter `\t` es el [carácter de tabulación], que añade algo de alineación a los mensajes `[ok]`.
[`any::type_name`]: https://doc.rust-lang.org/stable/core/any/fn.type_name.html
[carácter de tabulación]: https://en.wikipedia.org/wiki/Tab_character
Después de imprimir el nombre de la función, invocamos la función de prueba a través de `self()`. Esto solo funciona porque requerimos que `self` implemente el trait `Fn()`. Después de que la función de prueba retorna, imprimimos `[ok]` para indicar que la función no provocó un pánico.
El último paso es actualizar nuestro `test_runner` para usar el nuevo trait `Testable`:
```rust
// en src/main.rs
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Testable]) { // nuevo
serial_println!("Ejecutando {} pruebas", tests.len());
for test in tests {
test.run(); // nuevo
}
exit_qemu(QemuExitCode::Success);
}
```
Los únicos dos cambios son el tipo del argumento `tests` de `&[&dyn Fn()]` a `&[&dyn Testable]` y el hecho de que ahora llamamos a `test.run()` en lugar de `test()`.
Ahora podemos eliminar las declaraciones de impresión de nuestra prueba `trivial_assertion` ya que ahora se imprimen automáticamente:
```rust
// en src/main.rs
#[test_case]
fn trivial_assertion() {
assert_eq!(1, 1);
}
```
La salida de `cargo test` ahora se ve así:
```
Ejecutando 1 pruebas
blog_os::trivial_assertion... [ok]
```
El nombre de la función ahora incluye la ruta completa a la función, que es útil cuando las funciones de prueba en diferentes módulos tienen el mismo nombre. De lo contrario, la salida se ve igual que antes, pero ya no necesitamos agregar declaraciones de impresión a nuestras pruebas manualmente.
## Pruebas del Buffer VGA
Ahora que tenemos un marco de pruebas funcional, podemos crear algunas pruebas para nuestra implementación del buffer VGA. Primero, creamos una prueba muy simple para verificar que `println` funciona sin provocar un pánico:
```rust
// en src/vga_buffer.rs
#[test_case]
fn test_println_simple() {
println!("salida de test_println_simple");
}
```
La prueba simplemente imprime algo en el buffer VGA. Si termina sin provocar un pánico, significa que la invocación de `println` tampoco provocó un pánico.
Para asegurarnos de que no se produzca un pánico incluso si se imprimen muchas líneas y las líneas se desplazan de la pantalla, podemos crear otra prueba:
```rust
// en src/vga_buffer.rs
#[test_case]
fn test_println_many() {
for _ in 0..200 {
println!("salida de test_println_many");
}
}
```
También podemos crear una función de prueba para verificar que las líneas impresas realmente aparecen en la pantalla:
```rust
// en src/vga_buffer.rs
#[test_case]
fn test_println_output() {
let s = "Alguna cadena de prueba que cabe en una única línea";
println!("{}", s);
for (i, c) in s.chars().enumerate() {
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
}
```
La función define una cadena de prueba, la imprime usando `println`, y luego itera sobre los caracteres de pantalla del estático `WRITER`, que representa el buffer de texto VGA. Dado que `println` imprime en la última línea de pantalla y luego inmediatamente agrega una nueva línea, la cadena debería aparecer en la línea `BUFFER_HEIGHT - 2`.
Usando [`enumerate`], contamos el número de iteraciones en la variable `i`, que luego utilizamos para cargar el carácter de pantalla correspondiente a `c`. Al comparar el `ascii_character` del carácter de pantalla con `c`, nos aseguramos de que cada carácter de la cadena realmente aparece en el buffer de texto VGA.
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
Como puedes imaginar, podríamos crear muchas más funciones de prueba. Por ejemplo, una función que teste que no se produzca un pánico al imprimir líneas muy largas y que se envuelvan correctamente, o una función que pruebe que se manejan correctamente nuevas líneas, caracteres no imprimibles y caracteres no unicode.
Para el resto de esta publicación, sin embargo, explicaremos cómo crear _pruebas de integración_ para probar la interacción de diferentes componentes juntos.
## Pruebas de Integración
La convención para las [pruebas de integración] en Rust es ponerlas en un directorio `tests` en la raíz del proyecto (es decir, junto al directorio `src`). Tanto el marco de prueba predeterminado como los marcos de prueba personalizados recogerán y ejecutarán automáticamente todas las pruebas en ese directorio.
[pruebas de integración]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
Todas las pruebas de integración son sus propios ejecutables y completamente separadas de nuestro `main.rs`. Esto significa que cada prueba necesita definir su propia función de punto de entrada. Creemos una prueba de integración de ejemplo llamada `basic_boot` para ver cómo funciona en detalle:
```rust
// en tests/basic_boot.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
#[no_mangle] // no modificar el nombre de esta función
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
fn test_runner(tests: &[&dyn Fn()]) {
unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
loop {}
}
```
Dado que las pruebas de integración son ejecutables separados, necesitamos proporcionar todos los atributos de crate nuevamente (`no_std`, `no_main`, `test_runner`, etc.). También necesitamos crear una nueva función de punto de entrada `_start`, que llama a la función de punto de entrada de prueba `test_main`. No necesitamos ningún atributo `cfg(test)` porque los ejecutables de prueba de integración nunca se construyen en modo no prueba.
Usamos el macro [`unimplemented`] que siempre provoca un pánico como un marcador de posición para la función `test_runner` y simplemente hacemos `loop` en el manejador de pánico por ahora. Idealmente, queremos implementar estas funciones exactamente como lo hicimos en nuestro `main.rs` utilizando el macro `serial_println` y la función `exit_qemu`. El problema es que no tenemos acceso a estas funciones ya que las pruebas se construyen completamente por separado de nuestro ejecutable `main.rs`.
[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html
Si ejecutas `cargo test` en esta etapa, te quedarás atrapado en un bucle infinito porque el manejador de pánicos se queda en un bucle indefinidamente. Necesitas usar el atajo de teclado `ctrl+c` para salir de QEMU.
### Crear una Biblioteca
Para que las funciones requeridas estén disponibles para nuestra prueba de integración, necesitamos separar una biblioteca de nuestro `main.rs`, que pueda ser incluida por otros crates y ejecutables de pruebas de integración. Para hacer esto, creamos un nuevo archivo `src/lib.rs`:
```rust
// src/lib.rs
#![no_std]
```
Al igual que `main.rs`, `lib.rs` es un archivo especial que es automáticamente reconocido por cargo. La biblioteca es una unidad de compilación separada, por lo que necesitamos especificar el atributo `#![no_std]` nuevamente.
Para que nuestra biblioteca funcione con `cargo test`, también necesitamos mover las funciones y atributos de prueba de `main.rs` a `lib.rs`:
```rust
// en src/lib.rs
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub trait Testable {
fn run(&self) -> ();
}
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Ejecutando {} pruebas", tests.len());
for test in tests {
test.run();
}
exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {
serial_println!("[fallido]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
/// Punto de entrada para `cargo test`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
test_panic_handler(info)
}
```
Para hacer que nuestra `test_runner` esté disponible para los ejecutables y pruebas de integración, la hacemos pública y no le aplicamos el atributo `cfg(test)`. También extraemos la implementación de nuestro manejador de pánicos en una función pública `test_panic_handler`, para que esté disponible para los ejecutables también.
Dado que nuestra `lib.rs` se prueba independientemente de `main.rs`, necesitamos añadir una función de entrada `_start` y un manejador de pánico cuando la biblioteca se compila en modo de prueba. Usando el atributo [`cfg_attr`] de crate, habilitamos condicionalmente el atributo `no_main` en este caso.
[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
También movemos el enum `QemuExitCode` y la función `exit_qemu` y los hacemos públicos:
```rust
// en src/lib.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
Ahora los ejecutables y las pruebas de integración pueden importar estas funciones de la biblioteca y no necesitan definir sus propias implementaciones. Para también hacer que `println` y `serial_println` estén disponibles, movemos también las declaraciones de módulo:
```rust
// en src/lib.rs
pub mod serial;
pub mod vga_buffer;
```
Hacemos que los módulos sean públicos para que sean utilizables fuera de nuestra biblioteca. Esto también es necesario para hacer que nuestros macros `println` y `serial_println` sean utilizables ya que utilizan las funciones `_print` de los módulos.
Ahora podemos actualizar nuestro `main.rs` para usar la biblioteca:
```rust
// en src/main.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
use blog_os::println;
#[no_mangle]
pub extern "C" fn _start() -> ! {
println!("¡Hola Mundo{}!", "!");
#[cfg(test)]
test_main();
loop {}
}
/// Esta función se llama en caso de pánico.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
La biblioteca es utilizable como si fuera una crate externa normal. Se llama `blog_os`, como nuestra crate. El código anterior utiliza la función `test_runner` de `blog_os` en el atributo `test_runner` y la función `test_panic_handler` de `blog_os` en nuestro manejador de pánicos `cfg(test)`. También importa el macro `println` para hacerlo disponible en nuestras funciones `_start` y `panic`.
En este punto, `cargo run` y `cargo test` deberían funcionar nuevamente. Por supuesto, `cargo test` todavía se queda atrapado en un bucle infinito (puedes salir con `ctrl+c`). Vamos a solucionar esto usando las funciones de biblioteca requeridas en nuestra prueba de integración.
### Completar la Prueba de Integración
Al igual que nuestro `src/main.rs`, nuestro ejecutable `tests/basic_boot.rs` puede importar tipos de nuestra nueva biblioteca. Esto nos permite importar los componentes faltantes para completar nuestra prueba:
```rust
// en tests/basic_boot.rs
#![test_runner(blog_os::test_runner)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
En lugar de reimplementar el runner de prueba, usamos la función `test_runner` de nuestra biblioteca cambiando el atributo `#![test_runner(crate::test_runner)]` a `#![test_runner(blog_os::test_runner)]`. Ya no necesitamos la función de sanidad `test_runner` de referencia en `basic_boot.rs`, así que podemos eliminarla. Para nuestro manejador de pánicos, llamamos a la función `blog_os::test_panic_handler` como hicimos en nuestro archivo `main.rs`.
Ahora `cargo test` sale normalmente nuevamente. Cuando lo ejecutas, verás que construye y ejecuta las pruebas para `lib.rs`, `main.rs` y `basic_boot.rs` por separado después de cada uno. Para `main.rs` y las pruebas de integración `basic_boot`, informa "Ejecutando 0 pruebas" ya que estos archivos no tienen funciones anotadas con `#[test_case]`.
Ahora podemos añadir pruebas a nuestro `basic_boot.rs`. Por ejemplo, podemos probar que `println` funciona sin provocar un pánico, como hicimos en las pruebas del buffer VGA:
```rust
// en tests/basic_boot.rs
use blog_os::println;
#[test_case]
fn test_println() {
println!("salida de test_println");
}
```
Cuando ejecutamos `cargo test` ahora, vemos que encuentra y ejecuta la función de prueba.
La prueba podría parecer un poco inútil ahora ya que es casi idéntica a una de las pruebas del buffer VGA. Sin embargo, en el futuro, las funciones `_start` de nuestros `main.rs` y `lib.rs` podrían crecer y llamar a varias rutinas de inicialización antes de ejecutar la función `test_main`, de modo que las dos pruebas se ejecuten en entornos muy diferentes.
Al probar `println` en un entorno de `basic_boot` sin llamar a ninguna rutina de inicialización en `_start`, podemos asegurarnos de que `println` funcione justo después de arrancar. Esto es importante porque nos basamos en ello, por ejemplo, para imprimir mensajes de pánico.
### Pruebas Futuras
El poder de las pruebas de integración es que se tratan como ejecutables completamente separados. Esto les da el control total sobre el entorno, lo que hace posible probar que el código interactúa correctamente con la CPU o dispositivos de hardware.
Nuestra prueba `basic_boot` es un ejemplo muy simple de una prueba de integración. En el futuro, nuestro núcleo se volverá mucho más funcional e interactuará con el hardware de varias maneras. Al añadir pruebas de integración, podemos asegurarnos de que estas interacciones funcionen (y sigan funcionando) como se espera. Algunas ideas para posibles pruebas futuras son:
- **Excepciones de CPU**: Cuando el código realiza operaciones inválidas (por ejemplo, division por cero), la CPU lanza una excepción. El núcleo puede registrar funciones de manejo para tales excepciones. Una prueba de integración podría verificar que se llame al controlador de excepciones correcto cuando ocurre una excepción de CPU o que la ejecución continúe correctamente después de una excepción recuperable.
- **Tablas de Páginas**: Las tablas de páginas definen qué regiones de memoria son válidas y accesibles. Al modificar las tablas de páginas, es posible asignar nuevas regiones de memoria, por ejemplo, al lanzar programas. Una prueba de integración podría modificar las tablas de páginas en la función `_start` y verificar que las modificaciones tengan los efectos deseados en las funciones `#[test_case]`.
- **Programas en Espacio de Usuario**: Los programas en espacio de usuario son programas con acceso limitado a los recursos del sistema. Por ejemplo, no tienen acceso a las estructuras de datos del núcleo ni a la memoria de otros programas. Una prueba de integración podría lanzar programas en espacio de usuario que realicen operaciones prohibidas y verificar que el núcleo las prevenga todas.
Como puedes imaginar, son posibles muchas más pruebas. Al añadir tales pruebas, podemos asegurarnos de no romperlas accidentalmente al añadir nuevas características a nuestro núcleo o refactorizar nuestro código. Esto es especialmente importante cuando nuestro núcleo se vuelve más grande y complejo.
### Pruebas que Deberían Fallar
El marco de pruebas de la biblioteca estándar admite un atributo [`#[should_panic]`](https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panics) que permite construir funciones de prueba que deberían fallar. Esto es útil, por ejemplo, para verificar que una función falle cuando se pasa un argumento inválido. Desafortunadamente, este atributo no está soportado en crates `#[no_std]` ya que requiere soporte de la biblioteca estándar.
Si bien no podemos usar el atributo `#[should_panic]` en nuestro núcleo, podemos obtener un comportamiento similar creando una prueba de integración que salga con un código de error de éxito desde el manejador de pánicos. Comencemos a crear tal prueba con el nombre `should_panic`:
```rust
// en tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{QemuExitCode, exit_qemu, serial_println};
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
Esta prueba aún está incompleta ya que no define una función `_start` ni ninguno de los atributos del marco de prueba personalizados que faltan. Añadamos las partes que faltan:
```rust
// en tests/should_panic.rs
#![feature(custom_test_frameworks)]
#![test_runner(test_runner)]
#![reexport_test_harness_main = "test_main"]
#[no_mangle]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
pub fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Ejecutando {} pruebas", tests.len());
for test in tests {
test();
serial_println!("[la prueba no falló]");
exit_qemu(QemuExitCode::Failed);
}
exit_qemu(QemuExitCode::Success);
}
```
En lugar de reutilizar el `test_runner` de `lib.rs`, la prueba define su propia función `test_runner` que sale con un código de error de fallo cuando una prueba retorna sin provocar un pánico (queremos que nuestras pruebas fallen). Si no se define ninguna función de prueba, el runner sale con un código de éxito. Dado que el runner siempre sale después de ejecutar una sola prueba, no tiene sentido definir más de una función `#[test_case]`.
Ahora podemos crear una prueba que debería fallar:
```rust
// en tests/should_panic.rs
use blog_os::serial_print;
#[test_case]
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
```
La prueba utiliza `assert_eq` para afirmar que `0` y `1` son iguales. Por supuesto, esto falla, por lo que nuestra prueba provoca un pánico como se deseaba. Ten en cuenta que necesitamos imprimir manualmente el nombre de la función usando `serial_print!` aquí porque no usamos el trait `Testable`.
Cuando ejecutamos la prueba a través de `cargo test --test should_panic` vemos que es exitosa porque la prueba se produjo como se esperaba. Cuando comentamos la afirmación y ejecutamos la prueba nuevamente, vemos que, de hecho, falla con el mensaje _"la prueba no falló"_.
Una gran desventaja de este enfoque es que solo funciona para una única función de prueba. Con múltiples funciones `#[test_case]`, solo se ejecuta la primera función porque la ejecución no puede continuar después de que se ha llamado al manejador de pánicos. Actualmente no sé una buena manera de resolver este problema, ¡así que házmelo saber si tienes una idea!
### Pruebas Sin Harness
Para las pruebas de integración que solo tienen una única función de prueba (como nuestra prueba `should_panic`), el runner de prueba no es realmente necesario. Para casos como este, podemos deshabilitar completamente el runner de pruebas y ejecutar nuestra prueba directamente en la función `_start`.
La clave para esto es deshabilitar la bandera `harness` para la prueba en el `Cargo.toml`, que define si se usa un runner de prueba para una prueba de integración. Cuando está configurada como `false`, se desactivan tanto el marco de prueba predeterminado como la característica de marcos de prueba personalizados, por lo que la prueba se trata como un ejecutable normal.
Deshabilitemos la bandera `harness` para nuestra prueba `should_panic`:
```toml
# en Cargo.toml
[[test]]
name = "should_panic"
harness = false
```
Ahora simplificamos enormemente nuestra prueba `should_panic` al eliminar el código relacionado con el `test_runner`. El resultado se ve así:
```rust
// en tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
#[no_mangle]
pub extern "C" fn _start() -> ! {
should_fail();
serial_println!("[la prueba no falló]");
exit_qemu(QemuExitCode::Failed);
loop{}
}
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
Ahora llamamos a la función `should_fail` directamente desde nuestra función `_start` y salimos con un código de error de fallo si retorna. Cuando ejecutamos `cargo test --test should_panic` ahora, vemos que la prueba se comporta exactamente como antes.
Además de crear pruebas `should_panic`, deshabilitar el atributo `harness` también puede ser útil para pruebas de integración complejas, por ejemplo, cuando las funciones de prueba individuales tienen efectos secundarios y necesitan ejecutarse en un orden específico.
## Resumen
Las pruebas son una técnica muy útil para asegurarse de que ciertos componentes tengan el comportamiento deseado. Aunque no pueden mostrar la ausencia de errores, siguen siendo una herramienta útil para encontrarlos y especialmente para evitar regresiones.
Esta publicación explicó cómo configurar un marco de pruebas para nuestro núcleo Rust. Utilizamos la característica de marcos de prueba personalizados de Rust para implementar el soporte para un simple atributo `#[test_case]` en nuestro entorno de metal desnudo. Usando el dispositivo `isa-debug-exit` de QEMU, nuestro runner de pruebas puede salir de QEMU después de ejecutar las pruebas y reportar el estado de las pruebas. Para imprimir mensajes de error en la consola en lugar de en el buffer de VGA, creamos un controlador básico para el puerto serial.
Después de crear algunas pruebas para nuestro macro `println`, exploramos las pruebas de integración en la segunda mitad de la publicación. Aprendimos que viven en el directorio `tests` y se tratan como ejecutables completamente separados. Para dar acceso a la función `exit_qemu` y al macro `serial_println`, movimos la mayor parte de nuestro código a una biblioteca que pueden importar todos los ejecutables y pruebas de integración. Dado que las pruebas de integración se ejecutan en su propio entorno separado, permiten probar interacciones con el hardware o crear pruebas que deberían provocar pánicos.
Ahora tenemos un marco de pruebas que se ejecuta en un entorno realista dentro de QEMU. Al crear más pruebas en publicaciones futuras, podemos mantener nuestro núcleo manejable a medida que se vuelva más complejo.
## ¿Qué sigue?
En la próxima publicación, exploraremos _excepciones de CPU_. Estas excepciones son lanzadas por la CPU cuando ocurre algo ilegal, como una división por cero o un acceso a una página de memoria no mapeada (una llamada "falta de página"). Poder capturar y examinar estas excepciones es muy importante para depurar futuros errores. El manejo de excepciones también es muy similar al manejo de interrupciones de hardware, que es necesario para el soporte del teclado.
================================================
FILE: blog/content/edition-2/posts/04-testing/index.fa.md
================================================
+++
title = "تست کردن"
weight = 4
path = "fa/testing"
date = 2019-04-27
[extra]
# Please update this when updating the translation
translation_based_on_commit = "d007af4811469b974f7abb988dd9c9d1373b55f0"
# GitHub usernames of the people that translated this post
translators = ["hamidrezakp", "MHBahrampour"]
rtl = true
+++
این پست به بررسی تستهای واحد (ترجمه: unit) و یکپارچه (ترجمه: integration) در فایلهای اجرایی `no_std` میپردازد. ما از پشتیبانی Rust برای فریمورک تستهای سفارشی استفاده میکنیم تا توابع تست را درون کرنلمان اجرا کنیم. برای گزارش کردن نتایج خارج از QEMU، از ویژگیهای مختلف QEMU و ابزار `bootimage` استفاده میکنیم.
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. شما همچنین میتوانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را میتوانید در بِرَنچ [`post-04`][post branch] پیدا کنید.
[گیتهاب]: https://github.com/phil-opp/blog_os
[در زیر]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-04
## نیازمندیها
این پست جایگزین (حالا منسوخ شده) پستهای [_Unit Testing_] و [_Integration Tests_] میشود. فرض بر این است که شما پست [_یک کرنل مینیمال با Rust_] را پس از 27-09-2019 دنبال کردهاید. اساساً نیاز است که شما یک فایل `.cargo/config.toml` داشته باشید که [یک هدف پیشفرض مشخص میکند] و [یک اجرا کننده قابل اجرا تعریف میکند].
[_Unit Testing_]: @/edition-2/posts/deprecated/04-unit-testing/index.md
[_Integration Tests_]: @/edition-2/posts/deprecated/05-integration-tests/index.md
[_یک کرنل مینیمال با Rust_]: @/edition-2/posts/02-minimal-rust-kernel/index.md
[یک هدف پیشفرض مشخص میکند]: @/edition-2/posts/02-minimal-rust-kernel/index.md#set-a-default-target
[یک اجرا کننده قابل اجرا تعریف میکند]: @/edition-2/posts/02-minimal-rust-kernel/index.md#using-cargo-run
## تست کردن در Rust
زبان Rust یک [فریمورک تست توکار] دارد که قادر به اجرای تستهای واحد بدون نیاز به تنظیم هر چیزی است. فقط کافی است تابعی ایجاد کنید که برخی نتایج را از طریق اَسرشنها (کلمه: assertions) بررسی کند و صفت `#[test]` را به هدر تابع (ترجمه: function header) اضافه کنید. سپس `cargo test` به طور خودکار تمام تابعهای تست کریت شما را پیدا و اجرا میکند.
[فریمورک تست توکار]: https://doc.rust-lang.org/book/second-edition/ch11-00-testing.html
متأسفانه برای برنامههای `no_std` مانند هسته ما کمی پیچیدهتر است. مسئله این است که فریمورک تست Rust به طور ضمنی از کتابخانه [`test`] داخلی استفاده میکند که به کتابخانه استاندارد وابسته است. این بدان معناست که ما نمیتوانیم از فریمورک تست پیشفرض برای هسته `#[no_std]` خود استفاده کنیم.
[`test`]: https://doc.rust-lang.org/test/index.html
وقتی میخواهیم `cargo test` را در پروژه خود اجرا کنیم، چنین چیزی میبینیم:
```
> cargo test
Compiling blog_os v0.1.0 (/…/blog_os)
error[E0463]: can't find crate for `test`
```
از آنجایی که کریت `test` به کتابخانه استاندارد وابسته است، برای هدف bare metal ما در دسترس نیست. در حالی که استفاده از کریت `test` در یک `#[no_std]` [امکان پذیر است][utest]، اما بسیار ناپایدار بوده و به برخی هکها مانند تعریف مجدد ماکرو `panic` نیاز دارد.
[utest]: https://github.com/japaric/utest
### فریمورک تست سفارشی
خوشبختانه، Rust از جایگزین کردن فریمورک تست پیشفرض از طریق ویژگی [`custom_test_frameworks`] ناپایدار پشتیبانی میکند. این ویژگی به کتابخانه خارجی احتیاج ندارد و بنابراین در محیطهای `#[no_std]` نیز کار میکند. این کار با جمع آوری تمام توابع دارای صفت `#[test_case]` و سپس فراخوانی یک تابع اجرا کننده مشخص شده توسط کاربر و با لیست تستها به عنوان آرگومان کار میکند. بنابراین حداکثر کنترل فرآیند تست را به ما میدهد.
[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html
نقطه ضعف آن در مقایسه با فریمورک تست پیشفرض این است که بسیاری از ویژگیهای پیشرفته مانند [تستهای `should_panic`] در دسترس نیست. در عوض، تهیه این ویژگیها در صورت نیاز به پیادهسازی ما بستگی دارد. این برای ما ایده آل است، زیرا ما یک محیط اجرای بسیار ویژه داریم که پیاده سازی پیشفرض چنین ویژگیهای پیشرفتهای احتمالاً کارساز نخواهد بود. به عنوان مثال، صفت `#[should_panic]` متکی به stack unwinding برای گرفتن پنیکها (کلمه: panics) است، که ما آن را برای هسته خود غیرفعال کردیم.
[تستهای `should_panic`]: https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic
برای اجرای یک فریمورک تست سفارشی برای هسته خود، موارد زیر را به `main.rs` اضافه میکنیم:
```rust
// in src/main.rs
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
}
```
اجرا کننده ما فقط یک پیام کوتاه اشکال زدایی را چاپ میکند و سپس هر تابع تست درون لیست را فراخوانی میکند. نوع آرگومان `&[&dyn Fn()]` یک [_slice_] از [_trait object_] است که آن هم ارجاعی از تِرِیت (کلمه: trait) [_Fn()_] میباشد. در اصل لیستی از ارجاع به انواع است که میتوان آنها را مانند یک تابع صدا زد. از آنجایی که این تابع برای اجراهایی که تست نباشند بی فایده است، از ویژگی `#[cfg(test)]` استفاده میکنیم تا آن را فقط برای تست کردن در اضافه کنیم.
[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html
[_trait object_]: https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html
[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html
حال وقتی که `cargo test` را اجرا میکنیم، میبینیم که الان موفقیت آمیز است (اگر اینطور نیست یادداشت زیر را بخوانید). اگرچه، همچنان “Hello World” را به جای پیام `test_runner` میبینیم. دلیلش این است که تابع `_start` هنوز بعنوان نقطه شروع استفاده میشود. ویژگی فریمورک تست سفارشی، یک تابع `main` ایجاد میکند که `test_runner` را صدا میزند، اما این تابع نادیده گرفته میشود چرا که ما از ویژگی `#[no_main]` استفاده میکنیم و نقطه شروع خودمان را ایجاد کردیم.
**یادداشت:** درحال حاضر یک باگ در کارگو وجود دارد که در برخی موارد وقتی از `cargo test` استفاده میکنیم ما را به سمت خطای “duplicate lang item” میبرد. زمانی رخ میدهد که شما `panic = "abort"` را برای یک پروفایل در `Cargo.toml` تنظیم کردهاید. سعی کنید آن را حذف کنید، سپس `cargo test` باید به درستی کار کند. برای اطلاعات بیشتر [ایشوی کارگو](https://github.com/rust-lang/cargo/issues/7359) را ببینید.
برای حل کردن این مشکل، ما ابتدا نیاز داریم که نام تابع تولید شده را از طریق صفت `reexport_test_harness_main` به چیزی غیر از `main` تغییر دهیم. سپس میتوانیم تابع تغییر نام داده شده را از تابع `_start` صدا بزنیم:
```rust
// in src/main.rs
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
```
ما نام فریمورک تست تابع شروع را `test_main` گذاشتیم و آن را درون `_start` صدا زدیم. از [conditional compilation] برای اضافه کردن فراخوانی `test_main` فقط در زمینههای تست استفاده میکنیم زیرا تابع روی یک اجرای عادی تولید نشده است.
زمانی که `cargo test` را اجرا میکنیم، میبینیم که پیام "Running 0 tests" از `test_runner` روی صفحه نمایش داده میشود. حال ما آمادهایم تا اولین تابع تست را بسازیم:
```rust
// in src/main.rs
#[test_case]
fn trivial_assertion() {
print!("trivial assertion... ");
assert_eq!(1, 1);
println!("[ok]");
}
```
حال وقتی `cargo test` را اجرا میکنیم، خروجی زیر را میبینیم:
![QEMU printing "Hello World!", "Running 1 tests", and "trivial assertion... [ok]"](qemu-test-runner-output.png)
حالا بخش `tests` ارسال شده به تابع `test_runner` شامل یک ارجاع به تابع `trivial_assertion` است. از خروجی `trivial assertion... [ok]` روی صفحه میفهمیم که تست مورد نظر فراخوانی شده و موفقیت آمیز بوده است.
پس از اجرای تستها، `test_runner` به تابع `test_main` برمیگردد، که به نوبه خود به تابع `_start` برمیگردد. در انتهای `_start`، یک حلقه بیپایان ایجاد میکنیم زیرا تابع شروع اجازه برگردادن چیزی را ندارد (یعنی بدون خروجی است). این یک مشکل است، زیرا میخواهیم `cargo test` پس از اجرای تمام تستها به کار خود پایان دهد.
## خروج از QEMU
در حال حاضر ما یک حلقه بیپایان در انتهای تابع `"_start"` داریم و باید QEMU را به صورت دستی در هر مرحله از `cargo test` ببندیم. این جای تأسف دارد زیرا ما همچنین میخواهیم `cargo test` را در اسکریپتها بدون تعامل کاربر اجرا کنیم. یک راه حل خوب میتواند اجرای یک روش مناسب برای خاموش کردن سیستم عامل باشد. متأسفانه این کار نسبتاً پیچیده است، زیرا نیاز به پشتیبانی از استاندارد [APM] یا [ACPI] مدیریت توان دارد.
[APM]: https://wiki.osdev.org/APM
[ACPI]: https://wiki.osdev.org/ACPI
خوشبختانه، یک دریچه فرار وجود دارد: QEMU از یک دستگاه خاص `isa-debug-exit` پشتیبانی میکند، که راهی آسان برای خروج از سیستم QEMU از سیستم مهمان فراهم میکند. برای فعال کردن آن، باید یک آرگومان `-device` را به QEMU منتقل کنیم. ما میتوانیم این کار را با اضافه کردن کلید پیکربندی `pack.metadata.bootimage.test-args` در` Cargo.toml` انجام دهیم:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
```
`bootimage runner` برای کلیه تستهای اجرایی ` test-args` را به دستور پیش فرض QEMU اضافه می کند. برای یک `cargo run` عادی، آرگومانها نادیده گرفته میشوند.
همراه با نام دستگاه (`isa-debug-exit`)، دو پارامتر `iobase` و `iosize` را عبور میدهیم که _پورت I/O_ را مشخص میکند و هسته از طریق آن میتواند به دستگاه دسترسی داشته باشد.
### پورتهای I/O
برای برقراری ارتباط بین پردازنده و سخت افزار جانبی در x86، دو رویکرد مختلف وجود دارد،**memory-mapped I/O** و **port-mapped I/O**. ما قبلاً برای دسترسی به [بافر متن VGA] از طریق آدرس حافظه `0xb8000` از memory-mapped I/O استفاده کردهایم. این آدرس به RAM مپ (ترسیم) نشده است، بلکه به برخی از حافظههای دستگاه VGA مپ شده است.
[بافر متن VGA]: @/edition-2/posts/03-vga-text-buffer/index.md
در مقابل، port-mapped I/O از یک گذرگاه I/O جداگانه برای ارتباط استفاده میکند. هر قسمت جانبی متصل دارای یک یا چند شماره پورت است. برای برقراری ارتباط با چنین پورت I/O، دستورالعملهای CPU خاصی وجود دارد که `in` و `out` نامیده میشوند، که یک عدد پورت و یک بایت داده را میگیرند (همچنین این دستورات تغییراتی دارند که اجازه می دهد یک `u16` یا `u32` ارسال کنید).
دستگاههای `isa-debug-exit` از port-mapped I/O استفاده میکنند. پارامتر `iobase` مشخص میکند که دستگاه باید در کدام آدرس پورت قرار بگیرد (`0xf4` یک پورت [معمولاً استفاده نشده][list of x86 I/O ports] در گذرگاه IO x86 است) و `iosize` اندازه پورت را مشخص میکند (`0x04` یعنی چهار بایت).
[list of x86 I/O ports]: https://wiki.osdev.org/I/O_Ports#The_list
### استفاده از دستگاه خروج
عملکرد دستگاه `isa-debug-exit` بسیار ساده است. وقتی یک مقدار به پورت I/O مشخص شده توسط `iobase` نوشته میشود، باعث می شود QEMU با [exit status] خارج شود `(value << 1) | 1`. بنابراین هنگامی که ما `0` را در پورت مینویسیم، QEMU با وضعیت خروج `(0 << 1) | 1 = 1` خارج میشود و وقتی که ما `1` را در پورت مینویسیم با وضعیت خروج `(1 << 1) | 1 = 3` از آن خارج می شود.
[exit status]: https://en.wikipedia.org/wiki/Exit_status
به جای فراخوانی دستی دستورالعمل های اسمبلی `in` و `out`، ما از انتزاعات ارائه شده توسط کریت [`x86_64`] استفاده میکنیم. برای افزودن یک وابستگی به آن کریت، آن را به بخش `dependencies` در `Cargo.toml` اضافه میکنیم:
[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/
```toml
# in Cargo.toml
[dependencies]
x86_64 = "0.14.2"
```
اکنون میتوانیم از نوع [`Port`] ارائه شده توسط کریت برای ایجاد عملکرد `exit_qemu` استفاده کنیم:
[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html
```rust
// in src/main.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
این تابع یک [`Port`] جدید در `0xf4` ایجاد میکند، که `iobase` دستگاه `isa-debug-exit` است. سپس کد خروجی عبور داده شده را در پورت مینویسد. ما از `u32` استفاده میکنیم زیرا `iosize` دستگاه `isa-debug-exit` را به عنوان 4 بایت مشخص کردیم. هر دو عملیات ایمن نیستند، زیرا نوشتن در یک پورت I/O میتواند منجر به رفتار خودسرانه شود.
برای تعیین وضعیت خروج، یک اینام (کلمه: enum) `QemuExitCode` ایجاد می کنیم. ایده این است که اگر همه تستها موفقیت آمیز بود، با کد خروج موفقیت (ترجمه: success exit code) خارج شود و در غیر این صورت با کد خروج شکست (ترجمه: failure exit code) خارج شود. enum به عنوان `#[repr(u32)]` علامت گذاری شده است تا هر نوع را با یک عدد صحیح `u32` نشان دهد. برای موفقیت از کد خروجی `0x10` و برای شکست از `0x11` استفاده میکنیم. کدهای خروجی واقعی چندان هم مهم نیستند، به شرطی که با کدهای خروجی پیش فرض QEMU مغایرت نداشته باشند. به عنوان مثال، استفاده از کد خروجی `0` برای موفقیت ایده خوبی نیست زیرا پس از تغییر شکل تبدیل به `(0 << 1) | 1 = 1` میشود، که کد خروجی پیش فرض است برای زمانی که QEMU نمیتواند اجرا شود. بنابراین ما نمیتوانیم خطای QEMU را از یک تست موفقیت آمیز تشخیص دهیم.
اکنون می توانیم `test_runner` خود را به روز کنیم تا پس از اتمام تستها از QEMU خارج شویم:
```rust
fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
/// new
exit_qemu(QemuExitCode::Success);
}
```
حال وقتی `cargo test` را اجرا میکنیم، میبینیم که QEMU پس از اجرای تستها بلافاصله بسته میشود. مشکل این است که `cargo test` تست را به عنوان شکست تفسیر میکند حتی اگر کد خروج `Success` را عبور دهیم:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be
Building bootloader
Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader)
Finished release [optimized + debuginfo] target(s) in 1.07s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4,
iosize=0x04`
error: test failed, to rerun pass '--bin blog_os'
```
مسئله این است که `cargo test` همه کدهای خطا به غیر از `0` را به عنوان شکست در نظر میگیرد.
### کد خروج موفقیت
برای کار در این مورد، `bootimage` یک کلید پیکربندی `test-success-exit-code` ارائه میدهد که یک کد خروجی مشخص را به کد خروجی `0` مپ میکند:
```toml
[package.metadata.bootimage]
test-args = […]
test-success-exit-code = 33 # (0x10 << 1) | 1
```
با استفاده از این پیکربندی، `bootimage` کد خروج موفقیت ما را به کد خروج 0 مپ میکند، به طوری که `cargo test` به درستی مورد موفقیت را تشخیص میدهد و تست را شکست خورده به حساب نمیآورد.
اجرا کننده تست ما اکنون به طور خودکار QEMU را میبندد و نتایج تست را به درستی گزارش میکند. ما همچنان میبینیم که پنجره QEMU برای مدت بسیار کوتاهی باز است، اما این مدت بسیار کوتاه برای خواندن نتایج کافی نیست. جالب میشود اگر بتوانیم نتایج تست را به جای QEMU در کنسول چاپ کنیم، بنابراین پس از خروج از QEMU هنوز میتوانیم آنها را ببینیم.
## چاپ کردن در کنسول
برای دیدن خروجی تست روی کنسول، باید دادهها را از هسته خود به نحوی به سیستم میزبان ارسال کنیم. روشهای مختلفی برای دستیابی به این هدف وجود دارد، به عنوان مثال با ارسال دادهها از طریق رابط شبکه TCP. با این حال، تنظیم پشته شبکه یک کار کاملا پیچیده است، بنابراین ما به جای آن راه حل سادهتری را انتخاب خواهیم کرد.
### پورت سریال
یک راه ساده برای ارسال دادهها استفاده از [پورت سریال] است، یک استاندارد رابط قدیمی که دیگر در رایانههای مدرن یافت نمیشود. پیادهسازی آن آسان است و QEMU میتواند بایتهای ارسالی از طریق سریال را به خروجی استاندارد میزبان یا یک فایل هدایت کند.
[پورت سریال]: https://en.wikipedia.org/wiki/Serial_port
تراشههای پیاده سازی یک رابط سریال [UART] نامیده میشوند. در x86 [مدلهای UART زیادی] وجود دارد، اما خوشبختانه تنها تفاوت آنها ویژگیهای پیشرفتهای است که نیازی به آنها نداریم. UART هایِ رایج امروزه همه با [16550 UART] سازگار هستند، بنابراین ما از آن مدل برای فریمورک تست خود استفاده خواهیم کرد.
[UARTs]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
[مدلهای UART زیادی]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#Models
[16550 UART]: https://en.wikipedia.org/wiki/16550_UART
ما از کریت [`uart_16550`] برای شروع اولیه UART و ارسال دادهها از طریق پورت سریال استفاده خواهیم کرد. برای افزودن آن به عنوان یک وابستگی، ما `Cargo.toml` و `main.rs` خود را به روز میکنیم:
[`uart_16550`]: https://docs.rs/uart_16550
```toml
# in Cargo.toml
[dependencies]
uart_16550 = "0.2.0"
```
کریت `uart_16550` حاوی ساختار `SerialPort` است که نمایانگر ثباتهای UART است، اما ما هنوز هم باید نمونهای از آن را خودمان بسازیم. برای آن ما یک ماژول `serial` جدید با محتوای زیر ایجاد میکنیم:
```rust
// in src/main.rs
mod serial;
```
```rust
// in src/serial.rs
use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;
lazy_static! {
pub static ref SERIAL1: Mutex = {
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
serial_port.init();
Mutex::new(serial_port)
};
}
```
مانند [بافر متن VGA] [vga lazy-static]، ما از `lazy_static` و یک spinlock برای ایجاد یک نمونه نویسنده `static` استفاده میکنیم. با استفاده از `lazy_static` میتوان اطمینان حاصل کرد که متد `init` در اولین استفاده دقیقاً یک بار فراخوانی میشود.
مانند دستگاه `isa-debug-exit`، UART با استفاده از پورت I/O برنامه نویسی میشود. از آنجا که UART پیچیدهتر است، از چندین پورت I/O برای برنامه نویسی رجیسترهای مختلف دستگاه استفاده میکند. تابع ناامن `SerialPort::new` انتظار دارد که آدرس اولین پورت I/O از UART به عنوان آرگومان باشد، که از آن میتواند آدرس تمام پورتهای مورد نیاز را محاسبه کند. ما در حال عبور دادنِ آدرس پورت `0x3F8` هستیم که شماره پورت استاندارد برای اولین رابط سریال است.
[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
برای اینکه پورت سریال به راحتی قابل استفاده باشد، ماکروهای `serial_print!` و `serial_println!` را اضافه میکنیم:
```rust
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
use core::fmt::Write;
SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
}
/// Prints to the host through the serial interface.
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => {
$crate::serial::_print(format_args!($($arg)*));
};
}
/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
concat!($fmt, "\n"), $($arg)*));
}
```
پیاده سازی بسیار شبیه به پیاده سازی ماکروهای `print` و` println` است. از آنجا که نوع `SerialPort` تِرِیت [`fmt::Write`] را پیاده سازی میکند، نیازی نیست این پیاده سازی را خودمان انجام دهیم.
[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
اکنون میتوانیم به جای بافر متن VGA در کد تست خود، روی رابط سریال چاپ کنیم:
```rust
// in src/main.rs
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
[…]
}
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
توجه داشته باشید که ماکرو `serial_println` مستقیماً در زیر فضای نام (ترجمه: namespace) ریشه قرار میگیرد زیرا ما از صفت `#[macro_export]` استفاده کردیم، بنابراین وارد کردن آن از طریق `use crate::serial::serial_println` کار نمی کند.
### آرگومانهای QEMU
برای دیدن خروجی سریال از QEMU، باید از آرگومان `-serial` برای هدایت خروجی به stdout (خروجی استاندارد) استفاده کنیم:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio"
]
```
حالا وقتی `cargo test` را اجرا میکنیم، خروجی تست را مستقیماً در کنسول مشاهده خواهیم گرد:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [ok]
```
با این حال، هنگامی که یک تست ناموفق بود، ما همچنان خروجی را داخل QEMU مشاهده میکنیم، زیرا panic handler هنوز از `println` استفاده میکند. برای شبیهسازی این، میتوانیم assertion درون تست `trivial_assertion` را به `assert_eq!(0, 1)` تغییر دهیم:

میبینیم که پیام panic (تلفظ: پَنیک) هنوز در بافر VGA چاپ میشود، در حالی که خروجی تست دیگر (منظور تستی میباشد که پنیک نکند) در پورت سریال چاپ میشود. پیام پنیک کاملاً مفید است، بنابراین دیدن آن در کنسول نیز مفید خواهد بود.
### چاپ کردن پیام خطا هنگام پنیک کردن
برای خروج از QEMU با یک پیام خطا هنگامی که پنیک رخ میدهد، میتوانیم از [conditional compilation] برای استفاده از یک panic handler متفاوت در حالت تست استفاده کنیم:
[conditional compilation]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html
```rust
// our existing panic handler
#[cfg(not(test))] // new attribute
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
// our panic handler in test mode
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
```
برای panic handler تستِ خودمان، از `serial_println` به جای `println` استفاده میکنیم و سپس با کد خروج خطا از QEMU خارج میشویم. توجه داشته باشید که بعد از فراخوانی `exit_qemu` هنوز به یک حلقه بیپایان نیاز داریم زیرا کامپایلر نمیداند که دستگاه `isa-debug-exit` باعث خروج برنامه میشود.
اکنون QEMU برای تستهای ناموفق نیز خارج شده و یک پیام خطای مفید روی کنسول چاپ می کند:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [failed]
Error: panicked at 'assertion failed: `(left == right)`
left: `0`,
right: `1`', src/main.rs:65:5
```
از آنجایی که اکنون همه خروجیهای تست را در کنسول مشاهده میکنیم، دیگر نیازی به پنجره QEMU نداریم که برای مدت کوتاهی ظاهر میشود. بنابراین میتوانیم آن را کاملا پنهان کنیم.
### پنهان کردن QEMU
از آنجا که ما نتایج کامل تست را با استفاده از دستگاه `isa-debug-exit` و پورت سریال گزارش میکنیم، دیگر نیازی به پنجره QEMU نداریم. ما میتوانیم آن را با عبور دادن آرگومان `-display none` به QEMU پنهان کنیم:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio",
"-display", "none"
]
```
اکنون QEMU کاملا در پس زمینه اجرا میشود و دیگر هیچ پنجرهای باز نمیشود. این نه تنها کمتر آزار دهنده است، بلکه به فریمورک تست ما این امکان را میدهد که در محیطهای بدون رابط کاربری گرافیکی مانند سرویسهای CI یا کانکشنهای [SSH] اجرا شود.
[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
### Timeouts
از آنجا که `cargo test` منتظر میماند تا test runner (ترجمه: اجرا کننده تست) پایان یابد، تستی که هرگز به اتمام نمیرسد (چه موفق، چه ناموفق) میتواند برای همیشه اجرا کننده تست را مسدود کند. این جای تأسف دارد، اما در عمل مشکل بزرگی نیست زیرا اجتناب از حلقههای بیپایان به طور معمول آسان است. با این حال، در مورد ما، حلقههای بیپایان میتوانند در موقعیتهای مختلف رخ دهند:
- بوت لودر موفق به بارگیری هسته نمیشود، در نتیجه سیستم به طور بیوقفه راه اندازی مجدد شود.
- فریمورک BIOS/UEFI قادر به بارگیری بوت لودر نمیشود، در نتیجه باز هم باعث راهاندازی مجدد بیپایان میشود.
- وقتی که CPU در انتهای برخی از توابع ما وارد یک `loop {}` (حلقه بیپایان) میشود، به عنوان مثال به دلیل اینکه دستگاه خروج QEMU به درستی کار نمیکند.
- یا وقتی که سخت افزار باعث ریست شدن سیستم میشود، به عنوان مثال وقتی یک استثنای پردازنده (ترجمه: CPU exception) گیر نمیافتد (در پست بعدی توضیح داده شده است).
از آنجا که حلقه های بیپایان در بسیاری از شرایط ممکن است رخ دهد، به طور پیش فرض ابزار `bootimage` برای هر تست ۵ دقیقه زمان تعیین میکند. اگر تست در این زمان به پایان نرسد، به عنوان ناموفق علامت گذاری شده و خطای "Timed Out" در کنسول چاپ می شود. این ویژگی تضمین میکند که تستهایی که در یک حلقه بیپایان گیر کردهاند، `cargo test` را برای همیشه مسدود نمیکنند.
خودتان میتوانید با افزودن عبارت `loop {}` در تست `trivial_assertion` آن را امتحان کنید. هنگامی که `cargo test` را اجرا میکنید، میبینید که این تست پس از ۵ دقیقه به پایان رسیده است. مدت زمان مهلت از طریق یک کلید `test-timeout` در Cargo.toml [قابل پیکربندی][bootimage config] است:
[bootimage config]: https://github.com/rust-osdev/bootimage#configuration
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-timeout = 300 # (in seconds)
```
اگر نمیخواهید ۵ دقیقه منتظر بمانید تا تست `trivial_assertion` تمام شود، میتوانید به طور موقت مقدار فوق را کاهش دهید.
### اضافه کردن چاپ خودکار
تست `trivial_assertion` در حال حاضر باید اطلاعات وضعیت خود را با استفاده از `serial_print!`/`serial_println!` چاپ کند:
```rust
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
افزودن دستی این دستورات چاپی برای هر تستی که مینویسیم دست و پا گیر است، بنابراین بیایید `test_runner` خود را به روز کنیم تا به صورت خودکار این پیامها را چاپ کنیم. برای انجام این کار، ما باید یک تریت جدید به نام `Testable` ایجاد کنیم:
```rust
// in src/main.rs
pub trait Testable {
fn run(&self) -> ();
}
```
این ترفند اکنون پیاده سازی این تریت برای همه انواع `T` است که [`Fn()` trait] را پیاده سازی میکنند:
[`Fn()` trait]: https://doc.rust-lang.org/stable/core/ops/trait.Fn.html
```rust
// in src/main.rs
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
```
ما با اولین چاپِ نام تابع از طریق تابعِ [`any::type_name`]، تابع `run` را پیاده سازی می کنیم. این تابع مستقیماً در کامپایلر پیاده سازی شده و یک رشته توضیح از هر نوع را برمیگرداند. برای توابع، نوع آنها نامشان است، بنابراین این دقیقاً همان چیزی است که ما در این مورد میخواهیم. کاراکتر `\t` [کاراکتر tab] است، که مقداری ترازبندی به پیامهای `[ok]` اضافه میکند.
[`any::type_name`]: https://doc.rust-lang.org/stable/core/any/fn.type_name.html
[کاراکتر tab]: https://en.wikipedia.org/wiki/Tab_character
پس از چاپ نام تابع، ما از طریق `self ()` تابع تست را فراخوانی میکنیم. این فقط به این دلیل کار میکند که ما نیاز داریم که `self` تریت `Fn()` را پیاده سازی کند. بعد از بازگشت تابع تست، ما `[ok]` را چاپ میکنیم تا نشان دهد که تابع پنیک نکرده است.
آخرین مرحله به روزرسانی `test_runner` برای استفاده از تریت جدید` Testable` است:
```rust
// in src/main.rs
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run(); // new
}
exit_qemu(QemuExitCode::Success);
}
```
تنها دو تغییر رخ داده، نوع آرگومان `tests` از `&[&dyn Fn()]` به `&[&dyn Testable]` است و ما اکنون `test.run()` را به جای `test()` فراخوانی میکنیم.
اکنون میتوانیم عبارات چاپ را از تست `trivial_assertion` حذف کنیم، زیرا آنها اکنون به طور خودکار چاپ میشوند:
```rust
// in src/main.rs
#[test_case]
fn trivial_assertion() {
assert_eq!(1, 1);
}
```
خروجی `cargo test` اکنون به این شکل است:
```
Running 1 tests
blog_os::trivial_assertion... [ok]
```
نام تابع اکنون مسیر کامل به تابع را شامل میشود، که زمانی مفید است که توابع تست در ماژولهای مختلف نام یکسانی دارند. در غیر اینصورت خروجی همانند قبل است، اما دیگر نیازی نیست که به صورت دستی دستورات چاپ را به تستهای خود اضافه کنیم.
## تست کردن بافر VGA
اکنون که یک فریمورک تستِ کارا داریم، میتوانیم چند تست برای اجرای بافر VGA خود ایجاد کنیم. ابتدا، ما یک تست بسیار ساده برای تأیید اینکه `println` بدون پنیک کردن کار میکند ایجاد میکنیم:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_simple() {
println!("test_println_simple output");
}
```
این تست فقط چیزی را در بافر VGA چاپ می کند. اگر بدون پنیک تمام شود، به این معنی است که فراخوانی `println` نیز پنیک نکرده است.
برای اطمینان از این که پنیک ایجاد نمیشود حتی اگر خطوط زیادی چاپ شده و خطوط از صفحه خارج شوند، میتوانیم آزمایش دیگری ایجاد کنیم:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_many() {
for _ in 0..200 {
println!("test_println_many output");
}
}
```
همچنین میتوانیم تابع تستی ایجاد کنیم تا تأیید کنیم که خطوط چاپ شده واقعاً روی صفحه ظاهر می شوند:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_output() {
let s = "Some test string that fits on a single line";
println!("{}", s);
for (i, c) in s.chars().enumerate() {
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
}
```
این تابع یک رشته آزمایشی را تعریف میکند، آن را با استفاده از `println` چاپ میکند و سپس بر روی کاراکترهای صفحه از ` WRITER` ثابت تکرار (iterate) میکند، که نشان دهنده بافر متن vga است. از آنجا که `println` در آخرین خط صفحه چاپ میشود و سپس بلافاصله یک خط جدید اضافه میکند، رشته باید در خط` BUFFER_HEIGHT - 2` ظاهر شود.
با استفاده از [`enumerate`]، تعداد تکرارها را در متغیر `i` حساب میکنیم، سپس از آنها برای بارگذاری کاراکتر صفحه مربوط به `c` استفاده میکنیم. با مقایسه `ascii_character` از کاراکتر صفحه با `c`، اطمینان حاصل میکنیم که هر کاراکتر از این رشته واقعاً در بافر متن vga ظاهر میشود.
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
همانطور که میتوانید تصور کنید، ما میتوانیم توابع تست بیشتری ایجاد کنیم، به عنوان مثال تابعی که تست میکند هنگام چاپ خطوط طولانی پنیک ایجاد نمیشود و به درستی بستهبندی میشوند. یا تابعی برای تست این که خطوط جدید، کاراکترهای غیرقابل چاپ (ترجمه: non-printable) و کاراکترهای non-unicode به درستی اداره میشوند.
برای بقیه این پست، ما نحوه ایجاد _integration tests_ را برای تست تعامل اجزای مختلف با هم توضیح خواهیم داد.
## تستهای یکپارچه
قرارداد [تستهای یکپارچه] در Rust این است که آنها را در یک دایرکتوری `tests` در ریشه پروژه قرار دهید (یعنی در کنار فهرست `src`). فریمورک تست پیش فرض و فریمورکهای تست سفارشی به طور خودکار تمام تستهای موجود در آن فهرست را انتخاب و اجرا میکنند.
[تستهای یکپارچه]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
همه تستهای یکپارچه، فایل اجرایی خاص خودشان هستند و کاملاً از `main.rs` جدا هستند. این بدان معناست که هر تست باید تابع نقطه شروع خود را مشخص کند. بیایید یک نمونه تست یکپارچه به نام `basic_boot` ایجاد کنیم تا با جزئیات ببینیم که چگونه کار میکند:
```rust
// in tests/basic_boot.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
#[unsafe(no_mangle)] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
fn test_runner(tests: &[&dyn Fn()]) {
unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
loop {}
}
```
از آنجا که تستهای یکپارچه فایلهای اجرایی جداگانهای هستند، ما باید تمام صفتهای کریت (`no_std`، `no_main`، `test_runner` و غیره) را دوباره تهیه کنیم. ما همچنین باید یک تابع شروع جدید `_start` ایجاد کنیم که تابع نقطه شروع تست `test_main` را فراخوانی میکند. ما به هیچ یک از ویژگیهای `cfg (test)` نیازی نداریم زیرا اجراییهای تست یکپارچه هرگز در حالت غیر تست ساخته نمیشوند.
ما از ماکرو [ʻunimplemented] استفاده میکنیم که همیشه به عنوان یک مکان نگهدار برای تابع `test_runner` پنیک میکند و فقط در حلقه رسیدگی کننده `panic` فعلاً `loop` میزند. در حالت ایده آل، ما میخواهیم این توابع را دقیقاً همانطور که در `main.rs` خود با استفاده از ماکرو` serial_println` و تابع `exit_qemu` پیاده سازی کردیم، پیاده سازی کنیم. مشکل این است که ما به این توابع دسترسی نداریم زیرا تستها کاملاً جدا از اجرایی `main.rs` ساخته شدهاند.
[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html
اگر در این مرحله `cargo test` را انجام دهید، یک حلقه بیپایان خواهید گرفت زیرا رسیدگی کننده پنیک دارای حلقه بیپایان است. برای خروج از QEMU باید از میانبر صفحه کلید `Ctrl + c` استفاده کنید.
### ساخت یک کتابخانه
برای در دسترس قرار دادن توابع مورد نیاز در تست یکپارچه، باید یک کتابخانه را از `main.rs` جدا کنیم، کتابخانهای که میتواند توسط کریتهای دیگر و تستهای یکپارچه مورد استفاده قرار بگیرد. برای این کار، یک فایل جدید `src/lib.rs` ایجاد میکنیم:
```rust
// src/lib.rs
#![no_std]
```
مانند `main.rs` ،`lib.rs` یک فایل خاص است که به طور خودکار توسط کارگو شناسایی میشود. کتابخانه یک واحد تلفیقی جداگانه است، بنابراین باید ویژگی `#![no_std]` را دوباره مشخص کنیم.
برای اینکه کتابخانهمان با `cargo test` کار کند، باید توابع و صفتهای تست را نیز اضافه کنیم:
To make our library work with `cargo test`, we need to also add the test functions and attributes:
```rust
// in src/lib.rs
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub trait Testable {
fn run(&self) -> ();
}
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run();
}
exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
/// Entry point for `cargo test`
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
test_panic_handler(info)
}
```
برای اینکه `test_runner` را در دسترس تستهای یکپارچه و فایلهای اجرایی قرار دهیم، صفت `cfg(test)` را روی آن اعمال نمیکنیم و عمومی نمیکنیم. ما همچنین پیاده سازی رسیدگی کننده پنیک خود را به یک تابع عمومی `test_panic_handler` تبدیل میکنیم، به طوری که برای اجراییها نیز در دسترس باشد.
از آنجا که `lib.rs` به طور مستقل از` main.rs` ما تست میشود، هنگام کامپایل کتابخانه در حالت تست، باید یک نقطه شروع `_start` و یک رسیدگی کننده پنیک اضافه کنیم. با استفاده از صفت کریت [`cfg_attr`]، در این حالت ویژگی`no_main` را به طور مشروط فعال میکنیم.
[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
ما همچنین اینام `QemuExitCode` و تابع `exit_qemu` را عمومی میکنیم:
```rust
// in src/lib.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
اکنون فایلهای اجرایی و تستهای یکپارچه میتوانند این توابع را از کتابخانه وارد کنند و نیازی به تعریف پیاده سازیهای خود ندارند. برای در دسترس قرار دادن `println` و `serial_println`، اعلان ماژولها را نیز منتقل میکنیم:
```rust
// in src/lib.rs
pub mod serial;
pub mod vga_buffer;
```
ما ماژولها را عمومی میکنیم تا از خارج از کتابخانه قابل استفاده باشند. این امر همچنین برای استفاده از ماکروهای `println` و `serial_println` مورد نیاز است، زیرا آنها از توابع `_print` ماژولها استفاده میکنند.
اکنون می توانیم `main.rs` خود را برای استفاده از کتابخانه به روز کنیم:
```rust
// src/main.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
use blog_os::println;
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
/// This function is called on panic.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
کتابخانه مانند یک کریت خارجی معمولی قابل استفاده است. و مانند کریت (که در مورد ما کریت `blog_os` است) فراخوانی میشود. کد فوق از تابع `blog_os :: test_runner` در صفت `test_runner` و تابع `blog_os :: test_panic_handler` در رسیدگی کننده پنیک `cfg(test)` استفاده میکند. همچنین ماکرو `println` را وارد میکند تا در اختیار توابع `_start` و `panic` قرار گیرد.
در این مرحله، `cargo run` و `cargo test` باید دوباره کار کنند. البته، `cargo test` هنوز هم در یک حلقه بیپایان گیر میکند (با `ctrl + c` میتوانید خارج شوید). بیایید با استفاده از توابع مورد نیاز کتابخانه در تست یکپارچه این مشکل را برطرف کنیم.
### تمام کردن تست یکپارچه
مانند `src/main.rs`، اجرایی` test/basic_boot.rs` میتواند انواع مختلفی را از کتابخانه جدید ما وارد کند. که این امکان را به ما میدهد تا اجزای گمشده را برای تکمیل آزمایش وارد کنیم.
```rust
// in tests/basic_boot.rs
#![test_runner(blog_os::test_runner)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
ما به جای پیاده سازی مجدد اجرا کننده تست، از تابع `test_runner` در کتابخانه خود استفاده میکنیم. برای رسیدگی کننده `panic`، ما تابع `blog_os::test_panic_handler` را مانند آنچه در `main.rs` انجام دادیم، فراخوانی میکنیم.
اکنون `cargo test` مجدداً به طور معمول وجود دارد. وقتی آن را اجرا میکنید ، میبینید که تستهای `lib.rs`، `main.rs` و `basic_boot.rs` ما را به طور جداگانه و یکی پس از دیگری ایجاد و اجرا میکند. برای تستهای یکپارچه `main.rs` و `basic_boot`، متن "Running 0 tests" را نشان میدهد زیرا این فایلها هیچ تابعی با حاشیه نویسی `#[test_case]` ندارد.
اکنون میتوانیم تستها را به `basic_boot.rs` خود اضافه کنیم. به عنوان مثال، ما میتوانیم آزمایش کنیم که `println` بدون پنیک کار میکند، مانند آنچه در تستهای بافر vga انجام دادیم:
```rust
// in tests/basic_boot.rs
use blog_os::println;
#[test_case]
fn test_println() {
println!("test_println output");
}
```
حال وقتی `cargo test` را اجرا میکنیم، میبینیم که این تابع تست را پیدا و اجرا میکند.
این تست ممکن است در حال حاضر کمی بیفایده به نظر برسد، زیرا تقریباً مشابه یکی از تستهای بافر VGA است. با این حال، در آینده ممکن است توابع `_start` ما از `main.rs` و `lib.rs` رشد کرده و روالهای اولیه مختلفی را قبل از اجرای تابع `test_main` فراخوانی کنند، به طوری که این دو تست در محیطهای بسیار مختلف اجرا میشوند.
### تستهای آینده
قدرت تستهای یکپارچه این است که با آنها به عنوان اجرایی کاملاً جداگانه برخورد میشود. این امر به آنها اجازه کنترل کامل بر محیط را میدهد، و امکان تست کردن این که کد به درستی با CPU یا دستگاههای سختافزاری ارتباط دارد را به ما میدهد.
تست `basic_boot` ما یک مثال بسیار ساده برای تست یکپارچه است. در آینده، هسته ما ویژگیهای بسیار بیشتری پیدا میکند و از راههای مختلف با سخت افزار ارتباط برقرار میکند. با افزودن تست های یکپارچه، میتوانیم اطمینان حاصل کنیم که این تعاملات مطابق انتظار کار میکنند (و به کار خود ادامه میدهند). برخی از ایدهها برای تستهای احتمالی در آینده عبارتند از:
- **استثنائات CPU**: هنگامی که این کد عملیات نامعتبری را انجام میدهد (به عنوان مثال تقسیم بر صفر)، CPU یک استثنا را ارائه میدهد. هسته میتواند توابع رسیدگی کننده را برای چنین مواردی ثبت کند. یک تست یکپارچه میتواند تأیید کند که در صورت بروز استثنا پردازنده ، رسیدگی کننده استثنای صحیح فراخوانی میشود یا اجرای آن پس از استثناهای قابل حل به درستی ادامه دارد.
- **جدولهای صفحه**: جدولهای صفحه مشخص میکند که کدام مناطق حافظه معتبر و قابل دسترسی هستند. با اصلاح جدولهای صفحه، میتوان مناطق حافظه جدیدی را اختصاص داد، به عنوان مثال هنگام راهاندازی برنامهها. یک تست یکپارچه میتواند برخی از تغییرات جدولهای صفحه را در تابع `_start` انجام دهد و سپس تأیید کند که این تغییرات در تابعهای `# [test_case]` اثرات مطلوبی دارند.
- **برنامههای فضای کاربر**: برنامههای فضای کاربر برنامههایی با دسترسی محدود به منابع سیستم هستند. به عنوان مثال، آنها به ساختار دادههای هسته یا حافظه برنامههای دیگر دسترسی ندارند. یک تست یکپارچه میتواند برنامههای فضای کاربر را که عملیاتهای ممنوعه را انجام میدهند راهاندازی کرده و بررسی کند هسته از همه آنها جلوگیری میکند.
همانطور که میتوانید تصور کنید، تستهای بیشتری امکان پذیر است. با افزودن چنین تستهایی، میتوانیم اطمینان حاصل کنیم که وقتی ویژگیهای جدیدی به هسته خود اضافه میکنیم یا کد خود را دوباره میسازیم، آنها را به طور تصادفی خراب نمیکنیم. این امر به ویژه هنگامی مهمتر میشود که هسته ما بزرگتر و پیچیدهتر شود.
### تستهایی که باید پنیک کنند
فریمورک تست کتابخانه استاندارد از [صفت `#[should_panic]`][should_panic] پشتیبانی میکند که اجازه میدهد تستهایی را بسازد که باید ناموفق شوند (باید پنیک کنند). این مفید است، به عنوان مثال برای تأیید پنیک کردن یک تابع هنگام عبور دادن یک آرگومان نامعتبر به آن. متأسفانه این ویژگی در کریتهای `#[no_std]` پشتیبانی نمیشود زیرا به پشتیبانی از کتابخانه استاندارد نیاز دارد.
[should_panic]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panics
اگرچه نمیتوانیم از صفت `#[should_panic]` در هسته خود استفاده کنیم، اما میتوانیم با ایجاد یک تست یکپارچه که با کد خطای موفقیت آمیز از رسیدگی کننده پنیک خارج میشود، رفتار مشابهی داشته باشیم. بیایید شروع به ایجاد چنین تستی با نام `should_panic` کنیم:
```rust
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{QemuExitCode, exit_qemu, serial_println};
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
این تست هنوز ناقص است زیرا هنوز تابع `_start` یا هیچ یک از صفتهای اجرا کننده تست سفارشی را مشخص نکرده. بیایید قسمتهای گمشده را اضافه کنیم:
```rust
// in tests/should_panic.rs
#![feature(custom_test_frameworks)]
#![test_runner(test_runner)]
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
pub fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
}
exit_qemu(QemuExitCode::Success);
}
```
به جای استفاده مجدد از `test_runner` از `lib.rs`، تست تابع `test_runner` خود را تعریف میکند که هنگام بازگشت یک تست بدون پنیک با یک کد خروج خطا خارج میشود (ما میخواهیم تستهایمان پنیک داشته باشند). اگر هیچ تابع تستی تعریف نشده باشد، اجرا کننده با کد خطای موفقیت خارج میشود. از آنجا که اجرا کننده همیشه پس از اجرای یک تست خارج میشود، منطقی نیست که بیش از یک تابع `#[test_case]` تعریف شود.
اکنون میتوانیم یک تست ایجاد کنیم که باید شکست بخورد:
```rust
// in tests/should_panic.rs
use blog_os::serial_print;
#[test_case]
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
```
این تست با استفاده از `assert_eq` ادعا (ترجمه: assert) میکند که `0` و `1` برابر هستند. این البته ناموفق است، به طوری که تست ما مطابق دلخواه پنیک میکند. توجه داشته باشید که ما باید نام تابع را با استفاده از `serial_print!` در اینجا چاپ دستی کنیم زیرا از تریت `Testable` استفاده نمیکنیم.
هنگامی که ما تست را از طریق `cargo test --test should_panic` انجام دهیم، میبینیم که موفقیت آمیز است زیرا تست مطابق انتظار پنیک کرد. وقتی ادعا را کامنت کنیم و تست را دوباره اجرا کنیم، میبینیم که با پیام _"test did not panic"_ با شکست مواجه میشود.
یک اشکال قابل توجه در این روش این است که این روش فقط برای یک تابع تست کار میکند. با چندین تابع `#[test_case]`، فقط اولین تابع اجرا میشود زیرا پس اینکه رسیدگی کننده پنیک فراخوانی شد، اجرا تمام میشود. من در حال حاضر راه خوبی برای حل این مشکل نمیدانم، بنابراین اگر ایدهای دارید به من اطلاع دهید!
### تست های بدون مهار
برای تستهای یکپارچه که فقط یک تابع تست دارند (مانند تست `should_panic` ما)، اجرا کننده تست مورد نیاز نیست. برای مواردی از این دست، ما میتوانیم اجرا کننده تست را به طور کامل غیرفعال کنیم و تست خود را مستقیماً در تابع `_start` اجرا کنیم.
کلید این کار غیرفعال کردن پرچم `harness` برای تست در` Cargo.toml` است، که مشخص میکند آیا از یک اجرا کننده تست برای تست یکپارچه استفاده میشود. وقتی روی `false` تنظیم شود، هر دو اجرا ککنده تست پیش فرض و سفارشی غیرفعال میشوند، بنابراین با تست مانند یک اجرای معمولی رفتار میشود.
بیایید پرچم `harness` را برای تست `should_panic` خود غیرفعال کنیم:
```toml
# in Cargo.toml
[[test]]
name = "should_panic"
harness = false
```
اکنون ما با حذف کد مربوط به آاجرا کننده تست، تست `should_panic` خود را بسیار ساده کردیم. نتیجه به این شکل است:
```rust
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
should_fail();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
loop{}
}
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
اکنون تابع `should_fail` را مستقیماً از تابع `_start` خود فراخوانی میکنیم و در صورت بازگشت با کد خروج شکست خارج میشویم. اکنون وقتی `cargo test --test should_panic` را اجرا میکنیم، میبینیم که تست دقیقاً مانند قبل عمل میکند.
غیر از ایجاد تستهای `should_panic`، غیرفعال کردن صفت `harness` همچنین میتواند برای تستهای یکپارچه پیچیده مفید باشد، به عنوان مثال هنگامی که تابعهای منفرد دارای عوارض جانبی هستند و باید به ترتیب مشخصی اجرا شوند.
## خلاصه
تست کردن یک تکنیک بسیار مفید است تا اطمینان حاصل شود که اجزای خاصی رفتار مطلوبی دارند. حتی اگر آنها نتوانند فقدان اشکالات را نشان دهند، آنها هنوز هم یک ابزار مفید برای یافتن آنها و به ویژه برای جلوگیری از دوباره کاری و پسرفت هستند.
در این پست نحوه تنظیم فریمورک تست برای هسته Rust ما توضیح داده شده است. ما از ویژگی فریمورک تست سفارشی Rust برای پیاده سازی پشتیبانی از یک صفت ساده `#[test_case]` در محیط bare-metal خود استفاده کردیم. با استفاده از دستگاه `isa-debug-exit` شبیهساز ماشین و مجازیساز QEMU، اجرا کننده تست ما میتواند پس از اجرای تستها از QEMU خارج شده و وضعیت تست را گزارش دهد. برای چاپ پیامهای خطا به جای بافر VGA در کنسول، یک درایور اساسی برای پورت سریال ایجاد کردیم.
پس از ایجاد چند تست برای ماکرو `println`، در نیمه دوم پست به بررسی تستهای یکپارچه پرداختیم. ما فهمیدیم که آنها در دایرکتوری `tests` قرار میگیرند و به عنوان اجرایی کاملاً مستقل با آنها رفتار میشود. برای دسترسی دادن به آنها به تابع `exit_qemu` و ماکرو `serial_println`، بیشتر کدهای خود را به یک کتابخانه منتقل کردیم که میتواند توسط همه اجراها و تستهای یکپارچه وارد (import) شود. از آنجا که تستهای یکپارچه در محیط جداگانه خود اجرا میشوند، آنها تست تعاملاتی با سختافزار یا ایجاد تستهایی که باید پنیک کنند را امکان پذیر می کنند.
اکنون یک فریمورک تست داریم که در یک محیط واقع گرایانه در داخل QEMU اجرا میشود. با ایجاد تستهای بیشتر در پستهای بعدی، میتوانیم هسته خود را هنگامی که پیچیدهتر شود، نگهداری کنیم.
## مرحله بعدی چیست؟
در پست بعدی، ما _استثنائات CPU_ را بررسی خواهیم کرد. این موارد استثنایی توسط CPU در صورت بروز هرگونه اتفاق غیرقانونی، مانند تقسیم بر صفر یا دسترسی به صفحه حافظه مپ نشده (اصطلاحاً "خطای صفحه")، رخ میدهد. امکان کشف و بررسی این موارد استثنایی برای رفع اشکال در خطاهای آینده بسیار مهم است. رسیدگی به استثناها نیز بسیار شبیه رسیدگی به وقفههای سختافزاری است، که برای پشتیبانی صفحه کلید مورد نیاز است.
================================================
FILE: blog/content/edition-2/posts/04-testing/index.ja.md
================================================
+++
title = "テスト"
weight = 4
path = "ja/testing"
date = 2019-04-27
[extra]
# Please update this when updating the translation
translation_based_on_commit = "e6c148d6f47bcf8a34916393deaeb7e8da2d5e2a"
# GitHub usernames of the people that translated this post
translators = ["swnakamura", "JohnTitor","ic3w1ne"]
+++
この記事では、`no_std`な実行環境における単体テストと結合テストについて学びます。Rustではカスタムテストフレームワークがサポートされているので、これを使ってカーネルの中でテスト関数を実行します。QEMUの外へとテストの結果を通知するため、QEMUと`bootimage`の様々な機能を使います。
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-04` ブランチ][post branch]にあります。
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-04
## この記事を読む前に
この記事は、(古い版の)[単体テスト][_Unit Testing_]と[結合テスト][_Integration Tests_]の記事を置き換えるものです。この記事は、あなたが[最小のカーネル][_A Minimal Rust Kernel_]の記事を2019-04-27以降に読んだことを前提にしています。主に、あなたの`.cargo/config.toml`ファイルが[標準のターゲットを設定して][sets a default target]おり、[ランナー実行ファイルを定義している][defines a runner executable]ことが条件となります。
**訳注:** [最小のカーネル][_A Minimal Rust Kernel_]の記事が日本語に翻訳されたのはこの日より後なので、あなたがこのサイトを日本語で閲覧している場合は特に問題はありません。
[_Unit Testing_]: @/edition-2/posts/deprecated/04-unit-testing/index.md
[_Integration Tests_]: @/edition-2/posts/deprecated/05-integration-tests/index.md
[_A Minimal Rust Kernel_]: @/edition-2/posts/02-minimal-rust-kernel/index.ja.md
[sets a default target]: @/edition-2/posts/02-minimal-rust-kernel/index.ja.md#biao-zhun-notagetutowosetutosuru
[defines a runner executable]: @/edition-2/posts/02-minimal-rust-kernel/index.ja.md#cargo-runwoshi-u
## Rustにおけるテスト
Rustには[テストフレームワークが組み込まれて][built-in test framework]おり、特別な設定なしに単体テストを走らせることができます。何らかの結果をアサーションを使って確認する関数を作り、その関数のヘッダに`#[test]`属性をつけるだけです。その上で`cargo test`を実行すると、あなたのクレートのすべてのテスト関数を自動で見つけて実行してくれます。
[built-in test framework]: https://doc.rust-jp.rs/book-ja/ch11-00-testing.html
カーネルバイナリのテストを有効にするには、Cargo.toml の `test` フラグを `true` に設定します:
```toml
# Cargo.toml 内
[[bin]]
name = "blog_os"
test = true
bench = false
```
この [`[[bin]]` セクション](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#configuring-a-target) は、`cargo` が `blog_os` 実行可能ファイルをどのようにコンパイルするかを指定します。
`test` フィールドは、この実行可能ファイルに対してテストがサポートされているかどうかを指定します。
最初の投稿では、[`rust-analyzer` を正常に動作させる](@/edition-2/posts/01-freestanding-rust-binary/index.md#making-rust-analyzer-happy)ために `test = false` に設定しましたが、今回はテストを有効にしたいので、`true` に戻します。
残念なことに、私達のカーネルのような`no_std`のアプリケーションにとっては、テストは少しややこしくなります。問題なのは、Rustのテストフレームワークは組み込みの[`test`][`test`]ライブラリを内部で使っており、これは標準ライブラリに依存しているということです。つまり、私達の`#[no_std]`のカーネルには標準のテストフレームワークは使えないのです。
[`test`]: https://doc.rust-lang.org/test/index.html
私達のプロジェクト内で`cargo test`を実行しようとすればそれがわかります:
```
> cargo test
Compiling blog_os v0.1.0 (/…/blog_os)
error[E0463]: can't find crate for `test`
```
`test`クレートは標準ライブラリに依存しているので、私達のベアメタルのターゲットでは使えません。`test`クレートを`#[no_std]`環境に持ってくるということは[不可能ではない][utest]のですが、非常に不安定であり、また`panic`マクロの再定義といった技巧が必要になってしまいます。
[utest]: https://github.com/japaric/utest
### 独自のテストフレームワーク
ありがたいことに、Rustでは、不安定な[`custom_test_frameworks`][`custom_test_frameworks`]機能を使えば標準のテストフレームワークを置き換えることができます。この機能には外部ライブラリは必要なく、したがって`#[no_std]`環境でも動きます。これは、`#[test_case]`属性をつけられたすべての関数のリストを引数としてユーザの指定した実行関数を呼び出すことで働きます。こうすることで、(実行関数の)実装内容によってテストプロセスを最大限コントロールできるようにしているのです。
[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html
標準のテストフレームワークと比べた欠点は、[`should_panic`テスト][`should_panic` tests]のような多くの高度な機能が利用できないということです。それらの機能が必要なら、自分で実装して提供してください、というわけです。これは私達にとって全く申し分のないことで、というのも、私達の非常に特殊な実行環境では、それらの高度な機能の標準の実装はいずれにせようまく働かないだろうからです。例えば、`#[should_panic]`属性はパニックを検知するためにスタックアンワインドを使いますが、これは私達のカーネルでは無効化しています。
[`should_panic` tests]: https://doc.rust-jp.rs/book-ja/ch11-01-writing-tests.html#should_panicでパニックを確認する
私達のカーネルのための独自テストフレームワークを実装するため、以下を`main.rs`に追記します:
```rust
// in src/main.rs
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
}
```
このランナーは短いデバッグメッセージを表示し、リスト内のそれぞれの関数を呼び出すだけです。引数の型である`&[&dyn Fn()]`は、[Fn()][_Fn()_]トレイトの[トレイトオブジェクト][_trait object_]参照の[スライス][_slice_]です。これは要するに、関数のように呼び出せる型への参照のリストです。この (test_runner) 関数はテストでない実行のときには意味がないので、`#[cfg(test)]`属性を使って、テスト時にのみこれがインクルードされるようにします。
[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html
[_trait object_]: https://doc.rust-jp.rs/book-ja/ch17-02-trait-objects.html
[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html
`cargo test`を実行すると、今度は成功しているはずです(もし失敗したなら、下の補足を読んでください)。しかし、依然として、`test_runner`からのメッセージではなく "Hello World" が表示されてしまっています。この理由は、`_start`関数がまだエントリポイントとして使われているからです。「独自のテストフレームワーク」機能は`test_runner`を呼び出す`main`関数を生成するのですが、私達は`#[no_main]`属性を使っており、独自のエントリポイントを与えてしまっているため、このmain関数は無視されてしまうのです。
**補足:** 現在、cargoには`cargo test`を実行すると、いくらかのケースにおいて "duplicate lang item" エラーになってしまうバグが存在します。これは、`Cargo.toml`内のプロファイルにおいて`panic = "abort"`を設定していたときに起こります。これを取り除けば`cargo test`はうまくいくはずです。これについて、より詳しく知りたい場合は[cargoのissue](https://github.com/rust-lang/cargo/issues/7359)を読んでください。
これを修正するために、まず生成される関数の名前を`reexport_test_harness_main`属性を使って`main`とは違うものに変える必要があります。そして、その改名された関数を`_start`関数から呼び出せばよいです。
```rust
// in src/main.rs
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
```
テストフレームワークのエントリ関数の名前を`test_main`に設定し、私達の`_start`エントリポイントから呼び出しています。`test_main`関数は通常の実行時には生成されていないので、[条件付きコンパイル][conditional compilation]を用いて、テスト時にのみこの関数への呼び出しが追記されるようにしています。
`cargo test`を実行すると、 `test_runner`からの "Running 0 tests" というメッセージが画面に表示されます。これで、テスト関数を作り始める準備ができました:
```rust
// in src/main.rs
#[test_case]
fn trivial_assertion() {
print!("trivial assertion... "); // "些末なアサーション……"
assert_eq!(1, 1);
println!("[ok]");
}
```
`cargo test`を実行すると、以下の出力を得ます:
![QEMU printing "Hello World!", "Running 1 tests", and "trivial assertion... [ok]"](qemu-test-runner-output.png)
今、`test_runner`関数に渡される`test`のスライスは、`trivial_assertion`関数への参照を保持しています。`trivial assertion... [ok]`という画面の出力から、テストが呼び出され成功したことがわかります。
テストを実行したあとは、`test_runner`から`test_main`関数へとリターンし、さらに`_start`エントリポイント関数へとリターンします。エントリポイント関数がリターンすることは認められていないので、`_start`の最後では無限ループに入ります。しかし、`cargo test`にはすべてのテストを実行し終わった後に終了してほしいので、これは問題です。
## QEMUを終了する
今の所、`_start`関数の最後で無限ループがあるので、`cargo test`を実行するたびにQEMUを手動で終了しないといけません。ユーザによる入力などのないスクリプトでも`cargo test`を実行したいので、これは不都合です。これに対する綺麗な解決法はOSをシャットダウンする適切な方法を実装することでしょう。これは[APM]か[ACPI]というパワーマネジメント標準規格へのサポートを実装する必要があるので、残念なことに比較的複雑です。
[APM]: https://wiki.osdev.org/APM
[ACPI]: https://wiki.osdev.org/ACPI
しかし嬉しいことに、ある「脱出口」があるのです。QEMUは特殊な`isa-debug-exit`デバイスをサポートしており、これを使うとゲストシステムから簡単にQEMUを終了できます。これを有効化するためには、QEMUに`-device`引数を渡す必要があります。これは`Cargo.toml`に`package.metadata.bootimage.test-args`設定キーを追加することで行えます。
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
```
`bootimage runner`は、`test-args`をすべてのテスト実行可能ファイルの標準QEMUコマンドに追加します。通常の`cargo run`のとき、これらの引数は無視されます。
デバイス名 (`isa-debug-exit`) に加え、カーネルからそのデバイスにたどり着くための **I/Oポート** を指定する`iobase`と`iosize`という2つのパラメータを渡しています。
### I/Oポート
CPUと周辺機器が通信するやり方には、 **memory-mapped I/O** と **port-mapped I/O** の2つがあります。memory-mapped I/Oについては、すでに[VGAテキストバッファ][VGA text buffer]にメモリアドレス`0xb8000`を使ってアクセスしたときに使っています。このアドレスはRAMではなく、VGAデバイス上にあるメモリにマップされているのです。
[VGA text buffer]: @/edition-2/posts/03-vga-text-buffer/index.ja.md
一方、port-mapped I/Oは通信に別個のI/Oバスを使います。接続されたそれぞれの周辺機器は1つ以上のポート番号を持っています。それらのI/Oポートと通信するために、`in`と`out`という特別なCPU命令があり、これらはポート番号と1バイトのデータを受け取ります(`u16`や`u32`を送信できる、これらの亜種も存在します)。
`isa-debug-exit`はこのport-mapped I/Oを使います。`iobase`パラメータはどのポートにこのデバイスが繋がれているのか(`0xf4`はx86のI/Oバスにおいて[普通使われない][list of x86 I/O ports]ポートです)を、`iosize`はポートの大きさ(`0x04`は4バイトを意味します)を指定します。
[list of x86 I/O ports]: https://wiki.osdev.org/I/O_Ports#The_list
### 「終了デバイス」を使う
`isa-debug-exit`の機能は非常に単純です。値`value`が`iobase`により指定されたI/Oポートに書き込まれたら、QEMUは[終了ステータス][exit status]を`(value << 1) | 1`にして終了します。なので、このポートに`0`を書き込むと、QEMUは終了ステータス`(0 << 1) | 1 = 1`で、`1`を書き込むと終了ステータス`(1 << 1) | 1 = 3`で終了します。
[exit status]: https://ja.wikipedia.org/wiki/終了ステータス
`in`と`out`のアセンブリ命令を手動で呼び出す代わりに、[`x86_64`]クレートによって提供されるabstractionを使います。このクレートへの依存を追加するため、`Cargo.toml`の`dependencies`セクションにこれを追加しましょう:
[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/
```toml
# in Cargo.toml
[dependencies]
x86_64 = "0.14.2"
```
これで、このクレートによって提供される[`Port`]型を使って`exit_qemu`関数を作ることができます。
[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html
```rust
// in src/main.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
この関数は新しい[`Port`]を`0xf4`(`isa-debug-exit`デバイスの`iobase`です)に作ります。そして、渡された終了コードをポートに書き込みます。`isa-device-exit`デバイスの`iosize`に4バイトを指定していたので、`u32`を使うことにします。I/Oポートへの書き込みは一般にあらゆる振る舞いを引き起こしうるので、これらの命令は両方unsafeです。
終了ステータスを指定するために、`QemuExitCode`enumを作ります。成功したら成功(`Success`)の終了コードで、そうでなければ失敗(`Failed`)の終了コードで終了しようというわけです。enumは`#[repr(u32)]`をつけることで、それぞれのヴァリアントが`u32`の整数として表されるようにしています。終了コード`0x10`を成功に、`0x11`を失敗に使います。終了コードの実際の値は、QEMUの標準の終了コードと被ってしまわない限りはなんでも構いません。例えば、成功の終了コードに`0`を使うと、変換後`(0 << 1) | 1 = 1`になってしまい、これはQEMUが実行に失敗したときの標準終了コードなのでよくありません。QEMUのエラーとテスト実行の成功が区別できなくなります。
というわけで、`test_runner`を更新して、すべてのテストが実行されたあとでQEMUを終了するようにできますね:
```rust
fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
/// new
exit_qemu(QemuExitCode::Success);
}
```
`cargo test`を実行すると、QEMUはテスト実行後即座に閉じるのがわかります。しかし、問題は、`Success`の終了コードを渡したのに、`cargo test`はテストが失敗したと解釈することです:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be
Building bootloader
Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader)
Finished release [optimized + debuginfo] target(s) in 1.07s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4,
iosize=0x04`
error: test failed, to rerun pass '--bin blog_os'
```
問題は、`cargo test`が`0`でないすべてのエラーコードを失敗と解釈してしまうことです。
### 成功の終了コード
これを解決するために、`bootimage`は指定された終了コードを`0`へとマップする設定キー、`test-success-exit-code`を提供しています:
```toml
[package.metadata.bootimage]
test-args = […]
test-success-exit-code = 33 # (0x10 << 1) | 1
```
この設定を使うと、`bootimage`は私達の出した成功の終了コードを、終了コード0へとマップするので、`cargo test`は正しく成功を認識し、テストを失敗したと見做さなくなります。
これで私達のテストランナーは、自動でQEMUを閉じ、結果を報告するようになりました。しかし、QEMUの画面が非常に短い時間開くのは見えますが、短すぎて結果が読めません。QEMUが終了したあともテストの結果が見られるように、コンソールに出力できたら良さそうです。
## コンソールに出力する
テストの結果をコンソールで見るためには、カーネルからホストシステムにどうにかしてデータを送る必要があります。これを達成する方法は色々あり、例えばTCPネットワークインターフェースを通じてデータを送るというのが考えられます。しかし、ネットワークスタックを設定するのは非常に複雑なタスクなので、より簡単な解決策を取ることにしましょう。
### シリアルポート
データを送る簡単な方法とは、[シリアルポート][serial port]という、最近のコンピュータにはもはや見られない古いインターフェース標準を使うことです。これはプログラムするのが簡単で、QEMUはシリアルを通じて送られたデータをホストの標準出力やファイルにリダイレクトすることができます。
[serial port]: https://ja.wikipedia.org/wiki/シリアルポート
シリアルインターフェースを実装しているチップは[UART][UARTs]と呼ばれています。x86には[多くのUARTのモデルがありますが][lots of UART models]、幸運なことに、それらの違いは私達の必要としないような高度な機能だけです。今日よく見られるUARTはすべて[16550 UART]に互換性があるので、このモデルを私達のテストフレームワークに使いましょう。
[UARTs]: https://ja.wikipedia.org/wiki/UART
[lots of UART models]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#Models
[16550 UART]: https://ja.wikipedia.org/wiki/16550_UART
[`uart_16550`]クレートを使ってUARTを初期化しデータをシリアルポートを使って送信しましょう。これを依存先として追加するため、`Cargo.toml`と`main.rs`を書き換えます:
[`uart_16550`]: https://docs.rs/uart_16550
```toml
# in Cargo.toml
[dependencies]
uart_16550 = "0.2.0"
```
`uart_16550`クレートにはUARTレジスタを表現する`SerialPort`構造体が含まれていますが、これのインスタンスは私達自身で作らなくてはいけません。そのため、以下の内容で新しい`serial`モジュールを作りましょう:
```rust
// in src/main.rs
mod serial;
```
```rust
// in src/serial.rs
use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;
lazy_static! {
pub static ref SERIAL1: Mutex = {
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
serial_port.init();
Mutex::new(serial_port)
};
}
```
[VGAテキストバッファ][vga lazy-static]のときのように、`lazy_static`とスピンロックを使って`static`なwriterインスタンスを作ります。`lazy_static`を使うことで、`init`メソッドが初回使用時にのみ呼び出されることを保証できます。
`isa-debug-exit`デバイスのときと同じように、UARTはport I/Oを使ってプログラムされています。UARTはより複雑で、様々なデバイスレジスタ群をプログラムするために複数のI/Oポートを使います。unsafeな`SerialPort::new`関数はUARTの最初のI/Oポートを引数とします。この引数から、すべての必要なポートのアドレスを計算することができます。ポートアドレス`0x3F8`を渡していますが、これは最初のシリアルインターフェースの標準のポート番号です。
[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
シリアルポートを簡単に使えるようにするために、`serial_print!`と`serial_println!`マクロを追加します:
```rust
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
use core::fmt::Write;
SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
}
/// シリアルインターフェースを通じてホストに出力する。
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => {
$crate::serial::_print(format_args!($($arg)*));
};
}
/// シリアルインターフェースを通じてホストに出力し、改行を末尾に追加する。
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
concat!($fmt, "\n"), $($arg)*));
}
```
この実装は私達の`print`および`println`マクロとよく似ています。`SerialPort`型はすでに[`fmt::Write`]トレイトを実装しているので、自前の実装を提供する必要はありません。
[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
これで、テストコードにおいてVGAテキストバッファの代わりにシリアルインターフェースに出力することができます:
```rust
// in src/main.rs
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
[…]
}
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
`#[macro_export]`属性を使うことで、`serial_println`マクロはルート名前空間の直下に置かれるので、`use crate::serial::serial_println`とインポートするとうまくいかないということに注意してください。
### QEMUの引数
QEMUからのシリアル出力を見るために、出力を標準出力にリダイレクトしたいので、`-serial`引数を使う必要があります。
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio"
]
```
これで`cargo test`を実行すると、テスト出力がコンソールに直接出力されているのが見えるでしょう:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [ok]
```
しかし、テストが失敗したときは、私達のパニックハンドラはまだ`println`を使っているので、出力がQEMUの中に出てしまいます。これをシミュレートするには、`trivial_assertion`テストの中のアサーションを`assert_eq!(0, 1)`に変えればよいです:

他のテスト出力がシリアルポートに出力されている一方、パニックメッセージはまだVGAバッファに出力されているのがわかります。このパニックメッセージは非常に役に立つので、コンソールでこのメッセージも見られたら非常に便利でしょう。
### パニック時のエラーメッセージを出力する
パニック時にQEMUをエラーメッセージとともに終了するためには、[条件付きコンパイル][conditional compilation]を使うことで、テスト時に異なるパニックハンドラを使うことができます:
[conditional compilation]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html
```rust
// 前からあるパニックハンドラ
#[cfg(not(test))] // 新しく追加した属性
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
// テストモードで使うパニックハンドラ
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
```
テストパニックハンドラには`println`の代わりに`serial_println`を使い、そのあと失敗の終了コードでQEMUを終了します。コンパイラには、`exit_qemu`の呼び出しのあと`isa-debug-exit`デバイスがプログラムを終了させているということはわからないので、やはり最後に無限ループを入れないといけないことに注意してください。
これでQEMUはテストが失敗したときも終了し、コンソールに役に立つエラーメッセージを表示するようになります:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [failed]
Error: panicked at 'assertion failed: `(left == right)`
left: `0`,
right: `1`', src/main.rs:65:5
```
これですべてのテスト出力がコンソールで見られるようになったので、一瞬出てくるQEMUウィンドウはもはや必要ありません。ですので、これを完全に見えなくしてしまいましょう。
### QEMUを隠す
すべてのテスト結果を`isa-debug-exit`デバイスとシリアルポートを使って通知できるので、QEMUのウィンドウはもはや必要ありません。これは、QEMUに`-display none`引数を渡すことで隠すことができます:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio",
"-display", "none"
]
```
これでQEMUは完全にバックグラウンドで実行するようになり、ウィンドウはもう開きません。これで、ジャマが減っただけでなく、私達のテストフレームワークがグラフィカルユーザーインターフェースのない環境――たとえばCIサービスや[SSH]接続――でも使えるようになりました。
[SSH]: https://ja.wikipedia.org/wiki/Secure_Shell
### タイムアウト
`cargo test`はテストランナーが終了するまで待つので、絶対に終了しないテストがあるとテストランナーを永遠にブロックしかねません。これは悲しいですが、普通エンドレスループを回避するのは簡単なので、実際は大きな問題ではありません。しかしながら、私達のケースでは、様々な状況でエンドレスループが発生しうるのです:
- ブートローダーが私達のカーネルを読み込むのに失敗し、これによりシステムが延々と再起動し続ける。
- BIOS/UEFIファームウェアがブートローダーの読み込みに失敗し、同様に延々と再起動し続ける。
- 私達の関数のどれかの最後で、CPUが`loop {}`文に入ってしまう(例えば、QEMU終了デバイスがうまく動かなかったなどの理由で)。
- CPU例外(今後説明します)がうまく捕捉されなかった場合などに、ハードウェアがシステムリセットを行う。
エンドレスループは非常に多くの状況で発生しうるので、`bootimage`はそれぞれのテスト実行ファイルに対し標準で5分のタイムアウトを設定しています。テストがこの時間内に終了しなかった場合は失敗したとみなされ、"Timed Out" エラーがコンソールに出力されます。この機能により、エンドレスループで詰まったテストが`cargo test`を永遠にブロックしてしまうことがないことが保証されます。
これを自分で試すこともできます。`trivial_assertion`テストに`loop {}`文を追加してください。`cargo test`を実行すると、5分後にテストがタイムアウトしたことが表示されるでしょう。タイムアウトまでの時間は`Cargo.toml`の`test-timeout`キーで[設定可能][bootimage config]です:
[bootimage config]: https://github.com/rust-osdev/bootimage#configuration
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-timeout = 300 # (単位は秒)
```
`trivial_assertion`テストがタイムアウトするのを待ちたくない場合は、上の値を一時的に下げても良いでしょう。
### 出力機能を自動で挿入する
現在、私達の`trivial_assertion`テストは、自分のステータス情報を`serial_print!`/`serial_println!`を使って出力する必要があります:
```rust
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
私達の書くすべてのテストにこれらのprint文を手動で追加するのは煩わしいので、私達の`test_runner`を変更して、これらのメッセージを自動で出力するようにしましょう。そうするためには、`Testable`トレイトを作る必要があります:
```rust
// in src/main.rs
pub trait Testable {
fn run(&self) -> ();
}
```
ここで、[`Fn()`トレイト][`Fn()` trait]を持つ型`T`すべてにこのトレイトを実装してやるのがミソです:
[`Fn()` trait]: https://doc.rust-lang.org/stable/core/ops/trait.Fn.html
```rust
// in src/main.rs
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
```
`run`関数を実装するに当たり、まず[`any::type_name`]を使って関数の名前を出力します。この関数はコンパイラの中に直接実装されており、すべての型の文字列による説明を返すことができます。関数の型はその名前なので、今回の場合まさに私達のやりたいことができています。文字`\t`は[タブ文字][tab character]であり、メッセージ`[ok]`の前にちょっとしたアラインメント(幅を整えるための空白)をつけます。
[`any::type_name`]: https://doc.rust-lang.org/stable/core/any/fn.type_name.html
[tab character]: https://ja.wikipedia.org/wiki/タブキー#タブ文字
関数名を出力したあとは、テスト関数を`self()`を使って呼び出します。これは、`self`が`Fn()`トレイトを実装していることが要求されているからこそ可能です。テスト関数がリターンしたら、`[ok]`を出力してこの関数がパニックしなかったことを示します。
最後に、`test_runner`をこの`Testable`トレイトを使うように更新します:
```rust
// in src/main.rs
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run(); // ここを変更
}
exit_qemu(QemuExitCode::Success);
}
```
変更点は2つだけで、`tests`引数の型を`&[&dyn Fn()]`から`&[&dyn Testable]`に変えたことと、`test()`の変わりに`test.run()`を呼ぶようにしたことです。
また、`trivial_assertion`のprint文は今や自動で出力されるようになったので、これを取り除きましょう:
```rust
// in src/main.rs
#[test_case]
fn trivial_assertion() {
assert_eq!(1, 1);
}
```
これで`cargo test`の出力は以下のようになるはずです:
```
Running 1 tests
blog_os::trivial_assertion... [ok]
```
いま、関数名には関数までのフルパスが含まれていますが、これは異なるモジュールのテスト関数が同じ名前を持っているときに便利です。それ以外の点において出力は前と同じですが、もう手動でテストにprint文を付け加える必要はありません。
## VGAバッファをテストする
私達のテストフレームワークがうまく動くようになったので、私達のVGAバッファに関する実装のテストをいくつか作ってみましょう。まず、`println`がパニックすることなく成功することを確かめる非常に単純なテストを作ります:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_simple() {
println!("test_println_simple output");
}
```
このテストは、適当な文字列をVGAバッファにただ出力するだけです。このテストがパニックすることなく終了したなら、`println`の呼び出しもまたパニックしなかったということです。
たくさんの行が出力され、行がスクリーンから押し出されたとしてもパニックが起きないことを確かめるために、もう一つテストを作ってみましょう:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_many() {
for _ in 0..200 {
println!("test_println_many output");
}
}
```
出力された行が本当に画面に映っているのかを確かめるテスト関数も作ることができます:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_output() {
let s = "Some test string that fits on a single line";
println!("{}", s);
for (i, c) in s.chars().enumerate() {
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
}
```
この関数はテスト用文字列を定義し、`println`を使って出力し、静的な`WRITER`――VGAテキストバッファを表現しています――上の表示文字を走査しています。`println`は最後に出力された行につづけて出力し、即座に改行するので、`BUFFER_HEIGHT - 2`行目にこの文字列は現れるはずです。
[`enumerate`]を使うことで、変数`i`によって反復の回数を数え、これを`c`に対応する画面上の文字を読み込むのに使っています。画面の文字の`ascii_character`を`c`と比較することで、文字列のそれぞれの文字がVGAテキストバッファに確実に現れていることを確かめることができます。
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
ご想像の通り、もっとたくさんテストを作っても良いです。例えば、非常に長い行を出力しても、うまく折り返され、パニックしないことをテストする関数や、改行・出力不可能な文字・非ユニコード文字などが適切に処理されることを確かめるような関数を作ることもできます。
ですが、この記事の残りでは、 **結合テスト** を作って、異なる構成要素の相互作用をテストする方法を説明しましょう。
## 結合テスト
Rustにおける[結合テスト][integration tests]では、慣習としてプロジェクトのルートにおいた`tests`ディレクトリ (つまり`src`ディレクトリと同じ階層ですね) にテストプログラムを入れます。標準のテストフレームワークも、独自のテストフレームワークも、自動的にこのディレクトリにあるすべてのテストを実行します。
[integration tests]: https://doc.rust-jp.rs/book-ja/ch11-03-test-organization.html#結合テスト
すべての結合テストは、独自の実行可能ファイルを持っており、私達の`main.rs`とは完全に独立しています。つまり、それぞれのテストに独自のエントリポイント関数を定義しないといけないということです。どのような仕組みになっているのかを詳しく見るために、`basic_boot`という名前で試しに結合テストを作ってみましょう:
```rust
// in tests/basic_boot.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
#[unsafe(no_mangle)] // この関数の名前を変えない
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
fn test_runner(tests: &[&dyn Fn()]) {
unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
loop {}
}
```
結合テストは独立した実行ファイルであるので、クレート属性(`no_std`、`no_main`、`test_runner`など)をすべてもう一度与えないといけません。また、新しいエントリポイント関数`_start`も作らないといけません。これはテストエントリポイント関数`test_main`を呼び出します。結合テストの実行可能ファイルは、テストモードでないときはビルドされないので、`cfg(test)`属性は必要ありません。
今のところ、`test_runner`関数の中身として、常にパニックする[`unimplemented`]マクロを代わりに入れており、そして`panic`ハンドラにはただの`loop`を入れています。本当は、`serial_println`マクロと`exit_qemu`関数を使って、これらの関数を`main.rs`と全く同じように実装したいです。しかし問題は、テストが私達の`main.rs`実行ファイルとは完全に別にビルドされているので、これらの関数にアクセスすることができないということです。
[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html
この段階で`cargo test`を実行したら、パニックハンドラによってエンドレスループに入ってしまうでしょう。QEMUを終了するキーボードショートカットである`Ctrl+c`を使わないといけません。
### ライブラリを作る
結合テストに必要な関数を利用できるようにするために、`main.rs`からライブラリを分離してやる必要があります。こうすると、他のクレートや結合テスト実行ファイルがこれをインクルードできるようになります。これをするために、新しい`src/lib.rs`ファイルを作りましょう:
```rust
// src/lib.rs
#![no_std]
```
`main.rs`と同じく、`lib.rs`は自動的にcargoに認識される特別なファイルです。ライブラリは別のコンパイル単位なので、`#![no_std]`属性を再び指定する必要があります。
`cargo test`がライブラリにも使えるようにするために、テストのための関数や属性を`main.rs`から`lib.rs`へと移す必要もあります。
```rust
// in src/lib.rs
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub trait Testable {
fn run(&self) -> ();
}
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run();
}
exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
/// `cargo test`のときのエントリポイント
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
test_panic_handler(info)
}
```
`test_runner`を(`main.rs`の)実行可能ファイルと結合テストの両方から利用可能にするために、`cfg(test)`属性をこれに適用せず、また、publicにします。パニックハンドラの実装もpublicな`test_panic_handler`関数へと分離することで、実行可能ファイルからも使えるようにしています。
`lib.rs`は`main.rs`とは独立にコンパイルされるので、ライブラリがテストモードでコンパイルされるときは`_start`エントリポイントとパニックハンドラを追加する必要があります。このような場合、[`cfg_attr`]クレート属性を使うことで、`no_main`属性を条件付きで有効化することができます。
[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
`QemuExitCode`enumと`exit_qemu`関数も移動し、publicにします:
```rust
// in src/lib.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
これで、実行ファイルも結合テストもこれらの関数をライブラリからインポートでき、自前の実装を定義する必要はありません。`println`と`serial_println`も利用可能にするために、モジュールの宣言も移動させましょう:
```rust
// in src/lib.rs
pub mod serial;
pub mod vga_buffer;
```
モジュールをpublicにすることで、ライブラリの外からも使えるようにしています。`println`と`serial_println`マクロは、これらのモジュールの`_print`関数を使っているため、これらのマクロを使うためにも、この変更は必要です。
では、`main.rs`をこのライブラリを使うように更新しましょう:
```rust
// src/main.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
use blog_os::println;
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
/// この関数はパニック時に呼ばれる。
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
ライブラリは通常の外部クレートと同じように使うことができます。名前は、私達のクレート名――今回なら`blog_os`――になります。上のコードでは、`blog_os::test_runner`関数を`test_runner`属性で、`blog_os::test_panic_handler`関数を`cfg(test)`のパニックハンドラで使っています。また、`println`マクロをインポートすることで、`_start`と`panic`関数で使えるようにもしています。
この時点で、`cargo run`と`cargo test`は再びうまく実行できるようになっているはずです。もちろん、`cargo test`は依然エンドレスループするはずですが(`ctrl+c`で終了できます)。結合テストに必要な関数を使ってこれを修正しましょう。
### 結合テストを完成させる
`src/main.rs`と同じように、`tests/basic_boot.rs`実行ファイルは新しいライブラリから型をインポートできます。これで、テストを完成させるのに足りない要素をインポートすることができます。
```rust
// in tests/basic_boot.rs
#![test_runner(blog_os::test_runner)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
テストランナーを再実装することはせず、ライブラリの`test_runner`関数を使います。`panic`ハンドラとしては、`main.rs`でやったように`blog_os::test_panic_handler`関数を呼びます。
これで、`cargo test`は再び通常通り終了するはずです。実行すると、`lib.rs`、`main.rs`、そして`basic_boot.rs`を順にそれぞれビルドし、テストを実行するのが見えるはずです。`main.rs`と`basic_boot`結合テストに関しては、これらには`#[test_case]`のつけられた関数はないため、"Running 0 tests"と報告されるはずです。
これで、`basic_boot.rs`にテストを追加していくことができます。例えば、`println`がパニックすることなくうまく行くことを、VGAバッファのときのようにテストすることができます:
```rust
// in tests/basic_boot.rs
use blog_os::println;
#[test_case]
fn test_println() {
println!("test_println output");
}
```
`cargo test`を実行すると、テスト関数を見つけ出して実行しているのがわかるでしょう。
このテストは、VGAバッファのテストとほとんど同じであるため、今のところあまり意味がないように思われるかもしれません。しかし、将来的に`main.rs`の`_start`関数と`lib.rs`はどんどん大きくなり、`test_main`関数を実行する前に様々な初期化ルーチンを呼ぶようになるかもしれないので、これらの2つのテストは全然違う環境で実行されるようになるかもしれないのです。
`println`を`basic_boot`環境で(`_start`で初期化ルーチンを一切呼ぶことなく)テストすることにより、起動の直後に`println`が使えることが保証されます。私達は、例えばパニックメッセージの出力などを`println`に依存しているので、これは重要です。
### 今後のテスト
結合テストの魅力は、これらが完全に独立した実行ファイルとして扱われることです。これにより、実行環境を完全にコントロールすることができるので、コードがCPUやハードウェアデバイスと正しく相互作用していることをテストすることができるのです。
`basic_boot`テストは結合テストの非常に簡単な例でした。今後、私達のカーネルは機能がより豊富になり、そして様々な方法でハードウェアと相互作用するようになります。結合テストを追加することにより、それらの相互作用が期待通り動く(また、期待通り動きつづけている)ことを確かめることができるのです。今後追加できるテストの例としては、以下があります:
- **CPU例外**: プログラムが不正な操作(例えばゼロで割るなど)を行った場合、CPUは例外を投げます(訳注:例外を発することを、英語でthrow an exceptionというのにちなんで、慣例的に「投げる」と表現します)。カーネルはそのような例外に対するハンドラ関数を登録しておくことができます。結合テストで、CPU例外が起こったときに、例外ハンドラが呼ばれていることや、例外が解決可能だった場合に実行が継続することを確かめることができるでしょう。
- **ページテーブル**: ページテーブルは、どのメモリ領域が有効でアクセスできるかを定義しています。例えばプログラムを立ち上げるとき、このページテーブルを変更することで、新しいメモリ領域を割り当てることが可能です。結合テストで、ページテーブルに`_start`関数内で何らかの変更を施して、その変更が期待通りの効果を起こしているかを`#[test_case]`関数で確かめることができるでしょう。
- **ユーザー空間プログラム**: ユーザー空間プログラムは、システムの資源に限られたアクセスしか持たないプログラムのことです。これらは例えば、カーネルのデータ構造や、他のプログラムのメモリにアクセスすることはできません。結合テストで、禁止された操作を実行するようなユーザー空間プログラムを起動し、カーネルがそれらをすべて防ぐことを確かめることができるでしょう。
ご想像のとおり、もっと多くのテストが可能です。このようなテストを追加することで、カーネルに新しい機能を追加したときや、コードをリファクタリングしたときに、これらを壊してしまっていないことを保証できます。これは、私達のカーネルがより大きく、より複雑になったときに特に重要になります。
### パニックしなければならないテスト
標準ライブラリのテストフレームワークは、[`#[should_panic]`属性][should_panic]をサポートしています。これを使うと、失敗しなければならないテストを作ることができます。これは、例えば、関数が無効な引数を渡されたときに失敗することを確かめる場合などに便利です。残念なことに、この機能は標準ライブラリのサポートを必要とするため、`#[no_std]`クレートではこの属性はサポートされていません。
[should_panic]: https://doc.rust-jp.rs/rust-by-example-ja/testing/unit_testing.html#パニックをテストする
`#[should_panic]`属性は使えませんが、パニックハンドラから成功のエラーコードで終了するような結合テストを作れば、似たような動きをさせることはできます。そのようなテストを`should_panic`という名前で作ってみましょう:
```rust
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{QemuExitCode, exit_qemu, serial_println};
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
これは`_start`関数や、独自テストランナー属性などをまだ定義していないので未完成です。足りない部分を追加しましょう:
```rust
// in tests/should_panic.rs
#![feature(custom_test_frameworks)]
#![test_runner(test_runner)]
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
pub fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
}
exit_qemu(QemuExitCode::Success);
}
```
このテストは、`lib.rs`の`test_runner`を使い回さず、自前の、テストがパニックせずリターンしたときに失敗の終了コードを出すような`test_runner`関数を定義しています(私達はテストにパニックしてほしいわけですから)。もしテスト関数が一つも定義されていなければ、このランナーは成功のエラーコードで終了します。ランナーは一つテストを実行したら必ず終了するので、1つ以上の`#[test_case]`関数を定義しても意味はありません。
では、失敗するはずのテストを追加してみましょう:
```rust
// in tests/should_panic.rs
use blog_os::serial_print;
#[test_case]
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
```
このテストは`assert_eq`を使って`0`と`1`が等しいことをアサートしています。これはもちろん失敗するので、私達のテストは望み通りパニックします。ここで、`Testable`トレイトは使っていないので、関数名は`serial_print!`を使って自分で出力しないといけないことに注意してください。
`cargo test --test should_panic`を使ってテストすると、テストが期待通りパニックし、成功したことがわかるでしょう。アサーションをコメントアウトしテストをもう一度実行すると、"test did not panic"というメッセージとともに、テストが確かに失敗することがわかります。
この方法の無視できない欠点は、テスト関数を一つしか使えないことです。`#[test_case]`関数が複数ある場合、パニックハンドラが呼び出された後で(プログラムの)実行を続けることはできないので、最初の関数のみが実行されます。この問題を解決するいい方法を私は知らないので、もしなにかアイデアがあったら教えてください!
### ハーネスのないテスト
**訳注:** ハーネスとは、もともとは馬具の一種を意味する言葉です。転じて「制御する道具」一般を指し、また[テストハーネス](https://en.wikipedia.org/wiki/Test_harness)というと(`test_runner`のように)複数のテストケースを処理し、その振る舞い・出力などを適切に処理・整形してくれるプログラムのことを指します。
(私達の`should_panic`テストのように)一つしかテスト関数を持たない結合テストでは、テストランナーは必ずしも必要というわけではありません。このような場合、テストランナーは完全に無効化してしまって、`_start`関数からテストを直接実行することができます。
このためには、`Cargo.toml`でこのテストの`harness`フラグを無効化することがカギとなります。これは、結合テストにテストランナーが使われるかを定義しています。これが`false`に設定されると、標準のテストランナーと独自のテストランナーの両方が無効化され、通常の実行ファイルのように扱われるようになります。
`should_panic`テストの`harness`フラグを無効化してみましょう:
```toml
# in Cargo.toml
[[test]]
name = "should_panic"
harness = false
```
これで、テストランナーに関係するコードを取り除いて、`should_panic`テストを大幅に簡略化することができます。結果として以下のようになります:
```rust
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
should_fail();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
loop{}
}
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
`should_fail`関数を`_start`関数から直接呼び出して、もしリターンしたら失敗の終了コードで終了するようにしました。今`cargo test --test should_panic`を実行しても、以前と全く同じように振る舞います。
`should_panic`なテストを作るとき以外にも`harness`属性は有用なことがあります。例えば、それぞれのテスト関数が副作用を持っており、指定された順番で実行されないといけないときなどです。
## まとめ
テストは、ある要素が望み通りの振る舞いをしていることを保証するのにとても便利なテクニックです。バグが存在しないことを証明することはできないとはいえ、バグを発見したり、特にリグレッションを防ぐのに便利な方法であることは間違いありません。
この記事では、私達のRust製カーネルでテストフレームワークを組み立てる方法を説明しました。Rustの独自テストフレームワーク機能を使って、私達のベアメタル環境における、シンプルな`#[test_case]`属性のサポートを実装しました。私達のテストランナーは、QEMUの`isa-debug-exit`デバイスを使うことで、QEMUをテスト実行後に終了し、テストステータスを報告することができます。エラーメッセージを、VGAバッファの代わりにコンソールに出力するために、シリアルポートの単純なドライバを作りました。
`println`マクロのテストをいくつか作った後、記事の後半では結合テストについて見ました。結合テストは`tests`ディレクトリに置かれ、完全に独立した実行ファイルとして扱われることを学びました。結合テストから`exit_qemu`関数と`serial_println`マクロにアクセスできるようにするために、コードのほとんどをライブラリに移し、すべての実行ファイルと結合テストがインポートできるようにしました。結合テストはそれぞれ独自の環境で実行されるため、ハードウェアとの相互作用や、パニックするべきテストを作るといったことが可能になります。
QEMU内で現実に近い環境で実行できるテストフレームワークを手に入れました。今後の記事でより多くのテストを作っていくことで、カーネルがより複雑になってもメンテナンスし続けられるでしょう。
## 次は?
次の記事では、**CPU例外**を見ていきます。この例外というのは、CPUによってなにか「不法行為」――例えば、ゼロ除算やマップされていないメモリページへのアクセス(いわゆる「ページフォルト」)――が行われたときに投げられます。これらの例外を捕捉してテストできるようにしておくことは、将来エラーをデバッグするときに非常に重要です。例外の処理はまた、キーボードをサポートするのに必要になる、ハードウェア割り込みの処理に非常に似てもいます。
================================================
FILE: blog/content/edition-2/posts/04-testing/index.ko.md
================================================
+++
title = "커널을 위한 테스트 작성 및 실행하기"
weight = 4
path = "ko/testing"
date = 2019-04-27
[extra]
# Please update this when updating the translation
translation_based_on_commit = "1c9b5edd6a5a667e282ca56d6103d3ff1fd7cfcb"
# GitHub usernames of the people that translated this post
translators = ["JOE1994"]
# GitHub usernames of the people that contributed to this translation
translation_contributors = ["SNOOPYOF", "dalinaum"]
+++
이 글에서는 `no_std` 실행파일에 대한 유닛 테스트 및 통합 테스트 과정을 다룰 것입니다. Rust에서 지원하는 커스텀 테스트 프레임워크 기능을 이용해 우리가 작성한 커널 안에서 테스트 함수들을 실행할 것입니다. 그 후 테스트 결과를 QEMU 밖으로 가져오기 위해 QEMU 및 `bootimage` 도구가 제공하는 여러 기능들을 사용할 것입니다.
이 블로그는 [GitHub 저장소][GitHub]에서 오픈 소스로 개발되고 있으니, 문제나 문의사항이 있다면 저장소의 'Issue' 기능을 이용해 제보해주세요. [페이지 맨 아래][at the bottom]에 댓글을 남기실 수도 있습니다. 이 글과 관련된 모든 소스 코드는 저장소의 [`post-04 브랜치`][post branch]에서 확인하실 수 있습니다.
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-04
## 전제 조건
이 글은 이전에 작성된 글들 [_Unit Testing_]과 [_Integration Tests_]를 대체합니다 (예전에 작성된 이 두 포스트의 내용은 오래전 내용이라 현재는 더 이상 유효하지 않습니다). 이 글은 독자가 2019년 4월 27일 이후에 글 [_A Minimal Rust Kernel_]을 읽고 따라 실습해봤다는 가정하에 작성했습니다. 독자는 해당 포스트에서 작성했던 파일 ` .cargo/config.toml`을 가지고 있어야 합니다. 이 파일은 [컴파일 대상 환경을 설정][sets a default target]하고 [프로그램 실행 시작을 담당하는 실행 파일을 정의][defines a runner executable]합니다.
[_Unit Testing_]: @/edition-2/posts/deprecated/04-unit-testing/index.md
[_Integration Tests_]: @/edition-2/posts/deprecated/05-integration-tests/index.md
[_A Minimal Rust Kernel_]: @/edition-2/posts/02-minimal-rust-kernel/index.md
[sets a default target]: @/edition-2/posts/02-minimal-rust-kernel/index.md#set-a-default-target
[defines a runner executable]: @/edition-2/posts/02-minimal-rust-kernel/index.md#using-cargo-run
## Rust 프로그램 테스트하기
Rust 언어에 [내장된 자체 테스트 프레임워크][built-in test framework]를 사용하면 복잡한 초기 설정 과정 없이 유닛 테스트들을 실행할 수 있습니다. 작성한 함수에 가정 설정문 (assertion check)들을 삽입한 후, 함수 선언 바로 앞에 `#[test]` 속성을 추가하기만 하면 됩니다. 그 후에 `cargo test` 명령어를 실행하면 `cargo`가 자동으로 크레이트의 모든 테스트 함수들을 발견하고 실행합니다.
[built-in test framework]: https://doc.rust-lang.org/book/ch11-00-testing.html
안타깝게도 우리의 커널처럼 `no_std` 환경에서 구동할 프로그램은 Rust가 기본으로 제공하는 테스트 프레임워크를 이용하기 어렵습니다. Rust의 테스트 프레임워크는 기본적으로 언어에 내장된 [`test`] 라이브러리를 사용하는데, 이 라이브러리는 Rust 표준 라이브러리를 이용합니다. 우리의 `#no_std` 커널을 테스트할 때는 Rust의 기본 테스트 프레임워크를 사용할 수 없습니다.
[`test`]: https://doc.rust-lang.org/test/index.html
프로젝트 디렉터리 안에서 `cargo test` 명령어를 실행하면 아래와 같은 오류가 발생합니다:
```
> cargo test
Compiling blog_os v0.1.0 (/…/blog_os)
error[E0463]: can't find crate for `test`
```
`test` 크레이트가 표준 라이브러리에 의존하기에, 베어메탈 환경에서는 이 크레이트를 이용할 수 없습니다. `test` 크레이트를 `#[no_std]` 환경에서 이용할 수 있게 포팅(porting)하는 것이 [불가능한 것은 아니지만][utest], 일단 `test` 크레이트의 구현 변경이 잦아서 불안정하며 포팅 시 `panic` 매크로를 재정의하는 등 잡다하게 신경 써야 할 것들이 존재합니다.
[utest]: https://github.com/japaric/utest
### 커스텀 테스트 프레임워크
다행히 Rust의 [`custom_test_frameworks`] 기능을 이용하면 Rust의 기본 테스트 프레임워크 대신 다른 것을 사용할 수 있습니다. 이 기능은 외부 라이브러리가 필요하지 않기에 `#[no_std]` 환경에서도 사용할 수 있습니다.
이 기능은 `#[test case]` 속성이 적용된 함수들을 모두 리스트에 모은 후에 사용자가 작성한 테스트 실행 함수에 전달하는 방식으로 작동합니다. 따라서 사용자가 작성한 테스트 실행 함수 단에서 테스트 실행 과정을 전적으로 제어할 수 있습니다.
[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html
기본 테스트 프레임워크와 비교했을 때의 단점은 [`should_panic` 테스트][`should_panic` tests]와 같은 고급 기능이 준비되어 있지 않다는 것입니다. 베어메탈 환경에서는 Rust의 기본 테스트 프레임워크가 제공하는 고급 기능들이 지원되지 않기에, 이 중 필요한 것이 있다면 우리가 직접 코드로 구현해야 합니다. 예를 들어 `#[should_panic]` 속성은 스택 되감기를 사용해 패닉을 잡아내는데, 우리의 커널에서는 스택 되감기가 해제되어 있어 사용할 수 없습니다.
[`should_panic` tests]: https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic
커널 테스트용 테스트 프레임워크 작성의 첫 단계로 아래의 코드를 `main.rs`에 추가합니다:
```rust
// in src/main.rs
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
}
```
`test_runner`는 짧은 디버그 메시지를 출력한 후 주어진 리스트의 각 테스트 함수를 호출합니다. 인자 타입 `&[& dyn Fn()]`은 [_Fn()_] 트레이트를 구현하는 타입에 대한 레퍼런스들의 [_slice_]입니다. 좀 더 쉽게 말하면 이것은 함수처럼 호출될 수 있는 타입에 대한 레퍼런스들의 리스트입니다. `test_runner` 함수는 테스트 용도 외에 쓸모가 없기에 `#[cfg(test)]` 속성을 적용하여 테스트 시에만 빌드합니다.
[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html
[_trait object_]: https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html
[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html
`cargo test`를 다시 시도하면 실행이 성공할 것입니다 (실행이 실패한다면 아래의 노트를 확인해주세요). 하지만 "Hello World" 메시지만 출력될 뿐 `test_runner`로부터의 메시지는 출력되지 않는데, 아직 `_start` 함수를 프로그램 실행 시작 함수로 이용하고 있기 때문입니다. 우리가 `#[no_main]` 속성을 통해 별도의 실행 시작 함수를 사용하고 있기에, 커스텀 테스트 프레임워크가 `test_runner`를 호출하려고 생성한 `main`함수가 이용되지 않고 있습니다.
**각주:** 특정 상황에서 `cargo test` 실행 시 "duplicate lang item" 오류가 발생하는 버그가 존재합니다. `Cargo.toml`에 `panic = "abort"` 설정이 있으면 해당 오류가 발생할 수 있습니다. 해당 설정을 제거하면 `cargo test` 실행 시 오류가 발생하지 않을 것입니다. 더 자세한 정보는 [해당 버그에 대한 깃헙 이슈](https://github.com/rust-lang/cargo/issues/7359)를 참조해주세요.
이 문제를 해결하려면 우선 `reexport_test_harness_main` 속성을 사용해 테스트 프레임워크가 생성하는 함수의 이름을 `main` 이외의 이름으로 변경해야 합니다. 그 후에 `_start` 함수로부터 이름이 변경된 이 함수를 호출할 것입니다.
```rust
// in src/main.rs
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
```
테스트 프레임워크의 시작 함수를 `test_main`으로 설정하고, 커널 시작 함수 `_start`에서 `test_main` 함수를 호출합니다. `test_main` 함수는 테스트 상황이 아니면 생성되지 않기 때문에, [조건부 컴파일][conditional compilation]을 통해 테스트 상황에서만 `test_main` 함수를 호출하도록 합니다.
`cargo test` 명령어를 실행하면 "Running 0 tests"라는 메시지가 출력됩니다. 이제 첫 번째 테스트 함수를 작성할 준비가 되었습니다.
```rust
// in src/main.rs
#[test_case]
fn trivial_assertion() {
print!("trivial assertion... ");
assert_eq!(1, 1);
println!("[ok]");
}
```
위의 테스트 함수를 작성한 뒤 다시 `cargo test`를 실행하면 아래의 내용이 출력됩니다:
![QEMU printing "Hello World!", "Running 1 tests", and "trivial assertion... [ok]"](qemu-test-runner-output.png)
`test_runner` 함수에 인자로 전달되는 `tests` 슬라이스에 `trivial_assertion` 함수에 대한 레퍼런스가 들어 있습니다.
출력 메시지 `trivial assertion... [ok]`를 통해 테스트가 성공적으로 실행되었음을 확인할 수 있습니다.
테스트 실행 완료 후 `test_runner` 함수가 반환되어 제어 흐름이 `test_main` 함수로 돌아오고, 다시 이 함수가 반환되어 `_start` 함수로 제어 흐름이 돌아갑니다. 실행 시작 함수는 반환할 수 없기에 `_start` 함수의 맨 끝에서 무한 루프에 진입하는데, `cargo test`의 실행 완료 후 종료하기를 바라는 우리의 입장에서는 해결해야 할 문제입니다.
## QEMU 종료하기
`_start` 함수의 맨 뒤에 무한루프가 있어 `cargo test`의 실행을 종료하려면 실행 중인 QEMU를 수동으로 종료해야 합니다. 이 때문에 각종 명령어 스크립트에서 사람의 개입 없이는 `cargo test`를 사용할 수 없습니다. 이 불편을 해소하는 가장 직관적인 방법은 정식으로 운영체제를 종료하는 기능을 구현하는 것입니다. 하지만 이를 구현하려면 [APM] 또는 [ACPI] 전원 제어 표준을 지원하도록 커널 코드를 짜야 해서 제법 복잡한 작업이 될 것입니다.
[APM]: https://wiki.osdev.org/APM
[ACPI]: https://wiki.osdev.org/ACPI
다행히 이 불편을 해결할 차선책이 존재합니다: QEMU가 지원하는 `isa-debug-exit` 장치를 사용하면 게스트 시스템에서 쉽게 QEMU를 종료할 수 있습니다. QEMU 실행 시 `-device` 인자를 전달하여 이 장치를 활성화할 수 있습니다. `Cargo.toml`에 `package.metadata.bootimage.test-args`라는 설정 키 값을 추가하여 QEMU에 `device` 인자를 전달합니다:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
```
`bootimage runner`는 테스트 실행 파일을 실행할 때 QEMU 실행 명령어의 마지막에 `test-args`를 추가합니다. `cargo run` 실행의 경우에는 QEMU 실행 명령어 끝에 `test-args`를 추가하지 않습니다.
장치 이름(`isa-debug-exit`)과 함께 두 개의 인자 `iobase`와 `iosize`를 전달하는데, 이 두 인자는 우리의 커널이 어떤 _입출력 포트_ 를 이용해 `isa-debug-exit` 장치에 접근할 수 있는지 알립니다.
### 입출력 포트
x86 CPU와 주변 장치가 데이터를 주고받는 입출력 방법은 두 가지가 있습니다. 하나는 **메모리 맵 입출력(memory-mapped I/O)**이고 다른 하나는 **포트 맵 입출력(port-mapped I/O)**입니다. 예전에 우리는 메모리 맵 입출력을 이용해 [VGA 텍스트 버퍼][VGA text buffer]를 메모리 주소 `0xb8000`에 매핑하여 접근했었습니다. 이 주소는 RAM에 매핑되는 대신 VGA 장치의 메모리에 매핑됩니다.
[VGA text buffer]: @/edition-2/posts/03-vga-text-buffer/index.md
반면 포트 맵 입출력은 별도의 입출력 버스를 이용해 장치 간 통신을 합니다. CPU에 연결된 주변장치 당 1개 이상의 포트 번호가 배정됩니다. CPU 명령어 `in`과 `out`은 포트 번호와 1바이트의 데이터를 인자로 받습니다. CPU는 이 명령어들을 이용해 입출력 포트와 데이터를 주고받습니다 (`in`/`out`이 변형된 버전의 명령어로 `u16` 혹은 `u32` 단위로 데이터를 주고받을 수도 있습니다).
`isa-debug-exit` 장치는 port-mapped I/O 를 사용합니다. `iobase` 인자는 이 장치를 어느 포트에 연결할지 정합니다 (`0xf4`는 [x86 시스템의 입출력 버스 중 잘 안 쓰이는][list of x86 I/O ports] 포트입니다). `iosize` 인자는 포트의 크기를 정합니다 (`0x04`는 4 바이트 크기를 나타냅니다).
[list of x86 I/O ports]: https://wiki.osdev.org/I/O_Ports#The_list
### 종료 장치 사용하기
`isa-debug-exit` 장치가 하는 일은 매우 간단합니다. `iobase`가 가리키는 입출력 포트에 값 `value`가 쓰였을 때, 이 장치는 QEMU가 [종료 상태][exit status] `(value << 1) | 1`을 반환하며 종료하도록 합니다. 따라서 우리가 입출력 포트에 값 `0`을 보내면 QEMU가 `(0 << 1) | 1 = 1`의 종료 상태 코드를 반환하고, 값 `1`을 보내면 `(1 << 1) | 1 = 3`의 종료 상태 코드를 반환합니다.
[exit status]: https://en.wikipedia.org/wiki/Exit_status
`x86` 명령어 `in` 및 `out`을 사용하는 어셈블리 코드를 직접 작성하는 대신 `x86_64` 크레이트가 제공하는 추상화된 API를 사용할 것입니다. `Cargo.toml`의 `dependencies` 목록에 `x86_64` 크레이트를 추가합니다:
[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/
```toml
# in Cargo.toml
[dependencies]
x86_64 = "0.14.2"
```
`x86_64` 크레이트가 제공하는 [`Port`] 타입을 사용해 아래처럼 `exit_qemu` 함수를 작성합니다:
[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html
```rust
// in src/main.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
이 함수는 새로운 [`Port`]를 주소 `0xf4`(`isa-debug-exit` 장치의 `iobase`)에 생성합니다. 그다음 인자로 받은 종료 상태 코드를 포트로 전달합니다. 여기서 `u32` 타입을 사용하는 이유는 앞에서 우리가 `isa-debug-exit` 장치의 `iosize`를 4 바이트로 설정했기 때문입니다. 입출력 포트에 값을 쓰는 것은 잘못하면 프로그램이 예상치 못한 행동을 보일 수 있어 위험하다고 간주합니다. 따라서 이 함수가 처리하는 두 작업 모두 `unsafe` 블록 안에 배치해야 합니다.
`QemuExitCode` enum 타입을 이용하여 프로그램 종료 상태를 표현합니다. 모든 테스트가 성공적으로 실행되었다면 "성공" 종료 코드를 반환하고 그렇지 않았다면 "실패" 종료 코드를 반환하도록 구현할 것입니다. enum에는 `#[repr(u32)]` 속성이 적용하여 enum의 각 분류 값은 `u32` 타입의 값으로 표현됩니다. `0x10`을 성공 종료 코드로 사용하고 `0x11`을 실패 종료 코드로 사용할 것입니다. QEMU가 이미 사용 중인 종료 코드와 중복되지만 않는다면, 어떤 값을 성공/실패 종료 코드로 사용하는지는 크게 중요하지 않습니다. `0`을 성공 종료 코드로 사용하는 것은 바람직하지 않은데, 그 이유는 종료 코드 변환 결과인 `(0 << 1) | 1 = 1`의 값이 QEMU가 실행 실패 시 반환하는 코드와 동일하기 때문입니다. 이 경우 종료 코드만으로는 QEMU가 실행을 실패한 것인지 모든 테스트가 성공적으로 실행된 것인지 구분하기 어렵습니다.
이제 `test_runner` 함수를 수정하여 모든 테스트 실행 완료 시 QEMU가 종료하도록 합니다.
```rust
// in src/main.rs
fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
/// new
exit_qemu(QemuExitCode::Success);
}
```
`cargo test`를 다시 실행하면 테스트 실행 완료 직후에 QEMU가 종료되는 것을 확인할 수 있습니다.
여기서 문제는 우리가 `Success` 종료 코드를 전달했는데도 불구하고 `cargo test`는 테스트들이 전부 실패했다고 인식한다는 것입니다.
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be
Building bootloader
Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader)
Finished release [optimized + debuginfo] target(s) in 1.07s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4,
iosize=0x04`
error: test failed, to rerun pass '--bin blog_os'
```
`cargo test`는 `0` 이외의 모든 에러 코드 값을 보면 실행이 실패했다고 간주합니다.
### 실행 성공 시 종료 코드
`bootimage` 도구의 설정 키 `test-success-exit-code`를 이용하면 특정 종료 코드가 종료 코드 `0`처럼 취급되도록 할 수 있습니다.
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = […]
test-success-exit-code = 33 # (0x10 << 1) | 1
```
이 설정을 이용하면 우리가 반환하는 성공 종료 코드를 `bootimage` 도구가 종료 코드 0으로 변환합니다. 이제 `cargo test`는 테스트 실행이 성공했다고 인식합니다.
test_runner는 이제 테스트 결과를 출력한 후 QEMU를 자동으로 종료합니다. QEMU 창이 매우 짧은 시간 동안만 떠 있기에 QEMU 창에 출력된 테스트 결과를 제대로 읽기 어렵습니다. QEMU 종료 후에도 충분한 시간을 갖고 테스트 결과를 읽을 수 있으려면 테스트 결과가 콘솔에 출력되는 편이 나을 것입니다.
## 콘솔에 출력하기
테스트 결과를 콘솔에서 확인하려면 우리의 커널에서 호스트 시스템으로 출력 결과 데이터를 전송해야 합니다. 이것을 달성하는 방법은 여러 가지 있습니다. 한 방법은 TCP 네트워크 통신을 이용해 데이터를 전달하는 것입니다. 하지만 네트워크 통신 스택을 구현하는 것은 상당히 복잡하기에, 우리는 좀 더 간단한 해결책을 이용할 것입니다.
### 직렬 포트 (Serial Port)
데이터를 전송하는 쉬운 방법 중 하나는 바로 [직렬 포트 (serial port)][serial port]를 이용하는 것입니다. 직렬 포트 하드웨어는 근대의 컴퓨터들에서는 찾아보기 어렵습니다. 하지만 직렬 포트의 기능 자체는 소프트웨어로 쉽게 구현할 수 있으며, 직렬 통신을 통해 우리의 커널에서 QEMU로 전송한 데이터를 다시 QEMU에서 호스트 시스템의 표준 출력 및 파일로 재전달할 수 있습니다.
[serial port]: https://en.wikipedia.org/wiki/Serial_port
직렬 통신을 구현하는 칩을 [UART][UARTs]라고 부릅니다. x86에서 사용할 수 있는 [다양한 종류의 UART 구현 모델들][lots of UART models]이 존재하며, 다양한 구현 모델들 간 차이는 우리가 쓰지 않을 고급 기능 사용 시에만 유의미합니다. 우리의 테스트 프레임워크에서는 대부분의 UART 구현 모델들과 호환되는 [16550 UART] 모델을 이용할 것입니다.
[UARTs]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
[lots of UART models]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#Models
[16550 UART]: https://en.wikipedia.org/wiki/16550_UART
[`uart_16550`] 크레이트를 이용해 UART 초기 설정을 마친 후 직렬 포트를 통해 데이터를 전송할 것입니다. `Cargo.toml`과 `main.rs`에 아래의 내용을 추가하여 의존 크레이트를 추가합니다.
[`uart_16550`]: https://docs.rs/uart_16550
```toml
# in Cargo.toml
[dependencies]
uart_16550 = "0.2.0"
```
`uart_16550` 크레이트는 UART 레지스터를 나타내는 `SerialPort` 구조체 타입을 제공합니다. 이 구조체 타입의 인스턴스를 생성하기 위해 아래와 같이 새 모듈 `serial`을 작성합니다.
```rust
// in src/main.rs
mod serial;
```
```rust
// in src/serial.rs
use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;
lazy_static! {
pub static ref SERIAL1: Mutex = {
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
serial_port.init();
Mutex::new(serial_port)
};
}
```
[VGA 텍스트 버퍼][vga lazy-static]를 구현할 때와 마찬가지로 `lazy_static` 매크로와 스핀 락을 사용해 정적 변수 `SERIAL1`을 생성했습니다. `lazy_static`을 사용함으로써 `SERIAL1`이 최초로 사용되는 시점에 단 한 번만 `init` 함수가 호출됩니다.
`isa-debug-exit` 장치와 마찬가지로 UART 또한 포트 입출력을 통해 프로그래밍 됩니다. UART는 좀 더 복잡해서 장치의 레지스터 여러 개를 이용하기 위해 여러 개의 입출력 포트를 사용합니다. unsafe 함수 `SerialPort::new`는 첫 번째 입출력 포트의 주소를 인자로 받고 그것을 통해 필요한 모든 포트들의 주소들을 알아냅니다. 첫 번째 시리얼 통신 인터페이스의 표준 포트 번호인 `0x3F8`을 인자로 전달합니다.
[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
직렬 포트를 쉽게 사용할 수 있도록 `serial_print!` 및 `serial_println!` 매크로를 추가해줍니다.
```rust
// in src/serial.rs
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
use core::fmt::Write;
SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
}
/// Prints to the host through the serial interface.
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => {
$crate::serial::_print(format_args!($($arg)*));
};
}
/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
concat!($fmt, "\n"), $($arg)*));
}
```
구현은 이전 포스트에서 작성했던 `print` 및 `println` 매크로와 매우 유사합니다. `SerialPort` 타입은 이미 [`fmt::Write`] 트레이트를 구현하기에 우리가 새로 구현할 필요가 없습니다.
[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
이제 VGA 텍스트 버퍼가 아닌 직렬 통신 인터페이스로 메시지를 출력할 수 있습니다.
```rust
// in src/main.rs
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
[…]
}
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
`serial_println` 매크로에 `#[macro_export]` 속성을 적용하여 이제 이 매크로는 프로젝트 루트 네임스페이스에 배정되어 있습니다.
따라서 `use crate::serial::serial_println`을 이용해서는 해당 함수를 불러올 수 없습니다.
### QEMU로 전달해야 할 인자들
QEMU에서 직렬 통신 출력 내용을 확인하려면 QEMU에 `-serial` 인자를 전달하여 출력내용을 표준 출력으로 내보내야 합니다.
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio"
]
```
`cargo test` 실행 시 테스트 결과를 콘솔에서 바로 확인할 수 있습니다.
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [ok]
```
테스트 실패 시 여전히 출력 메시지가 QEMU에서 출력되는데, 그 이유는 패닉 핸들러가 `println`을 쓰고 있기 때문입니다.
테스트 `trivial_assertion` 내의 가정문을 `assert_eq!(0, 1)`로 변경하고 다시 실행하여 출력 결과를 확인해보세요.

다른 테스트 결과는 시리얼 포트를 통해 출력되지만, 패닉 메시지는 여전히 VGA 버퍼에 출력되고 있습니다. 패닉 메시지는 중요한 정보를 포함하기에 콘솔에서 다른 메시지들과 함께 볼 수 있는 편이 더 편리할 것입니다.
### 패닉 시 오류 메시지 출력하기
[조건부 컴파일][conditional compilation]을 통해 테스트 모드에서 다른 패닉 핸들러를 사용하도록 하면,
패닉 발생 시 콘솔에 에러 메시지를 출력한 후 QEMU를 종료시킬 수 있습니다.
[conditional compilation]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html
```rust
// in src/main.rs
// our existing panic handler
#[cfg(not(test))] // new attribute
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
// our panic handler in test mode
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
```
테스트용 패닉 핸들러에서는 `println` 대신 `serial_println`을 사용하고, QEMU는 실행 실패를 나타내는 종료 코드를 반환하면서 종료됩니다. 컴파일러는 `isa-debug-exit` 장치가 프로그램을 종료시킨다는 것을 알지 못하기에, `exit_qemu` 호출 이후의 무한 루프는 여전히 필요합니다.
이제 테스트 실패 시에도 QEMU가 종료되고 콘솔에 에러 메시지가 출력됩니다.
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [failed]
Error: panicked at 'assertion failed: `(left == right)`
left: `0`,
right: `1`', src/main.rs:65:5
```
이제 모든 테스트 결과 내용을 콘솔에서 확인할 수 있기에, 잠깐 생겼다가 사라지는 QEMU 윈도우 창은 더 이상 필요하지 않습니다. 이제 QEMU 창을 완전히 숨기는 방법에 대해 알아보겠습니다.
### QEMU 창 숨기기
우린 이제 `isa-debug-exit` 장치와 시리얼 포트를 통해 모든 테스트 결과를 보고하므로 더 이상 QEMU 윈도우 창이 필요하지 않습니다. `-display none` 인자를 QEMU에 전달하면 QEMU 윈도우 창을 숨길 수 있습니다:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio",
"-display", "none"
]
```
이제 QEMU는 완전히 백그라운드에서 동작합니다 (QEMU 윈도우 창이 생성되지 않습니다). 이제 우리의 테스트 프레임워크를 그래픽 사용자 인터페이스가 지원되지 않는 환경(CI 서비스 혹은 [SSH] 연결)에서도 구동할 수 있게 되었습니다.
[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
### 타임아웃
`cargo test`는 test_runner가 종료할 때까지 기다리기 때문에, 실행이 끝나지 않는 테스트가 있다면 test_runner와 `cargo test`는 영원히 종료되지 않을 수 있습니다. 일반적인 소프트웨어 개발 상황에서는 무한 루프를 방지하는 것이 어렵지 않습니다. 하지만 커널을 작성하는 경우에는 다양한 상황에서 무한 루프가 발생할 수 있습니다:
- 부트로더가 커널을 불러오는 것에 실패하는 경우, 시스템은 무한히 재부팅을 시도합니다.
- BIOS/UEFI 펌웨어가 부트로더를 불러오는 것에 실패하는 경우, 시스템은 무한히 재부팅을 시도합니다.
- QEMU의 `isa-debug-exit` 장치가 제대로 동작하지 않는 등의 이유로 제어 흐름이 우리가 구현한 함수들의 `loop {}`에 도착하는 경우.
- CPU 예외가 제대로 처리되지 않는 등의 이유로 하드웨어가 시스템 리셋을 일으키는 경우.
무한 루프가 발생할 수 있는 경우의 수가 너무 많기에 `bootimage` 도구는 각 테스트 실행에 5분의 시간 제한을 적용합니다. 제한 시간 안에 테스트 실행이 끝나지 않는다면 해당 테스트의 실행은 실패한 것으로 표기되며 "Timed Out"라는 오류 메시지가 콘솔에 출력됩니다. 덕분에 무한 루프에 갇힌 테스트가 있어도 `cargo test`의 실행이 무한히 지속되지는 않습니다.
`trivial_assertion` 테스트에 무한 루프 `loop {}`를 추가한 후 실행해보세요. `cargo test` 실행 시 5분 후에 해당 테스트가 시간 제한을 초과했다는 메시지가 출력될 것입니다. Cargo.toml의 `test-timeout` 키 값을 변경하여 [제한 시간을 조정][bootimage config]할 수도 있습니다:
[bootimage config]: https://github.com/rust-osdev/bootimage#configuration
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-timeout = 300 # (in seconds)
```
`trivial_assertion` 테스트가 타임아웃 되도록 5분 동안이나 기다리고 싶지 않다면 위의 `test-timeout` 값을 낮추세요.
### 자동으로 출력문 삽입하기
현재 `trivial_assertion` 테스트의 상태 정보는 `serial_print!`/`serial_println!` 매크로를 직접 입력해서 출력하고 있습니다.
```rust
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
새로운 테스트를 작성할 때마다 매번 출력문을 직접 입력하지 않아도 되도록 `test_runner`를 수정해보겠습니다. 아래와 같이 새로운 `Testable` 트레이트를 작성합니다.
```rust
// in src/main.rs
pub trait Testable {
fn run(&self) -> ();
}
```
[`Fn()` 트레이트][`Fn()` trait]를 구현하는 모든 타입 `T`에 대해 `Testable` 트레이트를 구현하는 것이 핵심입니다.
[`Fn()` trait]: https://doc.rust-lang.org/stable/core/ops/trait.Fn.html
```rust
// in src/main.rs
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
```
`run` 함수에서 먼저 [`any::type_name`] 함수를 이용해 테스트 함수의 이름을 출력합니다. 이 함수는 컴파일러 단에서 구현된 함수로, 주어진 타입의 이름을 문자열로 반환합니다. 함수 타입의 경우, 함수 이름이 곧 타입의 이름입니다. `\t` 문자는 [탭 문자][tab character]인데 `[ok]` 메시지를 출력 이전에 여백을 삽입합니다.
[`any::type_name`]: https://doc.rust-lang.org/stable/core/any/fn.type_name.html
[tab character]: https://en.wikipedia.org/wiki/Tab_character
함수명을 출력한 후 `self()`를 통해 테스트 함수를 호출합니다. `self`가 `Fn()` 트레이트를 구현한다는 조건을 걸어놨기 때문에 이것이 가능합니다. 테스트 함수가 반환된 후, `[ok]` 메시지를 출력하여 테스트 함수가 패닉하지 않았다는 것을 알립니다.
마지막으로 `test_runner`가 `Testable` 트레이트를 사용하도록 변경해줍니다.
```rust
// in src/main.rs
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run(); // new
}
exit_qemu(QemuExitCode::Success);
}
```
인자 `tests`의 타입을 `&[&dyn Fn()]`에서 `&[&dyn Testable]`로 변경했고, `test()` 대신 `test.run()`을 호출합니다.
이제 메시지가 자동으로 출력되기에 테스트 `trivial_assertion`에서 출력문들을 전부 지워줍니다.
```rust
// in src/main.rs
#[test_case]
fn trivial_assertion() {
assert_eq!(1, 1);
}
```
`cargo test` 실행 시 아래와 같은 출력 내용이 나타날 것입니다.
```
Running 1 tests
blog_os::trivial_assertion... [ok]
```
함수의 크레이트 네임스페이스 안에서의 전체 경로가 함수 이름으로 출력됩니다. 크레이트 내 다른 모듈들이 같은 이름의 테스트를 갖더라도 구분할 수 있습니다. 그 외에 출력 내용이 크게 달라진 것은 없고, 매번 print문을 직접 입력해야 하는 번거로움을 덜었습니다.
## VGA 버퍼 테스트 하기
제대로 작동하는 테스트 프레임워크를 갖췄으니, VGA 버퍼 구현을 테스트할 테스트들을 몇 개 작성해봅시다. 우선 아주 간단한 테스트를 통해 `println`이 패닉하지 않고 실행되는지 확인해봅시다.
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_simple() {
println!("test_println_simple output");
}
```
이 테스트는 VGA 버퍼에 간단한 메시지를 출력합니다. 이 테스트 함수가 패닉 없이 실행을 완료한다면 `println` 또한 패닉하지 않았다는 것을 확인할 수 있습니다.
여러 행이 출력되고 기존 행이 화면 밖으로 나가 지워지더라도 패닉이 일어나지 않는다는 것을 확인하기 위해 또다른 테스트를 작성합니다.
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_many() {
for _ in 0..200 {
println!("test_println_many output");
}
}
```
출력된 행들이 화면에 제대로 나타나는지 확인하는 테스트 또한 작성합니다.
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_output() {
let s = "Some test string that fits on a single line";
println!("{}", s);
for (i, c) in s.chars().enumerate() {
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
}
```
이 함수는 테스트 문자열을 정의하여 `println`을 통해 출력한 후, VGA 텍스트 버퍼를 나타내는 `WRITER`를 통해 화면에 출력된 문자들을 하나씩 순회합니다. `println`은 화면의 가장 아래 행에 문자열을 출력한 후 개행 문자를 추가하기 때문에 출력된 문자열은 VGA 버퍼의 `BUFFER_HEIGHT - 2` 번째 행에 저장되어 있습니다.
[`enumerate`]를 통해 문자열의 몇 번째 문자를 순회 중인지 변수 `i`에 기록하고, 변수 `c`로 `i`번째 문자에 접근합니다. screen_char의 `ascii_character`와 `c`를 비교하여 문자열 s의 각 문자가 실제로 VGA 텍스트 버퍼에 출력되었는지 점검합니다.
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
이 외에도 추가로 작성해볼 수 있는 테스트가 많이 있습니다. 아주 긴 문자열을 `println`을 통해 출력했을 때 패닉이 발생 안 하는지와 문자열이 화면 크기에 맞게 적절히 여러 행에 나누어져 제대로 출력되는지 확인하는 테스트를 작성해볼 수 있을 것입니다. 또한 개행 문자와 출력할 수 없는 문자 및 유니코드가 아닌 문자가 오류 없이 처리되는지 점검하는 테스트도 작성해볼 수 있을 것입니다.
이하 본문에서는 여러 컴포넌트들의 상호 작용을 테스트할 수 있는 _통합 테스트_ 를 어떻게 작성하는지 설명하겠습니다.
## 통합 테스트 (Integration Tests)
Rust에서는 [통합 테스트][integration tests]들을 프로젝트 루트에 `tests` 디렉터리를 만들어 저장하는 것이 관례입니다. Rust의 기본 테스트 프레임워크와 커스텀 테스트 프레임워크 모두 `tests` 디렉터리에 있는 테스트들을 자동으로 식별하고 실행합니다.
[integration tests]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
각 통합 테스트는 `main.rs`와 별개로 독립적인 실행 파일이기에, 실행 시작 함수를 별도로 지정해줘야 합니다.
예제 통합 테스트 `basic_boot`를 작성하면서 그 과정을 자세히 살펴봅시다:
```rust
// in tests/basic_boot.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
#[unsafe(no_mangle)] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
fn test_runner(tests: &[&dyn Fn()]) {
unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
loop {}
}
```
각 통합 테스트는 독립된 실행파일이기에 각각마다의 크레이트 속성(`no_std`, `no_main`, `test_runner` 등)들을 새로 설정해줘야 합니다. 테스트 시작 함수인 `test_main`을 호출할 실행 시작 함수 `_start` 또한 새로 만들어줘야 합니다. 통합 테스트는 테스트 모드가 아닌 이상 빌드되지 않기에 테스트 함수들에 `cfg(test)` 속성을 부여할 필요가 없습니다.
`test_runner` 함수에는 항상 패닉하는 [`unimplemented`] 매크로를 넣었고, 패닉 핸들러에는 무한 루프를 넣었습니다.
이 테스트 또한 `main.rs`에서 작성한 테스트와 동일하게 `serial_println` 매크로 및 `exit_qemu` 함수를 이용해 작성하면 좋겠지만, 해당 함수들은 별도의 컴파일 유닛인 `main.rs`에 정의되어 있기에 `basic_boot.rs`에서는 사용할 수 없습니다.
[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html
`cargo test` 명령어를 실행하면 패닉 핸들러 내의 무한 루프 때문에 실행이 끝나지 않습니다. 키보드에서 `Ctrl+c`를 입력해야 QEMU의 실행을 종료할 수 있습니다.
### 라이브러리 생성하기
`main.rs`에서 작성한 코드 일부를 따로 라이브러리 형태로 분리한 후, 통합 테스트에서 해당 라이브러리를 로드하여 필요한 함수들을 사용할 것입니다. 우선 아래와 같이 새 파일 `src/lib.rs`를 생성합니다.
```rust
// src/lib.rs
#![no_std]
```
`lib.rs` 또한 `main.rs`와 마찬가지로 cargo가 자동으로 인식하는 특별한 파일입니다. `lib.rs`를 통해 생성되는 라이브러리는 별도의 컴파일 유닛이기에 `lib.rs`에 새로 `#![no_std]` 속성을 명시해야 합니다.
이 라이브러리에 `cargo test`를 사용하도록 테스트 함수들과 속성들을 `main.rs`에서 `lib.rs`로 옮겨옵니다.
```rust
// in src/lib.rs
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub trait Testable {
fn run(&self) -> ();
}
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run();
}
exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
/// Entry point for `cargo test`
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
test_panic_handler(info)
}
```
실행 파일 및 통합 테스트에서 `test_runner`를 사용할 수 있도록, `test_runner`를 `public`으로 설정하고 `cfg(test)` 속성을 적용하지 않았습니다. 또한 다른 실행 파일에서 쓸 수 있도록 패닉 핸들러 구현도 public 함수 `test_panic_handler`로 옮겨놓습니다.
`lib.rs`는 `main.rs`와는 독립적으로 테스트됩니다. 그렇기에 라이브러리를 테스트 모드로 빌드할 경우 실행 시작 함수 `_start` 및 패닉 핸들러를 별도로 제공해야 합니다. [`cfg_attr`] 속성을 사용하여 `no_main` 을 인자로 제공해 `no_main` 속성을 테스트 모드 빌드 시에 적용합니다.
[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
`QemuExitcode` enum 과 `exit_qemu` 함수 또한 `src/lib.rs`로 옮기고 public (pub) 키워드를 달아줍니다.
```rust
// in src/lib.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
이제 실행 파일 및 통합 테스트에서 이 라이브러리로부터 함수들을 불러와 사용할 수 있습니다. `println` 와 `serial_println` 또한 사용 가능하도록 모듈 선언을 `lib.rs`로 옮깁니다.
```rust
// in src/lib.rs
pub mod serial;
pub mod vga_buffer;
```
각 모듈 선언에 `pub` 키워드를 달아주어 라이브러리 밖에서도 해당 모듈들을 사용할 수 있도록 합니다. `println` 및 `serial_println` 매크로가 각각 vga_buffer 모듈과 serial 모듈의 `_print` 함수 구현을 이용하기에 각 모듈 선언에 `pub` 키워드가 꼭 필요합니다.
`main.rs`를 수정하여 우리가 만든 라이브러리를 사용해보겠습니다.
```rust
// in src/main.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
use blog_os::println;
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
/// This function is called on panic.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
우리의 라이브러리는 외부 크레이트와 동일한 방식으로 사용 가능합니다. 라이브러리의 이름은 크레이트 이름 (`blog_os`)과 동일하게 설정됩니다. 위 코드에서 `test_runner` 속성에 `blog_os::test_runner` 함수를 사용하며, `cfg(test)` 속성이 적용된 패닉 핸들러에서 `blog_os::test_panic_handler` 함수를 사용합니다. 또한 라이브러리로부터 `println` 매크로를 가져와 `_start` 함수와 `panic` 함수에서 사용합니다.
이제 `cargo run` 및 `cargo test`가 다시 제대로 동작합니다. 물론 `cargo test`는 여전히 무한히 루프하기에 `ctrl+c`를 통해 종료해야 합니다. 통합 테스트에서 우리의 라이브러리 함수들을 이용해 이것을 고쳐보겠습니다.
### 통합 테스트 완료하기
`src/main.rs`처럼 `tests/basic_boot.rs`에서도 우리가 만든 라이브러리에서 타입들을 불러와 사용할 수 있습니다.
우린 이제 필요했던 타입 정보들을 불러와서 테스트 작성을 마칠 수 있게 되었습니다.
```rust
// in tests/basic_boot.rs
#![test_runner(blog_os::test_runner)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
테스트 실행 함수를 새로 작성하지 않는 대신 `#![test_runner(crate::test_runner)]` 속성을 `#![test_runner(blog_os::test_runner)]` 속성으로 변경하여 라이브러리의 `test_runner` 함수를 사용합니다. `basic_boot.rs`의 `test_runner` 함수는 이제 필요 없으니 지워줍니다. `main.rs`에서처럼 패닉 핸들러에서 `blog_os::test_panic_handler` 함수를 호출합니다.
다시 `cargo test`를 시도하면 실행을 정상적으로 완료합니다. `lib.rs`와 `main.rs` 그리고 `basic_boot.rs` 각각의 빌드 및 테스트가 따로 실행되는 것을 확인하실 수 있습니다. `main.rs`와 통합 테스트 `basic_boot`의 경우 `#[test_case]` 속성이 적용된 함수가 하나도 없어 "Running 0 tests"라는 메시지가 출력됩니다.
`basic_boot.rs`에 테스트들을 추가할 수 있게 되었습니다. VGA 버퍼를 테스트했던 것처럼, 여기서도 `println`이 패닉 없이 잘 동작하는지 테스트 해보겠습니다.
```rust
// in tests/basic_boot.rs
use blog_os::println;
#[test_case]
fn test_println() {
println!("test_println output");
}
```
`cargo test` 실행 시 테스트 함수들이 제대로 식별되고 실행되는 것을 확인할 수 있습니다.
이 테스트가 VGA 버퍼 테스트 중 하나와 거의 동일해서 이 테스트가 쓸모없어 보일 수 있습니다. 하지만 운영체제 개발을 하면서 점점 `main.rs`의 `_start` 함수와 `lib.rs`의 `_start` 함수에는 서로 다른 초기화 코드가 추가될 수 있기에, 미래에 가서는 두 테스트가 서로 많이 다른 환경을 테스트하게 될 것입니다.
`_start` 함수에서 별도의 초기화 함수를 호출하지 않고 바로 `println` 함수를 테스트함으로써 부팅 직후부터 `println`이 제대로 동작하는지를 확인할 수 있습니다. 패닉 메시지 출력에 `println`을 이용하고 있기에 이 함수가 제대로 동작하는 것이 상당히 중요합니다.
### 앞으로 추가할 만한 테스트들
통합 테스트는 크레이트 실행 파일과는 완전히 별개의 실행파일로 취급됩니다. 이 덕에 크레이트와는 별개의 독립적인 환경 설정을 적용할 수 있고, 또한 코드가 CPU 및 하드웨어 장치와 올바르게 상호 작용하는지 테스트할 수 있습니다.
`basic_boot`는 통합 테스트의 매우 간단한 예시입니다. 커널을 작성해나가면서 커널의 기능도 점점 많아지고 하드웨어와 상호작용하는 방식도 다양해질 것입니다. 통합 테스트를 통해 커널과 하드웨어와의 상호작용이 예상대로 작동하는지 확인할 수 있습니다. 아래와 같은 방향으로 통합 테스트를 작성해볼 수 있을 것입니다.
- **CPU 예외**: 프로그램 코드가 허용되지 않은 작업을 실행하는 경우 (예: 0으로 나누기 연산), CPU는 예외 시그널을 반환합니다. 커널은 이런 예외 상황에 대처할 예외 핸들러를 등록해놓을 수 있습니다. 통합 테스트를 통해 CPU 예외 발생 시 알맞은 예외 핸들러가 호출되는지, 혹은 예외 처리 후 원래 실행 중이던 코드가 문제없이 실행을 계속하는지 확인해볼 수 있습니다.
- **페이지 테이블**: 페이지 테이블은 어떤 메모리 영역에 접근할 수 있고 유효한지 정의합니다. 페이지 테이블의 내용을 변경하여 새 프로그램의 실행에 필요한 메모리 영역을 할당할 수 있습니다. 통합 테스트를 통해 `_start` 함수에서 페이지 테이블의 내용을 변경한 후 `#[test_case]` 속성이 부여된 테스트에서 이상 상황이 발생하지 않았는지 확인해볼 수 있습니다.
- **사용자 공간 프로그램**: 사용자 공간에서 실행되는 프로그램들은 시스템 자원에 대해 제한된 접근 권한을 가집니다. 예를 들면, 사용자 공간 프로그램은 커널의 자료구조 및 실행 중인 다른 프로그램의 메모리 영역에 접근할 수 없습니다. 통합 테스트를 통해 허용되지 않은 작업을 시도하는 사용자 공간 프로그램을 작성한 후 커널이 이를 제대로 차단하는지 확인해볼 수 있습니다.
통합 테스트를 작성할 아이디어는 많이 있습니다. 테스트들을 작성해놓으면 이후에 커널에 새로운 기능을 추가하거나 코드를 리팩토링 할 때 우리가 실수를 저지르지 않는지 확인할 수 있습니다. 커널 코드 구현이 크고 복잡해질수록 더 중요한 사항입니다.
### 패닉을 가정하는 테스트
표준 라이브러리의 테스트 프레임워크는 [`#[should_panic]` 속성][should_panic]을 지원합니다. 이 속성은 패닉 발생을 가정하는 테스트를 작성할 때 쓰입니다. 예를 들어, 유효하지 않은 인자가 함수에 전달된 경우 실행이 실패하는지 확인할 때 이 속성을 사용합니다. 이 속성은 표준 라이브러리의 지원이 필요해서 `#[no_std]` 크레이트에서는 사용할 수 없습니다.
[should_panic]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panics
`#[should_panic]` 속성을 커널에서 직접 사용하지는 못하지만, 패닉 핸들러에서 실행 성공 여부 코드를 반환하는 통합 테스트를 작성하여 비슷한 기능을 얻을 수 있습니다. 아래처럼 `should_panic`이라는 통합 테스트를 작성해보겠습니다.
```rust
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{QemuExitCode, exit_qemu, serial_println};
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
이 테스트는 아직 `_start` 함수 및 test_runner를 설정하는 속성들을 정의하지 않아 미완성인 상태입니다. 빠진 부분들을 채워줍시다.
```rust
// in tests/should_panic.rs
#![feature(custom_test_frameworks)]
#![test_runner(test_runner)]
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
pub fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
}
exit_qemu(QemuExitCode::Success);
}
```
`lib.rs`에서의 `test_runner`를 재사용하지 않습니다. 이 테스트는 자체적인 `test_runner` 함수를 정의하며 이 함수는 테스트가 패닉 없이 반환하는 경우, 실행 실패 코드를 반환하며 종료합니다. 정의된 테스트 함수가 하나도 없다면, `test_runner`는 실행 성공 코드를 반환하며 종료합니다. `test_runner`는 테스트 1개 실행 후 종료할 것이기에 `#[test_case]` 속성이 붙은 함수를 1개 이상 선언하는 것은 무의미합니다.
이제 패닉 발생을 가정하는 테스트를 작성할 수 있습니다.
```rust
// in tests/should_panic.rs
use blog_os::serial_print;
#[test_case]
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
```
테스트에서 `assert_eq` 매크로를 이용해 0과 1이 같다는 가정을 합니다. 이 가정은 늘 거짓이기에 테스트는 패닉할 것입니다. 여기서는 `Testable` 트레이트를 사용하지 않았기에, 수동으로 `serial_print!` 매크로를 삽입하여 테스트 함수 이름을 출력합니다.
명령어 `cargo test --test should_panic`을 실행하면 패닉이 발생하여 테스트가 성공하는 것을 확인할 수 있습니다.
`assert_eq` 매크로를 사용한 가정문을 지우고 다시 테스트를 실행하면 _"test did not panic"_ 이라는 메시지가 출력되며 테스트가 실패합니다.
이 방식의 큰 문제는 바로 테스트 함수를 하나밖에 쓸 수 없다는 점입니다. 패닉 핸들러가 호출된 후에는 다른 테스트의 실행을 계속할 수가 없어서, `#[test_case]` 속성이 붙은 함수가 여럿 있더라도 첫 함수만 실행이 됩니다. 이 문제의 해결책을 알고 계신다면 제게 꼭 알려주세요!
### 테스트 하네스 (test harness)를 쓰지 않는 테스트 {#no-harness-tests}
테스트 함수가 1개인 통합 테스트 (예: 우리의 `should_panic` 테스트)는 별도의 test_runner가 필요하지 않습니다.
이런 테스트들은 test_runner 사용을 해제하고 `_start` 함수에서 직접 실행해도 됩니다.
여기서 핵심은 `Cargo.toml`에서 해당 테스트에 대해 `harness` 플래그를 해제하는 것입니다. 이 플래그는 통합 테스트에 대해 test_runner의 사용 유무를 설정합니다. 플래그가 `false`로 설정된 경우, 기본 및 커스텀 test_runner 모두 사용이 해제되고, 테스트는 일반 실행파일로 취급됩니다.
`should_panic` 테스트에서 `harness` 플래그를 false로 설정합니다.
```toml
# in Cargo.toml
[[test]]
name = "should_panic"
harness = false
```
`should_panic` 테스트에서 test_runner 사용에 필요한 코드를 모두 제거하면 아래처럼 간소해집니다.
```rust
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
should_fail();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
loop{}
}
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
이제 `_start` 함수에서 직접 `should_fail` 함수를 호출하며, `should_fail` 함수가 반환하는 경우 `_start` 함수가 실행 실패를 나타내는 종료 코드를 반환하며 종료합니다. `cargo test --test should_panic`을 실행하여 테스트 결과는 이전과 동일함을 확인할 수 있습니다.
`harness` 속성을 해제하는 것은 복잡한 통합 테스트들을 실행할 때도 유용할 수 있습니다. 예를 들면, 테스트 함수마다 실행 환경에 특정 side effect를 일으키는 경우, 테스트들 간의 실행 순서가 중요하기에 `harness` 속성을 해제하고 테스트들을 원하는 순서대로 실행할 수 있습니다.
## 정리
소프트웨어 테스트는 각 컴포넌트가 예상대로 동작하는지 확인하는 데에 매우 유용합니다. 테스트를 통해 버그의 부재를 보장할 수는 없지만, 개발 중 새롭게 등장한 버그 및 기존의 버그를 찾아내는 데에 여전히 도움이 많이 됩니다.
이 글에서는 Rust 커널 테스트용 프레임워크를 설정하는 방법을 다뤘습니다. Rust가 지원하는 커스텀 테스트 프레임워크 기능을 통해 베어 메탈 환경에서 `#[test_case]` 속성이 적용된 테스트를 지원하는 기능을 구현했습니다. QEMU의 `isa-debug-exit` 장치를 사용해 `test_runner`가 테스트 완료 후 QEMU를 종료하고 테스트 결과를 보고하도록 만들었습니다. VGA 버퍼 대신 콘솔에 에러 메시지를 출력하기 위해 시리얼 포트를 이용하는 기초적인 드라이버 프로그램을 만들었습니다.
`println` 매크로의 구현을 점검하는 테스트들을 작성한 후, 이 글의 후반부에서는 통합 테스트 작성에 대해 다뤘습니다. 통합 테스트는 `tests` 디렉터리에 저장되며 별도의 실행파일로 취급된다는 것을 배웠습니다. 통합 테스트에서 `exit_qemu` 함수 및 `serial_println` 매크로를 사용할 수 있도록 필요한 코드 구현을 크레이트 내 새 라이브러리로 옮겼습니다. 통합 테스트는 분리된 환경에서 실행됩니다. 따라서 통합 테스트를 통해 하드웨어와의 상호작용을 구현한 코드를 시험해볼 수 있으며, 패닉 발생을 가정하는 테스트를 작성할 수도 있습니다.
실제 하드웨어 환경과 유사한 QEMU 상에서 동작하는 테스트 프레임워크를 완성했습니다. 앞으로 커널이 더 복잡해지더라도 더 많은 테스트를 작성하면서 커널 코드를 유지보수할 수 있을 것입니다.
## 다음 단계는 무엇일까요?
다음 글에서는 _CPU exception (예외)_ 에 대해 알아볼 것입니다. 분모가 0인 나누기 연산 혹은 매핑되지 않은 메모리 페이지에 대한 접근 (페이지 폴트) 등 허가되지 않은 작업이 일어났을 때 CPU가 예외를 발생시킵니다. 이러한 예외 발생을 포착하고 분석할 수 있어야 앞으로 커널에 발생할 수많은 오류를 디버깅할 수 있을 것입니다. 예외를 처리하는 과정은 하드웨어 인터럽트를 처리하는 과정(예: 컴퓨터의 키보드 입력을 지원할 때)과 매우 유사합니다.
================================================
FILE: blog/content/edition-2/posts/04-testing/index.md
================================================
+++
title = "Testing"
weight = 4
path = "testing"
date = 2019-04-27
[extra]
chapter = "Bare Bones"
comments_search_term = 1009
+++
This post explores unit and integration testing in `no_std` executables. We will use Rust's support for custom test frameworks to execute test functions inside our kernel. To report the results out of QEMU, we will use different features of QEMU and the `bootimage` tool.
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-04`][post branch] branch.
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-04
## Requirements
This post replaces the (now deprecated) [_Unit Testing_] and [_Integration Tests_] posts. It assumes that you have followed the [_A Minimal Rust Kernel_] post after 2019-04-27. Mainly, it requires that you have a `.cargo/config.toml` file that [sets a default target] and [defines a runner executable].
[_Unit Testing_]: @/edition-2/posts/deprecated/04-unit-testing/index.md
[_Integration Tests_]: @/edition-2/posts/deprecated/05-integration-tests/index.md
[_A Minimal Rust Kernel_]: @/edition-2/posts/02-minimal-rust-kernel/index.md
[sets a default target]: @/edition-2/posts/02-minimal-rust-kernel/index.md#set-a-default-target
[defines a runner executable]: @/edition-2/posts/02-minimal-rust-kernel/index.md#using-cargo-run
## Testing in Rust
Rust has a [built-in test framework] that is capable of running unit tests without the need to set anything up. Just create a function that checks some results through assertions and add the `#[test]` attribute to the function header. Then `cargo test` will automatically find and execute all test functions of your crate.
[built-in test framework]: https://doc.rust-lang.org/book/ch11-00-testing.html
To enable testing for our kernel binary, we can set the `test` flag in the Cargo.toml to `true`:
```toml
# in Cargo.toml
[[bin]]
name = "blog_os"
test = true
bench = false
```
This [`[[bin]]` section](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#configuring-a-target) specifies how `cargo` should compile our `blog_os` executable.
The `test` field specifies whether testing is supported for this executable.
We set `test = false` in the first post to [make `rust-analyzer` happy](@/edition-2/posts/01-freestanding-rust-binary/index.md#making-rust-analyzer-happy), but now we want to enable testing, so we set it back to `true`.
Unfortunately, testing is a bit more complicated for `no_std` applications such as our kernel. The problem is that Rust's test framework implicitly uses the built-in [`test`] library, which depends on the standard library. This means that we can't use the default test framework for our `#[no_std]` kernel.
[`test`]: https://doc.rust-lang.org/test/index.html
We can see this when we try to run `cargo test` in our project:
```
> cargo test
Compiling blog_os v0.1.0 (/…/blog_os)
error[E0463]: can't find crate for `test`
```
Since the `test` crate depends on the standard library, it is not available for our bare metal target. While porting the `test` crate to a `#[no_std]` context [is possible][utest], it is highly unstable and requires some hacks, such as redefining the `panic` macro.
[utest]: https://github.com/japaric/utest
### Custom Test Frameworks
Fortunately, Rust supports replacing the default test framework through the unstable [`custom_test_frameworks`] feature. This feature requires no external libraries and thus also works in `#[no_std]` environments. It works by collecting all functions annotated with a `#[test_case]` attribute and then invoking a user-specified runner function with the list of tests as an argument. Thus, it gives the implementation maximal control over the test process.
[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html
The disadvantage compared to the default test framework is that many advanced features, such as [`should_panic` tests], are not available. Instead, it is up to the implementation to provide such features itself if needed. This is ideal for us since we have a very special execution environment where the default implementations of such advanced features probably wouldn't work anyway. For example, the `#[should_panic]` attribute relies on stack unwinding to catch the panics, which we disabled for our kernel.
[`should_panic` tests]: https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic
To implement a custom test framework for our kernel, we add the following to our `main.rs`:
```rust
// in src/main.rs
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
}
```
Our runner just prints a short debug message and then calls each test function in the list. The argument type `&[&dyn Fn()]` is a [_slice_] of [_trait object_] references of the [_Fn()_] trait. It is basically a list of references to types that can be called like a function. Since the function is useless for non-test runs, we use the `#[cfg(test)]` attribute to include it only for tests.
[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html
[_trait object_]: https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html
[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html
When we run `cargo test` now, we see that it now succeeds (if it doesn't, see the note below). However, we still see our "Hello World" instead of the message from our `test_runner`. The reason is that our `_start` function is still used as entry point. The custom test frameworks feature generates a `main` function that calls `test_runner`, but this function is ignored because we use the `#[no_main]` attribute and provide our own entry point.
**Note:** There is currently a bug in cargo that leads to "duplicate lang item" errors on `cargo test` in some cases. It occurs when you have set `panic = "abort"` for a profile in your `Cargo.toml`. Try removing it, then `cargo test` should work. Alternatively, if that doesn't work, then add `panic-abort-tests = true` to the `[unstable]` section of your `.cargo/config.toml` file. See the [cargo issue](https://github.com/rust-lang/cargo/issues/7359) for more information on this.
To fix this, we first need to change the name of the generated function to something different than `main` through the `reexport_test_harness_main` attribute. Then we can call the renamed function from our `_start` function:
```rust
// in src/main.rs
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
```
We set the name of the test framework entry function to `test_main` and call it from our `_start` entry point. We use [conditional compilation] to add the call to `test_main` only in test contexts because the function is not generated on a normal run.
When we now execute `cargo test`, we see the "Running 0 tests" message from our `test_runner` on the screen. We are now ready to create our first test function:
```rust
// in src/main.rs
#[test_case]
fn trivial_assertion() {
print!("trivial assertion... ");
assert_eq!(1, 1);
println!("[ok]");
}
```
When we run `cargo test` now, we see the following output:
![QEMU printing "Hello World!", "Running 1 tests", and "trivial assertion... [ok]"](qemu-test-runner-output.png)
The `tests` slice passed to our `test_runner` function now contains a reference to the `trivial_assertion` function. From the `trivial assertion... [ok]` output on the screen, we see that the test was called and that it succeeded.
After executing the tests, our `test_runner` returns to the `test_main` function, which in turn returns to our `_start` entry point function. At the end of `_start`, we enter an endless loop because the entry point function is not allowed to return. This is a problem, because we want `cargo test` to exit after running all tests.
## Exiting QEMU
Right now, we have an endless loop at the end of our `_start` function and need to close QEMU manually on each execution of `cargo test`. This is unfortunate because we also want to run `cargo test` in scripts without user interaction. The clean solution to this would be to implement a proper way to shutdown our OS. Unfortunately, this is relatively complex because it requires implementing support for either the [APM] or [ACPI] power management standard.
[APM]: https://wiki.osdev.org/APM
[ACPI]: https://wiki.osdev.org/ACPI
Luckily, there is an escape hatch: QEMU supports a special `isa-debug-exit` device, which provides an easy way to exit QEMU from the guest system. To enable it, we need to pass a `-device` argument to QEMU. We can do so by adding a `package.metadata.bootimage.test-args` configuration key in our `Cargo.toml`:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
```
The `bootimage runner` appends the `test-args` to the default QEMU command for all test executables. For a normal `cargo run`, the arguments are ignored.
Together with the device name (`isa-debug-exit`), we pass the two parameters `iobase` and `iosize` that specify the _I/O port_ through which the device can be reached from our kernel.
### I/O Ports
There are two different approaches for communicating between the CPU and peripheral hardware on x86, **memory-mapped I/O** and **port-mapped I/O**. We already used memory-mapped I/O for accessing the [VGA text buffer] through the memory address `0xb8000`. This address is not mapped to RAM but to some memory on the VGA device.
[VGA text buffer]: @/edition-2/posts/03-vga-text-buffer/index.md
In contrast, port-mapped I/O uses a separate I/O bus for communication. Each connected peripheral has one or more port numbers. To communicate with such an I/O port, there are special CPU instructions called `in` and `out`, which take a port number and a data byte (there are also variations of these commands that allow sending a `u16` or `u32`).
The `isa-debug-exit` device uses port-mapped I/O. The `iobase` parameter specifies on which port address the device should live (`0xf4` is a [generally unused][list of x86 I/O ports] port on the x86's IO bus) and the `iosize` specifies the port size (`0x04` means four bytes).
[list of x86 I/O ports]: https://wiki.osdev.org/I/O_Ports#The_list
### Using the Exit Device
The functionality of the `isa-debug-exit` device is very simple. When a `value` is written to the I/O port specified by `iobase`, it causes QEMU to exit with [exit status] `(value << 1) | 1`. So when we write `0` to the port, QEMU will exit with exit status `(0 << 1) | 1 = 1`, and when we write `1` to the port, it will exit with exit status `(1 << 1) | 1 = 3`.
[exit status]: https://en.wikipedia.org/wiki/Exit_status
Instead of manually invoking the `in` and `out` assembly instructions, we use the abstractions provided by the [`x86_64`] crate. To add a dependency on that crate, we add it to the `dependencies` section in our `Cargo.toml`:
[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/
```toml
# in Cargo.toml
[dependencies]
x86_64 = "0.14.2"
```
Now we can use the [`Port`] type provided by the crate to create an `exit_qemu` function:
[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html
```rust
// in src/main.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
The function creates a new [`Port`] at `0xf4`, which is the `iobase` of the `isa-debug-exit` device. Then it writes the passed exit code to the port. We use `u32` because we specified the `iosize` of the `isa-debug-exit` device as 4 bytes. Both operations are unsafe because writing to an I/O port can generally result in arbitrary behavior.
To specify the exit status, we create a `QemuExitCode` enum. The idea is to exit with the success exit code if all tests succeeded and with the failure exit code otherwise. The enum is marked as `#[repr(u32)]` to represent each variant by a `u32` integer. We use the exit code `0x10` for success and `0x11` for failure. The actual exit codes don't matter much, as long as they don't clash with the default exit codes of QEMU. For example, using exit code `0` for success is not a good idea because it becomes `(0 << 1) | 1 = 1` after the transformation, which is the default exit code when QEMU fails to run. So we could not differentiate a QEMU error from a successful test run.
We can now update our `test_runner` to exit QEMU after all tests have run:
```rust
// in src/main.rs
fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
/// new
exit_qemu(QemuExitCode::Success);
}
```
When we run `cargo test` now, we see that QEMU immediately closes after executing the tests. The problem is that `cargo test` interprets the test as failed even though we passed our `Success` exit code:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be
Building bootloader
Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader)
Finished release [optimized + debuginfo] target(s) in 1.07s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4,
iosize=0x04`
error: test failed, to rerun pass '--bin blog_os'
```
The problem is that `cargo test` considers all error codes other than `0` as failure.
### Success Exit Code
To work around this, `bootimage` provides a `test-success-exit-code` configuration key that maps a specified exit code to the exit code `0`:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = […]
test-success-exit-code = 33 # (0x10 << 1) | 1
```
With this configuration, `bootimage` maps our success exit code to exit code 0, so that `cargo test` correctly recognizes the success case and does not count the test as failed.
Our test runner now automatically closes QEMU and correctly reports the test results. We still see the QEMU window open for a very short time, but it does not suffice to read the results. It would be nice if we could print the test results to the console instead, so we can still see them after QEMU exits.
## Printing to the Console
To see the test output on the console, we need to send the data from our kernel to the host system somehow. There are various ways to achieve this, for example, by sending the data over a TCP network interface. However, setting up a networking stack is quite a complex task, so we will choose a simpler solution instead.
### Serial Port
A simple way to send the data is to use the [serial port], an old interface standard which is no longer found in modern computers. It is easy to program and QEMU can redirect the bytes sent over serial to the host's standard output or a file.
[serial port]: https://en.wikipedia.org/wiki/Serial_port
The chips implementing a serial interface are called [UARTs]. There are [lots of UART models] on x86, but fortunately the only differences between them are some advanced features we don't need. The common UARTs today are all compatible with the [16550 UART], so we will use that model for our testing framework.
[UARTs]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
[lots of UART models]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#Models
[16550 UART]: https://en.wikipedia.org/wiki/16550_UART
We will use the [`uart_16550`] crate to initialize the UART and send data over the serial port. To add it as a dependency, we update our `Cargo.toml` and `main.rs`:
[`uart_16550`]: https://docs.rs/uart_16550
```toml
# in Cargo.toml
[dependencies]
uart_16550 = "0.2.0"
```
The `uart_16550` crate contains a `SerialPort` struct that represents the UART registers, but we still need to construct an instance of it ourselves. For that, we create a new `serial` module with the following content:
```rust
// in src/main.rs
mod serial;
```
```rust
// in src/serial.rs
use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;
lazy_static! {
pub static ref SERIAL1: Mutex = {
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
serial_port.init();
Mutex::new(serial_port)
};
}
```
Like with the [VGA text buffer][vga lazy-static], we use `lazy_static` and a spinlock to create a `static` writer instance. By using `lazy_static` we can ensure that the `init` method is called exactly once on its first use.
Like the `isa-debug-exit` device, the UART is programmed using port I/O. Since the UART is more complex, it uses multiple I/O ports for programming different device registers. The unsafe `SerialPort::new` function expects the address of the first I/O port of the UART as an argument, from which it can calculate the addresses of all needed ports. We're passing the port address `0x3F8`, which is the standard port number for the first serial interface.
[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
To make the serial port easily usable, we add `serial_print!` and `serial_println!` macros:
```rust
// in src/serial.rs
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
use core::fmt::Write;
SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
}
/// Prints to the host through the serial interface.
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => {
$crate::serial::_print(format_args!($($arg)*));
};
}
/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
concat!($fmt, "\n"), $($arg)*));
}
```
The implementation is very similar to the implementation of our `print` and `println` macros. Since the `SerialPort` type already implements the [`fmt::Write`] trait, we don't need to provide our own implementation.
[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
Now we can print to the serial interface instead of the VGA text buffer in our test code:
```rust
// in src/main.rs
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
[…]
}
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
Note that the `serial_println` macro lives directly under the root namespace because we used the `#[macro_export]` attribute, so importing it through `use crate::serial::serial_println` will not work.
### QEMU Arguments
To see the serial output from QEMU, we need to use the `-serial` argument to redirect the output to stdout:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio"
]
```
When we run `cargo test` now, we see the test output directly in the console:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [ok]
```
However, when a test fails, we still see the output inside QEMU because our panic handler still uses `println`. To simulate this, we can change the assertion in our `trivial_assertion` test to `assert_eq!(0, 1)`:

We see that the panic message is still printed to the VGA buffer, while the other test output is printed to the serial port. The panic message is quite useful, so it would be useful to see it in the console too.
### Print an Error Message on Panic
To exit QEMU with an error message on a panic, we can use [conditional compilation] to use a different panic handler in testing mode:
[conditional compilation]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html
```rust
// in src/main.rs
// our existing panic handler
#[cfg(not(test))] // new attribute
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
// our panic handler in test mode
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
```
For our test panic handler, we use `serial_println` instead of `println` and then exit QEMU with a failure exit code. Note that we still need an endless `loop` after the `exit_qemu` call because the compiler does not know that the `isa-debug-exit` device causes a program exit.
Now QEMU also exits for failed tests and prints a useful error message on the console:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [failed]
Error: panicked at 'assertion failed: `(left == right)`
left: `0`,
right: `1`', src/main.rs:65:5
```
Since we see all test output on the console now, we no longer need the QEMU window that pops up for a short time. So we can hide it completely.
### Hiding QEMU
Since we report out the complete test results using the `isa-debug-exit` device and the serial port, we don't need the QEMU window anymore. We can hide it by passing the `-display none` argument to QEMU:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio",
"-display", "none"
]
```
Now QEMU runs completely in the background and no window gets opened anymore. This is not only less annoying, but also allows our test framework to run in environments without a graphical user interface, such as CI services or [SSH] connections.
[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
### Timeouts
Since `cargo test` waits until the test runner exits, a test that never returns can block the test runner forever. That's unfortunate, but not a big problem in practice since it's usually easy to avoid endless loops. In our case, however, endless loops can occur in various situations:
- The bootloader fails to load our kernel, which causes the system to reboot endlessly.
- The BIOS/UEFI firmware fails to load the bootloader, which causes the same endless rebooting.
- The CPU enters a `loop {}` statement at the end of some of our functions, for example because the QEMU exit device doesn't work properly.
- The hardware causes a system reset, for example when a CPU exception is not caught (explained in a future post).
Since endless loops can occur in so many situations, the `bootimage` tool sets a timeout of 5 minutes for each test executable by default. If the test does not finish within this time, it is marked as failed and a "Timed Out" error is printed to the console. This feature ensures that tests that are stuck in an endless loop don't block `cargo test` forever.
You can try it yourself by adding a `loop {}` statement in the `trivial_assertion` test. When you run `cargo test`, you see that the test is marked as timed out after 5 minutes. The timeout duration is [configurable][bootimage config] through a `test-timeout` key in the Cargo.toml:
[bootimage config]: https://github.com/rust-osdev/bootimage#configuration
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-timeout = 300 # (in seconds)
```
If you don't want to wait 5 minutes for the `trivial_assertion` test to time out, you can temporarily decrease the above value.
### Insert Printing Automatically
Our `trivial_assertion` test currently needs to print its own status information using `serial_print!`/`serial_println!`:
```rust
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
Manually adding these print statements for every test we write is cumbersome, so let's update our `test_runner` to print these messages automatically. To do that, we need to create a new `Testable` trait:
```rust
// in src/main.rs
pub trait Testable {
fn run(&self) -> ();
}
```
The trick now is to implement this trait for all types `T` that implement the [`Fn()` trait]:
[`Fn()` trait]: https://doc.rust-lang.org/stable/core/ops/trait.Fn.html
```rust
// in src/main.rs
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
```
We implement the `run` function by first printing the function name using the [`any::type_name`] function. This function is implemented directly in the compiler and returns a string description of every type. For functions, the type is their name, so this is exactly what we want in this case. The `\t` character is the [tab character], which adds some alignment to the `[ok]` messages.
[`any::type_name`]: https://doc.rust-lang.org/stable/core/any/fn.type_name.html
[tab character]: https://en.wikipedia.org/wiki/Tab_character
After printing the function name, we invoke the test function through `self()`. This only works because we require that `self` implements the `Fn()` trait. After the test function returns, we print `[ok]` to indicate that the function did not panic.
The last step is to update our `test_runner` to use the new `Testable` trait:
```rust
// in src/main.rs
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Testable]) { // new
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run(); // new
}
exit_qemu(QemuExitCode::Success);
}
```
The only two changes are the type of the `tests` argument from `&[&dyn Fn()]` to `&[&dyn Testable]` and the fact that we now call `test.run()` instead of `test()`.
We can now remove the print statements from our `trivial_assertion` test since they're now printed automatically:
```rust
// in src/main.rs
#[test_case]
fn trivial_assertion() {
assert_eq!(1, 1);
}
```
The `cargo test` output now looks like this:
```
Running 1 tests
blog_os::trivial_assertion... [ok]
```
The function name now includes the full path to the function, which is useful when test functions in different modules have the same name. Otherwise, the output looks the same as before, but we no longer need to add print statements to our tests manually.
## Testing the VGA Buffer
Now that we have a working test framework, we can create a few tests for our VGA buffer implementation. First, we create a very simple test to verify that `println` works without panicking:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_simple() {
println!("test_println_simple output");
}
```
The test just prints something to the VGA buffer. If it finishes without panicking, it means that the `println` invocation did not panic either.
To ensure that no panic occurs even if many lines are printed and lines are shifted off the screen, we can create another test:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_many() {
for _ in 0..200 {
println!("test_println_many output");
}
}
```
We can also create a test function to verify that the printed lines really appear on the screen:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_output() {
let s = "Some test string that fits on a single line";
println!("{}", s);
for (i, c) in s.chars().enumerate() {
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
}
```
The function defines a test string, prints it using `println`, and then iterates over the screen characters of the static `WRITER`, which represents the VGA text buffer. Since `println` prints to the last screen line and then immediately appends a newline, the string should appear on line `BUFFER_HEIGHT - 2`.
By using [`enumerate`], we count the number of iterations in the variable `i`, which we then use for loading the screen character corresponding to `c`. By comparing the `ascii_character` of the screen character with `c`, we ensure that each character of the string really appears in the VGA text buffer.
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
As you can imagine, we could create many more test functions. For example, a function that tests that no panic occurs when printing very long lines and that they're wrapped correctly, or a function for testing that newlines, non-printable characters, and non-unicode characters are handled correctly.
For the rest of this post, however, we will explain how to create _integration tests_ to test the interaction of different components together.
## Integration Tests
The convention for [integration tests] in Rust is to put them into a `tests` directory in the project root (i.e., next to the `src` directory). Both the default test framework and custom test frameworks will automatically pick up and execute all tests in that directory.
[integration tests]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
All integration tests are their own executables and completely separate from our `main.rs`. This means that each test needs to define its own entry point function. Let's create an example integration test named `basic_boot` to see how it works in detail:
```rust
// in tests/basic_boot.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
#[unsafe(no_mangle)] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
fn test_runner(tests: &[&dyn Fn()]) {
unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
loop {}
}
```
Since integration tests are separate executables, we need to provide all the crate attributes (`no_std`, `no_main`, `test_runner`, etc.) again. We also need to create a new entry point function `_start`, which calls the test entry point function `test_main`. We don't need any `cfg(test)` attributes because integration test executables are never built in non-test mode.
We use the [`unimplemented`] macro that always panics as a placeholder for the `test_runner` function and just `loop` in the `panic` handler for now. Ideally, we want to implement these functions exactly as we did in our `main.rs` using the `serial_println` macro and the `exit_qemu` function. The problem is that we don't have access to these functions since tests are built completely separately from our `main.rs` executable.
[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html
If you run `cargo test` at this stage, you will get an endless loop because the panic handler loops endlessly. You need to use the `ctrl+c` keyboard shortcut for exiting QEMU.
### Create a Library
To make the required functions available to our integration test, we need to split off a library from our `main.rs`, which can be included by other crates and integration test executables. To do this, we create a new `src/lib.rs` file:
```rust
// src/lib.rs
#![no_std]
```
Like the `main.rs`, the `lib.rs` is a special file that is automatically recognized by cargo. The library is a separate compilation unit, so we need to specify the `#![no_std]` attribute again.
To make our library work with `cargo test`, we need to also move the test functions and attributes from `main.rs` to `lib.rs`:
```rust
// in src/lib.rs
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub trait Testable {
fn run(&self) -> ();
}
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run();
}
exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
/// Entry point for `cargo test`
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
test_panic_handler(info)
}
```
To make our `test_runner` available to executables and integration tests, we make it public and don't apply the `cfg(test)` attribute to it. We also factor out the implementation of our panic handler into a public `test_panic_handler` function, so that it is available for executables too.
Since our `lib.rs` is tested independently of our `main.rs`, we need to add a `_start` entry point and a panic handler when the library is compiled in test mode. By using the [`cfg_attr`] crate attribute, we conditionally enable the `no_main` attribute in this case.
[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
We also move over the `QemuExitCode` enum and the `exit_qemu` function and make them public:
```rust
// in src/lib.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
Now executables and integration tests can import these functions from the library and don't need to define their own implementations. To also make `println` and `serial_println` available, we move the module declarations too:
```rust
// in src/lib.rs
pub mod serial;
pub mod vga_buffer;
```
We make the modules public to make them usable outside of our library. This is also required for making our `println` and `serial_println` macros usable since they use the `_print` functions of the modules.
Now we can update our `main.rs` to use the library:
```rust
// in src/main.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
use blog_os::println;
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
/// This function is called on panic.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
The library is usable like a normal external crate. It is called `blog_os`, like our crate. The above code uses the `blog_os::test_runner` function in the `test_runner` attribute and the `blog_os::test_panic_handler` function in our `cfg(test)` panic handler. It also imports the `println` macro to make it available to our `_start` and `panic` functions.
At this point, `cargo run` and `cargo test` should work again. Of course, `cargo test` still loops endlessly (you can exit with `ctrl+c`). Let's fix this by using the required library functions in our integration test.
### Completing the Integration Test
Like our `src/main.rs`, our `tests/basic_boot.rs` executable can import types from our new library. This allows us to import the missing components to complete our test:
```rust
// in tests/basic_boot.rs
#![test_runner(blog_os::test_runner)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
Instead of reimplementing the test runner, we use the `test_runner` function from our library by changing the `#![test_runner(crate::test_runner)]` attribute to `#![test_runner(blog_os::test_runner)]`. We then don't need the `test_runner` stub function in `basic_boot.rs` anymore, so we can remove it. For our `panic` handler, we call the `blog_os::test_panic_handler` function like we did in our `main.rs`.
Now `cargo test` exits normally again. When you run it, you will see that it builds and runs the tests for our `lib.rs`, `main.rs`, and `basic_boot.rs` separately after each other. For the `main.rs` and the `basic_boot` integration tests, it reports "Running 0 tests" since these files don't have any functions annotated with `#[test_case]`.
We can now add tests to our `basic_boot.rs`. For example, we can test that `println` works without panicking, like we did in the VGA buffer tests:
```rust
// in tests/basic_boot.rs
use blog_os::println;
#[test_case]
fn test_println() {
println!("test_println output");
}
```
When we run `cargo test` now, we see that it finds and executes the test function.
The test might seem a bit useless right now since it's almost identical to one of the VGA buffer tests. However, in the future, the `_start` functions of our `main.rs` and `lib.rs` might grow and call various initialization routines before running the `test_main` function, so that the two tests are executed in very different environments.
By testing `println` in a `basic_boot` environment without calling any initialization routines in `_start`, we can ensure that `println` works right after booting. This is important because we rely on it, e.g., for printing panic messages.
### Future Tests
The power of integration tests is that they're treated as completely separate executables. This gives them complete control over the environment, which makes it possible to test that the code interacts correctly with the CPU or hardware devices.
Our `basic_boot` test is a very simple example of an integration test. In the future, our kernel will become much more featureful and interact with the hardware in various ways. By adding integration tests, we can ensure that these interactions work (and keep working) as expected. Some ideas for possible future tests are:
- **CPU Exceptions**: When the code performs invalid operations (e.g., divides by zero), the CPU throws an exception. The kernel can register handler functions for such exceptions. An integration test could verify that the correct exception handler is called when a CPU exception occurs or that the execution continues correctly after a resolvable exception.
- **Page Tables**: Page tables define which memory regions are valid and accessible. By modifying the page tables, it is possible to allocate new memory regions, for example when launching programs. An integration test could modify the page tables in the `_start` function and verify that the modifications have the desired effects in `#[test_case]` functions.
- **Userspace Programs**: Userspace programs are programs with limited access to the system's resources. For example, they don't have access to kernel data structures or to the memory of other programs. An integration test could launch userspace programs that perform forbidden operations and verify that the kernel prevents them all.
As you can imagine, many more tests are possible. By adding such tests, we can ensure that we don't break them accidentally when we add new features to our kernel or refactor our code. This is especially important when our kernel becomes larger and more complex.
### Tests that Should Panic
The test framework of the standard library supports a [`#[should_panic]` attribute][should_panic] that allows constructing tests that should fail. This is useful, for example, to verify that a function fails when an invalid argument is passed. Unfortunately, this attribute isn't supported in `#[no_std]` crates since it requires support from the standard library.
[should_panic]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panics
While we can't use the `#[should_panic]` attribute in our kernel, we can get similar behavior by creating an integration test that exits with a success error code from the panic handler. Let's start creating such a test with the name `should_panic`:
```rust
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{QemuExitCode, exit_qemu, serial_println};
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
This test is still incomplete as it doesn't define a `_start` function or any of the custom test runner attributes yet. Let's add the missing parts:
```rust
// in tests/should_panic.rs
#![feature(custom_test_frameworks)]
#![test_runner(test_runner)]
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
pub fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
}
exit_qemu(QemuExitCode::Success);
}
```
Instead of reusing the `test_runner` from our `lib.rs`, the test defines its own `test_runner` function that exits with a failure exit code when a test returns without panicking (we want our tests to panic). If no test function is defined, the runner exits with a success error code. Since the runner always exits after running a single test, it does not make sense to define more than one `#[test_case]` function.
Now we can create a test that should fail:
```rust
// in tests/should_panic.rs
use blog_os::serial_print;
#[test_case]
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
```
The test uses `assert_eq` to assert that `0` and `1` are equal. Of course, this fails, so our test panics as desired. Note that we need to manually print the function name using `serial_print!` here because we don't use the `Testable` trait.
When we run the test through `cargo test --test should_panic` we see that it is successful because the test panicked as expected. When we comment out the assertion and run the test again, we see that it indeed fails with the _"test did not panic"_ message.
A significant drawback of this approach is that it only works for a single test function. With multiple `#[test_case]` functions, only the first function is executed because the execution cannot continue after the panic handler has been called. I currently don't know of a good way to solve this problem, so let me know if you have an idea!
### No Harness Tests
For integration tests that only have a single test function (like our `should_panic` test), the test runner isn't really needed. For cases like this, we can disable the test runner completely and run our test directly in the `_start` function.
The key to this is to disable the `harness` flag for the test in the `Cargo.toml`, which defines whether a test runner is used for an integration test. When it's set to `false`, both the default test runner and the custom test runner feature are disabled, so that the test is treated like a normal executable.
Let's disable the `harness` flag for our `should_panic` test:
```toml
# in Cargo.toml
[[test]]
name = "should_panic"
harness = false
```
Now we vastly simplify our `should_panic` test by removing the `test_runner`-related code. The result looks like this:
```rust
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
should_fail();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
loop{}
}
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
We now call the `should_fail` function directly from our `_start` function and exit with a failure exit code if it returns. When we run `cargo test --test should_panic` now, we see that the test behaves exactly as before.
Apart from creating `should_panic` tests, disabling the `harness` attribute can also be useful for complex integration tests, for example, when the individual test functions have side effects and need to be run in a specified order.
## Summary
Testing is a very useful technique to ensure that certain components have the desired behavior. Even if they cannot show the absence of bugs, they're still a useful tool for finding them and especially for avoiding regressions.
This post explained how to set up a test framework for our Rust kernel. We used Rust's custom test frameworks feature to implement support for a simple `#[test_case]` attribute in our bare-metal environment. Using the `isa-debug-exit` device of QEMU, our test runner can exit QEMU after running the tests and report the test status. To print error messages to the console instead of the VGA buffer, we created a basic driver for the serial port.
After creating some tests for our `println` macro, we explored integration tests in the second half of the post. We learned that they live in the `tests` directory and are treated as completely separate executables. To give them access to the `exit_qemu` function and the `serial_println` macro, we moved most of our code into a library that can be imported by all executables and integration tests. Since integration tests run in their own separate environment, they make it possible to test interactions with the hardware or to create tests that should panic.
We now have a test framework that runs in a realistic environment inside QEMU. By creating more tests in future posts, we can keep our kernel maintainable when it becomes more complex.
## What's next?
In the next post, we will explore _CPU exceptions_. These exceptions are thrown by the CPU when something illegal happens, such as a division by zero or an access to an unmapped memory page (a so-called “page fault”). Being able to catch and examine these exceptions is very important for debugging future errors. Exception handling is also very similar to the handling of hardware interrupts, which is required for keyboard support.
================================================
FILE: blog/content/edition-2/posts/04-testing/index.pt-BR.md
================================================
+++
title = "Testes"
weight = 4
path = "pt-BR/testing"
date = 2019-04-27
[extra]
chapter = "O Básico"
comments_search_term = 1009
# Please update this when updating the translation
translation_based_on_commit = "33b7979468235b8637584e91e4c599cef37d9687"
# GitHub usernames of the people that translated this post
translators = ["richarddalves"]
+++
Este post explora testes unitários e de integração em executáveis `no_std`. Usaremos o suporte do Rust para frameworks de teste customizados para executar funções de teste dentro do nosso kernel. Para reportar os resultados para fora do QEMU, usaremos diferentes recursos do QEMU e da ferramenta `bootimage`.
Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-04`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[na parte inferior]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-04
## Requisitos
Este post substitui os posts (agora deprecados) [_Unit Testing_] e [_Integration Tests_]. Ele assume que você seguiu o post [_A Minimal Rust Kernel_] depois de 27-04-2019. Principalmente, ele requer que você tenha um arquivo `.cargo/config.toml` que [define um alvo padrão] e [define um executável runner].
[_Unit Testing_]: @/edition-2/posts/deprecated/04-unit-testing/index.md
[_Integration Tests_]: @/edition-2/posts/deprecated/05-integration-tests/index.md
[_A Minimal Rust Kernel_]: @/edition-2/posts/02-minimal-rust-kernel/index.pt-BR.md
[define um alvo padrão]: @/edition-2/posts/02-minimal-rust-kernel/index.pt-BR.md#definir-um-alvo-padrao
[define um executável runner]: @/edition-2/posts/02-minimal-rust-kernel/index.pt-BR.md#usando-cargo-run
## Testes em Rust
Rust tem um [framework de testes integrado] que é capaz de executar testes unitários sem a necessidade de configurar nada. Basta criar uma função que verifica alguns resultados através de assertions e adicionar o atributo `#[test]` ao cabeçalho da função. Então `cargo test` automaticamente encontrará e executará todas as funções de teste da sua crate.
[framework de testes integrado]: https://doc.rust-lang.org/book/ch11-00-testing.html
Para habilitar testes para nosso binário kernel, podemos definir a flag `test` no Cargo.toml como `true`:
```toml
# em Cargo.toml
[[bin]]
name = "blog_os"
test = true
bench = false
```
Esta [seção `[[bin]]`](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#configuring-a-target) especifica como o `cargo` deve compilar nosso executável `blog_os`. O campo `test` especifica se testes são suportados para este executável. Definimos `test = false` no primeiro post para [deixar o `rust-analyzer` feliz](@/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md#deixando-rust-analyzer-feliz), mas agora queremos habilitar testes, então o definimos de volta para `true`.
Infelizmente, testes são um pouco mais complicados para aplicações `no_std` como nosso kernel. O problema é que o framework de testes do Rust usa implicitamente a biblioteca [`test`] integrada, que depende da biblioteca padrão. Isso significa que não podemos usar o framework de testes padrão para nosso kernel `#[no_std]`.
[`test`]: https://doc.rust-lang.org/test/index.html
Podemos ver isso quando tentamos executar `cargo test` no nosso projeto:
```
> cargo test
Compiling blog_os v0.1.0 (/…/blog_os)
error[E0463]: can't find crate for `test`
```
Como a crate `test` depende da biblioteca padrão, ela não está disponível para nosso alvo bare metal. Embora portar a crate `test` para um contexto `#[no_std]` [seja possível][utest], é altamente instável e requer alguns hacks, como redefinir a macro `panic`.
[utest]: https://github.com/japaric/utest
### Frameworks de Teste Customizados
Felizmente, Rust suporta substituir o framework de testes padrão através do recurso instável [`custom_test_frameworks`]. Este recurso não requer bibliotecas externas e, portanto, também funciona em ambientes `#[no_std]`. Funciona coletando todas as funções anotadas com um atributo `#[test_case]` e então invocando uma função runner especificada pelo usuário com a lista de testes como argumento. Assim, dá à implementação controle máximo sobre o processo de teste.
[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html
A desvantagem comparada ao framework de testes padrão é que muitos recursos avançados, como [testes `should_panic`], não estão disponíveis. Em vez disso, cabe à implementação fornecer tais recursos ela mesma se necessário. Isso é ideal para nós, pois temos um ambiente de execução muito especial onde as implementações padrão de tais recursos avançados provavelmente não funcionariam de qualquer forma. Por exemplo, o atributo `#[should_panic]` depende de stack unwinding para capturar os panics, que desabilitamos para nosso kernel.
[testes `should_panic`]: https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic
Para implementar um framework de testes customizado para nosso kernel, adicionamos o seguinte ao nosso `main.rs`:
```rust
// em src/main.rs
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
}
```
Nosso runner apenas imprime uma breve mensagem de debug e então chama cada função de teste na lista. O tipo de argumento `&[&dyn Fn()]` é uma [_slice_] de referências a [_trait object_] da trait [_Fn()_]. É basicamente uma lista de referências a tipos que podem ser chamados como uma função. Como a função é inútil para execuções não-teste, usamos o atributo `#[cfg(test)]` para incluí-la apenas para testes.
[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html
[_trait object_]: https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html
[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html
Quando executamos `cargo test` agora, vemos que ele agora é bem-sucedido (se não for, veja a nota abaixo). No entanto, ainda vemos nosso "Hello World" em vez da mensagem do nosso `test_runner`. A razão é que nossa função `_start` ainda é usada como ponto de entrada. O recurso de frameworks de teste customizados gera uma função `main` que chama `test_runner`, mas esta função é ignorada porque usamos o atributo `#[no_main]` e fornecemos nosso próprio ponto de entrada.
**Nota:** Atualmente há um bug no cargo que leva a erros de "duplicate lang item" no `cargo test` em alguns casos. Ocorre quando você definiu `panic = "abort"` para um profile no seu `Cargo.toml`. Tente removê-lo, então `cargo test` deve funcionar. Alternativamente, se isso não funcionar, então adicione `panic-abort-tests = true` à seção `[unstable]` do seu arquivo `.cargo/config.toml`. Veja o [issue do cargo](https://github.com/rust-lang/cargo/issues/7359) para mais informações sobre isso.
Para corrigir isso, primeiro precisamos mudar o nome da função gerada para algo diferente de `main` através do atributo `reexport_test_harness_main`. Então podemos chamar a função renomeada da nossa função `_start`:
```rust
// em src/main.rs
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
```
Definimos o nome da função de entrada do framework de testes como `test_main` e a chamamos do nosso ponto de entrada `_start`. Usamos [compilação condicional] para adicionar a chamada a `test_main` apenas em contextos de teste porque a função não é gerada em uma execução normal.
Quando agora executamos `cargo test`, vemos a mensagem "Running 0 tests" do nosso `test_runner` na tela. Agora estamos prontos para criar nossa primeira função de teste:
```rust
// em src/main.rs
#[test_case]
fn trivial_assertion() {
print!("trivial assertion... ");
assert_eq!(1, 1);
println!("[ok]");
}
```
Quando executamos `cargo test` agora, vemos a seguinte saída:
![QEMU imprimindo "Hello World!", "Running 1 tests" e "trivial assertion... [ok]"](qemu-test-runner-output.png)
A slice `tests` passada para nossa função `test_runner` agora contém uma referência à função `trivial_assertion`. Da saída `trivial assertion... [ok]` na tela, vemos que o teste foi chamado e que foi bem-sucedido.
Após executar os testes, nosso `test_runner` retorna à função `test_main`, que por sua vez retorna à nossa função de ponto de entrada `_start`. No final de `_start`, entramos em um loop infinito porque a função de ponto de entrada não tem permissão para retornar. Isso é um problema, porque queremos que `cargo test` saia após executar todos os testes.
## Saindo do QEMU
Agora, temos um loop infinito no final da nossa função `_start` e precisamos fechar o QEMU manualmente em cada execução de `cargo test`. Isso é infeliz porque também queremos executar `cargo test` em scripts sem interação do usuário. A solução limpa para isso seria implementar uma maneira adequada de desligar nosso SO. Infelizmente, isso é relativamente complexo porque requer implementar suporte para o padrão de gerenciamento de energia [APM] ou [ACPI].
[APM]: https://wiki.osdev.org/APM
[ACPI]: https://wiki.osdev.org/ACPI
Felizmente, há uma saída de emergência: O QEMU suporta um dispositivo especial `isa-debug-exit`, que fornece uma maneira fácil de sair do QEMU do sistema guest. Para habilitá-lo, precisamos passar um argumento `-device` ao QEMU. Podemos fazer isso adicionando uma chave de configuração `package.metadata.bootimage.test-args` no nosso `Cargo.toml`:
```toml
# em Cargo.toml
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
```
O `bootimage runner` anexa os `test-args` ao comando QEMU padrão para todos os executáveis de teste. Para um `cargo run` normal, os argumentos são ignorados.
Junto com o nome do dispositivo (`isa-debug-exit`), passamos os dois parâmetros `iobase` e `iosize` que especificam a _porta I/O_ através da qual o dispositivo pode ser alcançado do nosso kernel.
### Portas I/O
Existem duas abordagens diferentes para comunicação entre a CPU e hardware periférico no x86, **I/O mapeado em memória** e **I/O mapeado em porta**. Já usamos I/O mapeado em memória para acessar o [buffer de texto VGA] através do endereço de memória `0xb8000`. Este endereço não é mapeado para RAM, mas para alguma memória no dispositivo VGA.
[buffer de texto VGA]: @/edition-2/posts/03-vga-text-buffer/index.pt-BR.md
Em contraste, I/O mapeado em porta usa um barramento I/O separado para comunicação. Cada periférico conectado tem um ou mais números de porta. Para comunicar com tal porta I/O, existem instruções especiais de CPU chamadas `in` e `out`, que recebem um número de porta e um byte de dados (também há variações desses comandos que permitem enviar um `u16` ou `u32`).
O dispositivo `isa-debug-exit` usa I/O mapeado em porta. O parâmetro `iobase` especifica em qual endereço de porta o dispositivo deve viver (`0xf4` é uma porta [geralmente não utilizada][lista de portas I/O x86] no barramento IO do x86) e o `iosize` especifica o tamanho da porta (`0x04` significa quatro bytes).
[lista de portas I/O x86]: https://wiki.osdev.org/I/O_Ports#The_list
### Usando o Dispositivo de Saída
A funcionalidade do dispositivo `isa-debug-exit` é muito simples. Quando um `value` é escrito na porta I/O especificada por `iobase`, ele faz com que o QEMU saia com [status de saída] `(value << 1) | 1`. Então, quando escrevemos `0` na porta, o QEMU sairá com status de saída `(0 << 1) | 1 = 1`, e quando escrevemos `1` na porta, ele sairá com status de saída `(1 << 1) | 1 = 3`.
[status de saída]: https://en.wikipedia.org/wiki/Exit_status
Em vez de invocar manualmente as instruções assembly `in` e `out`, usamos as abstrações fornecidas pela crate [`x86_64`]. Para adicionar uma dependência nessa crate, a adicionamos à seção `dependencies` no nosso `Cargo.toml`:
[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/
```toml
# em Cargo.toml
[dependencies]
x86_64 = "0.14.2"
```
Agora podemos usar o tipo [`Port`] fornecido pela crate para criar uma função `exit_qemu`:
[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html
```rust
// em src/main.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
A função cria um novo [`Port`] em `0xf4`, que é o `iobase` do dispositivo `isa-debug-exit`. Então ela escreve o código de saída passado para a porta. Usamos `u32` porque especificamos o `iosize` do dispositivo `isa-debug-exit` como 4 bytes. Ambas as operações são unsafe porque escrever em uma porta I/O geralmente pode resultar em comportamento arbitrário.
Para especificar o status de saída, criamos um enum `QemuExitCode`. A ideia é sair com o código de saída de sucesso se todos os testes foram bem-sucedidos e com o código de saída de falha caso contrário. O enum é marcado como `#[repr(u32)]` para representar cada variante por um inteiro `u32`. Usamos o código de saída `0x10` para sucesso e `0x11` para falha. Os códigos de saída reais não importam muito, desde que não colidam com os códigos de saída padrão do QEMU. Por exemplo, usar código de saída `0` para sucesso não é uma boa ideia porque ele se torna `(0 << 1) | 1 = 1` após a transformação, que é o código de saída padrão quando o QEMU falha ao executar. Então não poderíamos diferenciar um erro do QEMU de uma execução de teste bem-sucedida.
Agora podemos atualizar nosso `test_runner` para sair do QEMU após todos os testes terem sido executados:
```rust
// em src/main.rs
fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
/// novo
exit_qemu(QemuExitCode::Success);
}
```
Quando executamos `cargo test` agora, vemos que o QEMU fecha imediatamente após executar os testes. O problema é que `cargo test` interpreta o teste como falhado mesmo que tenhamos passado nosso código de saída `Success`:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be
Building bootloader
Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader)
Finished release [optimized + debuginfo] target(s) in 1.07s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4,
iosize=0x04`
error: test failed, to rerun pass '--bin blog_os'
```
O problema é que `cargo test` considera todos os códigos de erro diferentes de `0` como falha.
### Código de Saída de Sucesso
Para contornar isso, `bootimage` fornece uma chave de configuração `test-success-exit-code` que mapeia um código de saída especificado para o código de saída `0`:
```toml
# em Cargo.toml
[package.metadata.bootimage]
test-args = […]
test-success-exit-code = 33 # (0x10 << 1) | 1
```
Com esta configuração, `bootimage` mapeia nosso código de saída de sucesso para o código de saída 0, para que `cargo test` reconheça corretamente o caso de sucesso e não conte o teste como falhado.
Nosso test runner agora fecha automaticamente o QEMU e reporta corretamente os resultados do teste. Ainda vemos a janela do QEMU abrir por um tempo muito curto, mas não é suficiente para ler os resultados. Seria bom se pudéssemos imprimir os resultados do teste no console em vez disso, para que ainda possamos vê-los após o QEMU sair.
## Imprimindo no Console
Para ver a saída do teste no console, precisamos enviar os dados do nosso kernel para o sistema host de alguma forma. Existem várias maneiras de conseguir isso, por exemplo, enviando os dados por uma interface de rede TCP. No entanto, configurar uma pilha de rede é uma tarefa bastante complexa, então escolheremos uma solução mais simples em vez disso.
### Porta Serial
Uma maneira simples de enviar os dados é usar a [porta serial], um antigo padrão de interface que não é mais encontrado em computadores modernos. É fácil de programar e o QEMU pode redirecionar os bytes enviados pela porta serial para a saída padrão do host ou um arquivo.
[porta serial]: https://en.wikipedia.org/wiki/Serial_port
Os chips que implementam uma interface serial são chamados [UARTs]. Existem [muitos modelos de UART] no x86, mas felizmente as únicas diferenças entre eles são alguns recursos avançados que não precisamos. Os UARTs comuns hoje são todos compatíveis com o [UART 16550], então usaremos esse modelo para nosso framework de testes.
[UARTs]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
[muitos modelos de UART]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#Models
[UART 16550]: https://en.wikipedia.org/wiki/16550_UART
Usaremos a crate [`uart_16550`] para inicializar o UART e enviar dados pela porta serial. Para adicioná-la como dependência, atualizamos nosso `Cargo.toml` e `main.rs`:
[`uart_16550`]: https://docs.rs/uart_16550
```toml
# em Cargo.toml
[dependencies]
uart_16550 = "0.2.0"
```
A crate `uart_16550` contém uma struct `SerialPort` que representa os registradores UART, mas ainda precisamos construir uma instância dela nós mesmos. Para isso, criamos um novo módulo `serial` com o seguinte conteúdo:
```rust
// em src/main.rs
mod serial;
```
```rust
// em src/serial.rs
use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;
lazy_static! {
pub static ref SERIAL1: Mutex = {
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
serial_port.init();
Mutex::new(serial_port)
};
}
```
Como com o [buffer de texto VGA][vga lazy-static], usamos `lazy_static` e um spinlock para criar uma instância writer `static`. Ao usar `lazy_static` podemos garantir que o método `init` seja chamado exatamente uma vez em seu primeiro uso.
Como o dispositivo `isa-debug-exit`, o UART é programado usando I/O de porta. Como o UART é mais complexo, ele usa múltiplas portas I/O para programar diferentes registradores do dispositivo. A função unsafe `SerialPort::new` espera o endereço da primeira porta I/O do UART como argumento, a partir do qual ela pode calcular os endereços de todas as portas necessárias. Estamos passando o endereço de porta `0x3F8`, que é o número de porta padrão para a primeira interface serial.
[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.pt-BR.md#lazy-statics
Para tornar a porta serial facilmente utilizável, adicionamos macros `serial_print!` e `serial_println!`:
```rust
// em src/serial.rs
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
use core::fmt::Write;
SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
}
/// Imprime no host através da interface serial.
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => {
$crate::serial::_print(format_args!($($arg)*));
};
}
/// Imprime no host através da interface serial, anexando uma newline.
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
concat!($fmt, "\n"), $($arg)*));
}
```
A implementação é muito similar à implementação das nossas macros `print` e `println`. Como o tipo `SerialPort` já implementa a trait [`fmt::Write`], não precisamos fornecer nossa própria implementação.
[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
Agora podemos imprimir na interface serial em vez do buffer de texto VGA no nosso código de teste:
```rust
// em src/main.rs
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
[…]
}
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
Note que a macro `serial_println` vive diretamente sob o namespace raiz porque usamos o atributo `#[macro_export]`, então importá-la através de `use crate::serial::serial_println` não funcionará.
### Argumentos do QEMU
Para ver a saída serial do QEMU, precisamos usar o argumento `-serial` para redirecionar a saída para stdout:
```toml
# em Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio"
]
```
Quando executamos `cargo test` agora, vemos a saída do teste diretamente no console:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [ok]
```
No entanto, quando um teste falha, ainda vemos a saída dentro do QEMU porque nosso handler de panic ainda usa `println`. Para simular isso, podemos mudar a assertion no nosso teste `trivial_assertion` para `assert_eq!(0, 1)`:

Vemos que a mensagem de panic ainda é impressa no buffer VGA, enquanto a outra saída de teste é impressa na porta serial. A mensagem de panic é bastante útil, então seria útil vê-la no console também.
### Imprimir uma Mensagem de Erro no Panic
Para sair do QEMU com uma mensagem de erro em um panic, podemos usar [compilação condicional] para usar um handler de panic diferente no modo de teste:
[compilação condicional]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html
```rust
// em src/main.rs
// nosso handler de panic existente
#[cfg(not(test))] // novo atributo
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
// nosso handler de panic em modo de teste
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
```
Para nosso handler de panic de teste, usamos `serial_println` em vez de `println` e então saímos do QEMU com um código de saída de falha. Note que ainda precisamos de um `loop` infinito após a chamada `exit_qemu` porque o compilador não sabe que o dispositivo `isa-debug-exit` causa uma saída do programa.
Agora o QEMU também sai para testes falhados e imprime uma mensagem de erro útil no console:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [failed]
Error: panicked at 'assertion failed: `(left == right)`
left: `0`,
right: `1`', src/main.rs:65:5
```
Como agora vemos toda a saída do teste no console, não precisamos mais da janela do QEMU que aparece por um curto tempo. Então podemos ocultá-la completamente.
### Ocultando o QEMU
Como reportamos os resultados completos do teste usando o dispositivo `isa-debug-exit` e a porta serial, não precisamos mais da janela do QEMU. Podemos ocultá-la passando o argumento `-display none` ao QEMU:
```toml
# em Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio",
"-display", "none"
]
```
Agora o QEMU executa completamente em segundo plano e nenhuma janela é mais aberta. Isso não é apenas menos irritante, mas também permite que nosso framework de testes execute em ambientes sem interface gráfica do usuário, como serviços de CI ou conexões [SSH].
[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
### Timeouts
Como `cargo test` espera até que o test runner saia, um teste que nunca retorna pode bloquear o test runner para sempre. Isso é infeliz, mas não é um grande problema na prática, pois geralmente é fácil evitar loops infinitos. No nosso caso, no entanto, loops infinitos podem ocorrer em várias situações:
- O bootloader falha ao carregar nosso kernel, o que causa o sistema reiniciar infinitamente.
- O firmware BIOS/UEFI falha ao carregar o bootloader, o que causa a mesma reinicialização infinita.
- A CPU entra em uma declaração `loop {}` no final de algumas das nossas funções, por exemplo porque o dispositivo de saída do QEMU não funciona corretamente.
- O hardware causa um reset do sistema, por exemplo quando uma exceção de CPU não é capturada (explicado em um post futuro).
Como loops infinitos podem ocorrer em tantas situações, a ferramenta `bootimage` define um timeout de 5 minutos para cada executável de teste por padrão. Se o teste não terminar dentro deste tempo, ele é marcado como falhado e um erro "Timed Out" é impresso no console. Este recurso garante que testes que estão presos em um loop infinito não bloqueiem `cargo test` para sempre.
Você pode tentar você mesmo adicionando uma declaração `loop {}` no teste `trivial_assertion`. Quando você executa `cargo test`, vê que o teste é marcado como timed out após 5 minutos. A duração do timeout é [configurável][bootimage config] através de uma chave `test-timeout` no Cargo.toml:
[bootimage config]: https://github.com/rust-osdev/bootimage#configuration
```toml
# em Cargo.toml
[package.metadata.bootimage]
test-timeout = 300 # (em segundos)
```
Se você não quiser esperar 5 minutos para o teste `trivial_assertion` dar timeout, pode diminuir temporariamente o valor acima.
### Inserir Impressão Automaticamente
Nosso teste `trivial_assertion` atualmente precisa imprimir suas próprias informações de status usando `serial_print!`/`serial_println!`:
```rust
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
Adicionar manualmente essas declarações de impressão para cada teste que escrevemos é trabalhoso, então vamos atualizar nosso `test_runner` para imprimir essas mensagens automaticamente. Para fazer isso, precisamos criar uma nova trait `Testable`:
```rust
// em src/main.rs
pub trait Testable {
fn run(&self) -> ();
}
```
O truque agora é implementar esta trait para todos os tipos `T` que implementam a [trait `Fn()`]:
[trait `Fn()`]: https://doc.rust-lang.org/stable/core/ops/trait.Fn.html
```rust
// em src/main.rs
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
```
Implementamos a função `run` primeiro imprimindo o nome da função usando a função [`any::type_name`]. Esta função é implementada diretamente no compilador e retorna uma descrição em string de cada tipo. Para funções, o tipo é seu nome, então isso é exatamente o que queremos neste caso. O caractere `\t` é o [caractere tab], que adiciona algum alinhamento às mensagens `[ok]`.
[`any::type_name`]: https://doc.rust-lang.org/stable/core/any/fn.type_name.html
[caractere tab]: https://en.wikipedia.org/wiki/Tab_character
Após imprimir o nome da função, invocamos a função de teste através de `self()`. Isso só funciona porque exigimos que `self` implemente a trait `Fn()`. Após a função de teste retornar, imprimimos `[ok]` para indicar que a função não entrou em panic.
O último passo é atualizar nosso `test_runner` para usar a nova trait `Testable`:
```rust
// em src/main.rs
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Testable]) { // novo
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run(); // novo
}
exit_qemu(QemuExitCode::Success);
}
```
As únicas duas mudanças são o tipo do argumento `tests` de `&[&dyn Fn()]` para `&[&dyn Testable]` e o fato de que agora chamamos `test.run()` em vez de `test()`.
Agora podemos remover as declarações de impressão do nosso teste `trivial_assertion` já que elas são impressas automaticamente:
```rust
// em src/main.rs
#[test_case]
fn trivial_assertion() {
assert_eq!(1, 1);
}
```
A saída de `cargo test` agora se parece com isto:
```
Running 1 tests
blog_os::trivial_assertion... [ok]
```
O nome da função agora inclui o caminho completo para a função, o que é útil quando funções de teste em diferentes módulos têm o mesmo nome. Caso contrário, a saída parece igual a antes, mas não precisamos mais adicionar declarações de impressão aos nossos testes manualmente.
## Testando o Buffer VGA
Agora que temos um framework de testes funcionando, podemos criar alguns testes para nossa implementação de buffer VGA. Primeiro, criamos um teste muito simples para verificar que `println` funciona sem entrar em panic:
```rust
// em src/vga_buffer.rs
#[test_case]
fn test_println_simple() {
println!("test_println_simple output");
}
```
O teste apenas imprime algo no buffer VGA. Se ele terminar sem entrar em panic, significa que a invocação de `println` também não entrou em panic.
Para garantir que nenhum panic ocorra mesmo se muitas linhas forem impressas e as linhas forem deslocadas para fora da tela, podemos criar outro teste:
```rust
// em src/vga_buffer.rs
#[test_case]
fn test_println_many() {
for _ in 0..200 {
println!("test_println_many output");
}
}
```
Também podemos criar uma função de teste para verificar que as linhas impressas realmente aparecem na tela:
```rust
// em src/vga_buffer.rs
#[test_case]
fn test_println_output() {
let s = "Some test string that fits on a single line";
println!("{}", s);
for (i, c) in s.chars().enumerate() {
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
}
```
A função define uma string de teste, a imprime usando `println`, e então itera sobre os caracteres da tela do `WRITER` static, que representa o buffer de texto VGA. Como `println` imprime na última linha da tela e então anexa imediatamente uma newline, a string deve aparecer na linha `BUFFER_HEIGHT - 2`.
Ao usar [`enumerate`], contamos o número de iterações na variável `i`, que então usamos para carregar o caractere da tela correspondente a `c`. Ao comparar o `ascii_character` do caractere da tela com `c`, garantimos que cada caractere da string realmente aparece no buffer de texto VGA.
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
Como você pode imaginar, poderíamos criar muitas mais funções de teste. Por exemplo, uma função que testa que nenhum panic ocorre ao imprimir linhas muito longas e que elas são quebradas corretamente, ou uma função para testar que newlines, caracteres não imprimíveis e caracteres não-unicode são tratados corretamente.
Para o resto deste post, no entanto, explicaremos como criar _testes de integração_ para testar a interação de diferentes componentes juntos.
## Testes de Integração
A convenção para [testes de integração] em Rust é colocá-los em um diretório `tests` na raiz do projeto (ou seja, ao lado do diretório `src`). Tanto o framework de testes padrão quanto frameworks de testes customizados detectarão e executarão automaticamente todos os testes naquele diretório.
[testes de integração]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
Todos os testes de integração são seus próprios executáveis e completamente separados do nosso `main.rs`. Isso significa que cada teste precisa definir sua própria função de ponto de entrada. Vamos criar um teste de integração de exemplo chamado `basic_boot` para ver como funciona em detalhes:
```rust
// em tests/basic_boot.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
#[unsafe(no_mangle)] // não altere (mangle) o nome desta função
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
fn test_runner(tests: &[&dyn Fn()]) {
unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
loop {}
}
```
Como testes de integração são executáveis separados, precisamos fornecer todos os atributos da crate (`no_std`, `no_main`, `test_runner`, etc.) novamente. Também precisamos criar uma nova função de ponto de entrada `_start`, que chama a função de ponto de entrada de teste `test_main`. Não precisamos de nenhum atributo `cfg(test)` porque executáveis de teste de integração nunca são construídos em modo não-teste.
Usamos a macro [`unimplemented`] que sempre entra em panic como placeholder para a função `test_runner` e apenas fazemos `loop` no handler de `panic` por enquanto. Idealmente, queremos implementar essas funções exatamente como fizemos no nosso `main.rs` usando a macro `serial_println` e a função `exit_qemu`. O problema é que não temos acesso a essas funções porque os testes são construídos completamente separados do nosso executável `main.rs`.
[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html
Se você executar `cargo test` neste estágio, entrará em um loop infinito porque o handler de panic faz loop infinitamente. Você precisa usar o atalho de teclado `ctrl+c` para sair do QEMU.
### Criar uma Biblioteca
Para tornar as funções necessárias disponíveis para nosso teste de integração, precisamos separar uma biblioteca do nosso `main.rs`, que pode ser incluída por outras crates e executáveis de teste de integração. Para fazer isso, criamos um novo arquivo `src/lib.rs`:
```rust
// src/lib.rs
#![no_std]
```
Como o `main.rs`, o `lib.rs` é um arquivo especial que é automaticamente reconhecido pelo cargo. A biblioteca é uma unidade de compilação separada, então precisamos especificar o atributo `#![no_std]` novamente.
Para fazer nossa biblioteca funcionar com `cargo test`, precisamos também mover as funções de teste e atributos de `main.rs` para `lib.rs`:
```rust
// em src/lib.rs
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub trait Testable {
fn run(&self) -> ();
}
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run();
}
exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
/// Ponto de entrada para `cargo test`
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
test_panic_handler(info)
}
```
Para tornar nosso `test_runner` disponível para executáveis e testes de integração, o tornamos público e não aplicamos o atributo `cfg(test)` a ele. Também fatoramos a implementação do nosso handler de panic em uma função pública `test_panic_handler`, para que ela esteja disponível para executáveis também.
Como nosso `lib.rs` é testado independentemente do nosso `main.rs`, precisamos adicionar um ponto de entrada `_start` e um handler de panic quando a biblioteca é compilada em modo de teste. Ao usar o atributo de crate [`cfg_attr`], habilitamos condicionalmente o atributo `no_main` neste caso.
[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
Também movemos o enum `QemuExitCode` e a função `exit_qemu` e os tornamos públicos:
```rust
// em src/lib.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
Agora executáveis e testes de integração podem importar essas funções da biblioteca e não precisam definir suas próprias implementações. Para também tornar `println` e `serial_println` disponíveis, movemos as declarações de módulo também:
```rust
// em src/lib.rs
pub mod serial;
pub mod vga_buffer;
```
Tornamos os módulos públicos para torná-los utilizáveis fora da nossa biblioteca. Isso também é necessário para tornar nossas macros `println` e `serial_println` utilizáveis, já que elas usam as funções `_print` dos módulos.
Agora podemos atualizar nosso `main.rs` para usar a biblioteca:
```rust
// em src/main.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
use blog_os::println;
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
/// Esta função é chamada em caso de pânico.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
A biblioteca é utilizável como uma crate externa normal. É chamada `blog_os`, como nossa crate. O código acima usa a função `blog_os::test_runner` no atributo `test_runner` e a função `blog_os::test_panic_handler` no nosso handler de `panic` `cfg(test)`. Também importa a macro `println` para torná-la disponível para nossas funções `_start` e `panic`.
Neste ponto, `cargo run` e `cargo test` devem funcionar novamente. É claro que `cargo test` ainda faz loop infinitamente (você pode sair com `ctrl+c`). Vamos corrigir isso usando as funções necessárias da biblioteca no nosso teste de integração.
### Completando o Teste de Integração
Como nosso `src/main.rs`, nosso executável `tests/basic_boot.rs` pode importar tipos da nossa nova biblioteca. Isso nos permite importar os componentes faltantes para completar nosso teste:
```rust
// em tests/basic_boot.rs
#![test_runner(blog_os::test_runner)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
Em vez de reimplementar o test runner, usamos a função `test_runner` da nossa biblioteca mudando o atributo `#![test_runner(crate::test_runner)]` para `#![test_runner(blog_os::test_runner)]`. Então não precisamos mais da função stub `test_runner` em `basic_boot.rs`, então podemos removê-la. Para nosso handler de `panic`, chamamos a função `blog_os::test_panic_handler` como fizemos no nosso `main.rs`.
Agora `cargo test` sai normalmente novamente. Quando você o executa, verá que ele constrói e executa os testes para nosso `lib.rs`, `main.rs` e `basic_boot.rs` separadamente um após o outro. Para o `main.rs` e os testes de integração `basic_boot`, ele reporta "Running 0 tests" já que esses arquivos não têm nenhuma função anotada com `#[test_case]`.
Agora podemos adicionar testes ao nosso `basic_boot.rs`. Por exemplo, podemos testar que `println` funciona sem entrar em panic, como fizemos nos testes do buffer VGA:
```rust
// em tests/basic_boot.rs
use blog_os::println;
#[test_case]
fn test_println() {
println!("test_println output");
}
```
Quando executamos `cargo test` agora, vemos que ele encontra e executa a função de teste.
O teste pode parecer um pouco inútil agora já que é quase idêntico a um dos testes do buffer VGA. No entanto, no futuro, as funções `_start` do nosso `main.rs` e `lib.rs` podem crescer e chamar várias rotinas de inicialização antes de executar a função `test_main`, então os dois testes são executados em ambientes muito diferentes.
Ao testar `println` em um ambiente `basic_boot` sem chamar nenhuma rotina de inicialização em `_start`, podemos garantir que `println` funciona logo após o boot. Isso é importante porque dependemos dele, por exemplo, para imprimir mensagens de panic.
### Testes Futuros
O poder dos testes de integração é que eles são tratados como executáveis completamente separados. Isso lhes dá controle completo sobre o ambiente, o que torna possível testar que o código interage corretamente com a CPU ou dispositivos de hardware.
Nosso teste `basic_boot` é um exemplo muito simples de um teste de integração. No futuro, nosso kernel se tornará muito mais cheio de recursos e interagirá com o hardware de várias maneiras. Ao adicionar testes de integração, podemos garantir que essas interações funcionem (e continuem funcionando) como esperado. Algumas ideias para possíveis testes futuros são:
- **Exceções de CPU**: Quando o código executa operações inválidas (por exemplo, divide por zero), a CPU lança uma exceção. O kernel pode registrar funções handler para tais exceções. Um teste de integração poderia verificar que o handler de exceção correto é chamado quando uma exceção de CPU ocorre ou que a execução continua corretamente após uma exceção resolvível.
- **Tabelas de Página**: Tabelas de página definem quais regiões de memória são válidas e acessíveis. Ao modificar as tabelas de página, é possível alocar novas regiões de memória, por exemplo ao lançar programas. Um teste de integração poderia modificar as tabelas de página na função `_start` e verificar que as modificações têm os efeitos desejados nas funções `#[test_case]`.
- **Programas Userspace**: Programas userspace são programas com acesso limitado aos recursos do sistema. Por exemplo, eles não têm acesso a estruturas de dados do kernel ou à memória de outros programas. Um teste de integração poderia lançar programas userspace que executam operações proibidas e verificar que o kernel as impede todas.
Como você pode imaginar, muitos mais testes são possíveis. Ao adicionar tais testes, podemos garantir que não os quebramos acidentalmente quando adicionamos novos recursos ao nosso kernel ou refatoramos nosso código. Isso é especialmente importante quando nosso kernel se torna maior e mais complexo.
### Testes que Devem Entrar em Panic
O framework de testes da biblioteca padrão suporta um [atributo `#[should_panic]`][should_panic] que permite construir testes que devem falhar. Isso é útil, por exemplo, para verificar que uma função falha quando um argumento inválido é passado. Infelizmente, este atributo não é suportado em crates `#[no_std]` porque requer suporte da biblioteca padrão.
[should_panic]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panics
Embora não possamos usar o atributo `#[should_panic]` no nosso kernel, podemos obter comportamento similar criando um teste de integração que sai com um código de erro de sucesso do handler de panic. Vamos começar a criar tal teste com o nome `should_panic`:
```rust
// em tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{QemuExitCode, exit_qemu, serial_println};
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
Este teste ainda está incompleto, pois não define uma função `_start` ou nenhum dos atributos customizados de test runner ainda. Vamos adicionar as partes faltantes:
```rust
// em tests/should_panic.rs
#![feature(custom_test_frameworks)]
#![test_runner(test_runner)]
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
pub fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
}
exit_qemu(QemuExitCode::Success);
}
```
Em vez de reutilizar o `test_runner` do nosso `lib.rs`, o teste define sua própria função `test_runner` que sai com um código de saída de falha quando um teste retorna sem entrar em panic (queremos que nossos testes entrem em panic). Se nenhuma função de teste for definida, o runner sai com um código de erro de sucesso. Como o runner sempre sai após executar um único teste, não faz sentido definir mais de uma função `#[test_case]`.
Agora podemos criar um teste que deveria falhar:
```rust
// em tests/should_panic.rs
use blog_os::serial_print;
#[test_case]
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
```
O teste usa `assert_eq` para afirmar que `0` e `1` são iguais. É claro que isso falha, então nosso teste entra em panic como desejado. Note que precisamos imprimir manualmente o nome da função usando `serial_print!` aqui porque não usamos a trait `Testable`.
Quando executamos o teste através de `cargo test --test should_panic` vemos que ele é bem-sucedido porque o teste entrou em panic como esperado. Quando comentamos a assertion e executamos o teste novamente, vemos que ele de fato falha com a mensagem _"test did not panic"_.
Uma desvantagem significativa desta abordagem é que ela só funciona para uma única função de teste. Com múltiplas funções `#[test_case]`, apenas a primeira função é executada porque a execução não pode continuar após o handler de panic ter sido chamado. Atualmente não conheço uma boa maneira de resolver este problema, então me avise se você tiver uma ideia!
### Testes Sem Harness
Para testes de integração que têm apenas uma única função de teste (como nosso teste `should_panic`), o test runner realmente não é necessário. Para casos como este, podemos desabilitar o test runner completamente e executar nosso teste diretamente na função `_start`.
A chave para isso é desabilitar a flag `harness` para o teste no `Cargo.toml`, que define se um test runner é usado para um teste de integração. Quando está definido como `false`, tanto o test runner padrão quanto o recurso de test runner customizado são desabilitados, de modo que o teste é tratado como um executável normal.
Vamos desabilitar a flag `harness` para nosso teste `should_panic`:
```toml
# em Cargo.toml
[[test]]
name = "should_panic"
harness = false
```
Agora simplificamos vastamente nosso teste `should_panic` removendo o código relacionado ao `test_runner`. O resultado se parece com isto:
```rust
// em tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
should_fail();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
loop{}
}
fn should_fail() {
serial_print!("should_panic::should_fail...\t");
assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
Agora chamamos a função `should_fail` diretamente da nossa função `_start` e saímos com um código de saída de falha se ela retornar. Quando executamos `cargo test --test should_panic` agora, vemos que o teste se comporta exatamente como antes.
Além de criar testes `should_panic`, desabilitar o atributo `harness` também pode ser útil para testes de integração complexos, por exemplo, quando as funções de teste individuais têm efeitos colaterais e precisam ser executadas em uma ordem especificada.
## Resumo
Testes são uma técnica muito útil para garantir que certos componentes tenham o comportamento desejado. Mesmo que não possam mostrar a ausência de bugs, ainda são uma ferramenta útil para encontrá-los e especialmente para evitar regressões.
Este post explicou como configurar um framework de testes para nosso kernel Rust. Usamos o recurso de frameworks de teste customizados do Rust para implementar suporte para um atributo `#[test_case]` simples no nosso ambiente bare metal. Usando o dispositivo `isa-debug-exit` do QEMU, nosso test runner pode sair do QEMU após executar os testes e reportar o status do teste. Para imprimir mensagens de erro no console em vez do buffer VGA, criamos um driver básico para a porta serial.
Após criar alguns testes para nossa macro `println`, exploramos testes de integração na segunda metade do post. Aprendemos que eles vivem no diretório `tests` e são tratados como executáveis completamente separados. Para dar a eles acesso à função `exit_qemu` e à macro `serial_println`, movemos a maior parte do nosso código para uma biblioteca que pode ser importada por todos os executáveis e testes de integração. Como testes de integração são executados em seu próprio ambiente separado, eles tornam possível testar interações com o hardware ou criar testes que devem entrar em panic.
Agora temos um framework de testes que executa em um ambiente realista dentro do QEMU. Ao criar mais testes em posts futuros, podemos manter nosso kernel sustentável quando ele se tornar mais complexo.
## O que vem a seguir?
No próximo post, exploraremos _exceções de CPU_. Essas exceções são lançadas pela CPU quando algo ilegal acontece, como uma divisão por zero ou um acesso a uma página de memória não mapeada (um chamado "page fault"). Ser capaz de capturar e examinar essas exceções é muito importante para depuração de erros futuros. O tratamento de exceções também é muito similar ao tratamento de interrupções de hardware, que é necessário para suporte a teclado.
================================================
FILE: blog/content/edition-2/posts/04-testing/index.zh-CN.md
================================================
+++
title = "内核测试"
weight = 4
path = "zh-CN/testing"
date = 2019-04-27
[extra]
# Please update this when updating the translation
translation_based_on_commit = "e6c148d6f47bcf8a34916393deaeb7e8da2d5e2a"
# GitHub usernames of the people that translated this post
translators = ["luojia65", "Rustin-Liu", "liuyuran","ic3w1ne"]
# GitHub usernames of the people that contributed to this translation
translation_contributors = ["JiangengDong"]
+++
本文主要讲述了在`no_std`环境下进行单元测试和集成测试的方法。我们将通过Rust的自定义测试框架来在我们的内核中执行一些测试函数。为了将结果反馈到QEMU上,我们需要使用QEMU的一些其他的功能以及`bootimage`工具。
这个系列的blog在[GitHub]上开放开发,如果你有任何问题,请在这里开一个issue来讨论。当然你也可以在[底部][at the bottom]留言。你可以在[`post-04`][post branch]找到这篇文章的完整源码。
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-04
## 阅读要求
这篇文章替换了此前的(现在已经过时了) [_单元测试(Unit Testing)_][_Unit Testing_] 和 [_集成测试(Integration Tests)_][_Integration Tests_] 两篇文章。这里我将假定你是在2019-04-27日后阅读的[_最小Rust内核_][_A Minimal Rust Kernel_]一文。总而言之,本文要求你已经有一个[已设置默认目标][sets a default target]的 `.cargo/config` 文件且[定义了一个runner可执行文件][defines a runner executable]。
[_Unit Testing_]: @/edition-2/posts/deprecated/04-unit-testing/index.md
[_Integration Tests_]: @/edition-2/posts/deprecated/05-integration-tests/index.md
[_A Minimal Rust Kernel_]: @/edition-2/posts/02-minimal-rust-kernel/index.md
[sets a default target]: @/edition-2/posts/02-minimal-rust-kernel/index.md#set-a-default-target
[defines a runner executable]: @/edition-2/posts/02-minimal-rust-kernel/index.md#using-cargo-run
## Rust中的测试
Rust有一个**内置的测试框架**([built-in test framework][built-in test framework]):无需任何设置就可以进行单元测试,只需要创建一个通过assert来检查结果的函数并在函数的头部加上 `#[test]` 属性即可。然后 `cargo test` 会自动找到并执行你的crate中的所有测试函数。
[built-in test framework]: https://doc.rust-lang.org/book/second-edition/ch11-00-testing.html
为了启用内核二进制文件的测试功能,我们可以在 `Cargo.toml` 中将 `test` 标志设置为 `true`:
```toml
# 在 Cargo.toml 中
[[bin]]
name = "blog_os"
test = true
bench = false
```
这个 [`[[bin]]` 配置段](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#configuring-a-target) 指定了 `cargo` 应如何编译 `blog_os` 可执行文件。
其中 `test` 字段用于指定该可执行文件是否支持测试。
在第一篇文章中,我们为了 [使 `rust-analyzer` 正常](@/edition-2/posts/01-freestanding-rust-binary/index.md#making-rust-analyzer-happy) 将其设置为 `false`,但现在我们需要启用测试,因此将其重新设为 `true`。
不幸的是,对于像内核这样的 `no_std` 应用来说,测试会变得比较复杂。问题在于 Rust 的测试框架隐式地使用了内置的 [`test`][`test`] 库,而这个库依赖于标准库。这意味着我们无法为 `#[no_std]` 内核使用默认的测试框架。
[`test`]: https://doc.rust-lang.org/test/index.html
当我们试图在我们的项目中执行 `cargo test` 时,我们可以看到如下信息:
```
> cargo test
Compiling blog_os v0.1.0 (/…/blog_os)
error[E0463]: can't find crate for `test`
```
由于 `test` 库依赖于标准库,所以它在我们的裸机目标上并不可用。虽然将 `test` 库移植到一个 `#[no_std]` 上下文环境中是[可能的][utest],但是这样做是高度不稳定的,并且还会需要一些特殊的hacks,例如重定义 `panic` 宏。
[utest]: https://github.com/japaric/utest
### 自定义测试框架
幸运的是,Rust支持通过使用不稳定的**自定义测试框架**([`custom_test_frameworks`]) 功能来替换默认的测试框架。该功能不需要额外的库,因此在 `#[no_std]`环境中它也可以工作。它的工作原理是收集所有标注了 `#[test_case]`属性的函数,然后将这个测试函数的列表作为参数传递给用户指定的runner函数。因此,它实现了对测试过程的最大控制。
[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html
与默认的测试框架相比,它的缺点是有一些高级功能诸如 [`should_panic` tests] 都不可用了。相对的,如果需要这些功能,我们需要自己来实现。当然,这点对我们来说是好事,因为我们的环境非常特殊,在这个环境里,这些高级功能的默认实现无论如何都是无法工作的,举个例子, `#[should_panic]` 属性依赖于栈展开来捕获内核panic,而我们的内核早已将其禁用了。
[`should_panic` tests]: https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic
要为我们的内核实现自定义测试框架,我们需要将如下代码添加到我们的 `main.rs` 中去:
```rust
// in src/main.rs
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
}
```
我们的runner会打印一个简短的debug信息然后调用列表中的每个测试函数。参数类型 `&[&dyn Fn()]` 是[_Fn()_] trait的 [_trait object_] 引用的一个 [_slice_]。它基本上可以被看做一个可以像函数一样被调用的类型的引用列表。由于这个函数在不进行测试的时候没有什么用,这里我们使用 `#[cfg(test)]`属性保证它只会出现在测试中。
[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html
[_trait object_]: https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html
[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html
现在当我们运行 `cargo test` ,我们可以发现运行成功了。然而,我们看到的仍然是"Hello World"而不是我们的 `test_runner`传递来的信息。这是由于我们的入口点仍然是 `_start` 函数——自定义测试框架会生成一个`main`函数来调用`test_runner`,但是由于我们使用了 `#[no_main]`并提供了我们自己的入口点,所以这个`main`函数就被忽略了。
**Note:** cargo目前有个bug,就是某些测试用例会在执行 `cargo test` 时抛出 `duplicate lang item` 错误。目前已知的复现条件是在你的 `Cargo.toml` 中配置 `panic = "abort"`,只要移除掉,`cargo test` 即可正常执行。如果你对这个bug感兴趣,可以关注一下这个 [cargo issue](https://github.com/rust-lang/cargo/issues/7359)。
为了修复这个问题,我们需要通过 `reexport_test_harness_main`属性来将生成的函数的名称更改为与`main`不同的名称。然后我们可以在我们的`_start`函数里调用这个重命名的函数:
```rust
// in src/main.rs
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
```
我们将测试框架的入口函数的名字设置为`test_main`,并在我们的 `_start`入口点里调用它。通过使用**条件编译**([conditional compilation]),我们能够只在上下文环境为测试(test)时调用 `test_main` ,因为该函数将不在非测试上下文中生成。
现在当我们执行 `cargo test`时,我们可以看到我们的`test_runner`将"Running 0 tests"信息显示在屏幕上了。我们可以创建第一个测试函数了:
```rust
// in src/main.rs
#[test_case]
fn trivial_assertion() {
print!("trivial assertion... ");
assert_eq!(1, 1);
println!("[ok]");
}
```
现在,当我们运行 `cargo test` 时,我们可以看到如下输出:
![QEMU printing "Hello World!", "Running 1 tests", and "trivial assertion... [ok]"](https://os.phil-opp.com/testing/qemu-test-runner-output.png)
传递给 `test_runner`函数的`tests`切片里包含了一个 `trivial_assertion` 函数的引用,从屏幕上输出的 `trivial assertion... [ok]` 信息可见,我们的测试已被调用并且顺利通过。
在执行完tests后, `test_runner` 会将结果返回给 `test_main` 函数,而这个函数又返回到 `_start` 入口点函数——这样我们就进入了一个死循环,因为入口点函数是不允许返回的。这将导致一个问题:我们希望 `cargo test` 在所有的测试运行完毕后,直接返回并退出。
## 退出QEMU
现在我们在 `_start` 函数结束后进入了一个死循环,所以每次执行完 `cargo test` 后我们都需要手动去关闭QEMU;但是我们还想在没有用户交互的脚本环境下执行 `cargo test`。解决这个问题的最佳方式,是实现一个合适的方法来关闭我们的操作系统——不幸的是,这个方式实现起来相对有些复杂,因为这要求我们实现对[APM]或[ACPI]电源管理标准的支持。
[APM]: https://wiki.osdev.org/APM
[ACPI]: https://wiki.osdev.org/ACPI
幸运的是,还有一个绕开这些问题的办法:QEMU支持一种名为 `isa-debug-exit` 的特殊设备,它提供了一种从客户系统(guest system)里退出QEMU的简单方式。为了使用这个设备,我们需要向QEMU传递一个 `-device` 参数。当然,我们也可以通过将 `package.metadata.bootimage.test-args` 配置关键字添加到我们的 `Cargo.toml` 来达到目的:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
```
`bootimage runner` 会在QEMU的默认测试命令后添加 `test-args` 参数。(对于 `cargo run` 命令,这个参数会被忽略。)
在传递设备名 (`isa-debug-exit`)的同时,我们还传递了两个参数,`iobase` 和 `iosize` 。这两个参数指定了一个_I/O 端口_,我们的内核将通过它来访问设备。
### I/O 端口
在x86平台上,CPU和外围硬件通信通常有两种方式,**内存映射I/O**和**端口映射I/O**。之前,我们已经使用内存映射的方式,通过内存地址 `0xb8000` 访问了[VGA文本缓冲区]。该地址并没有映射到RAM,而是映射到了VGA设备的一部分内存上。
[VGA text buffer]: @/edition-2/posts/03-vga-text-buffer/index.md
与内存映射不同,端口映射I/O使用独立的I/O总线来进行通信。每个外围设备都有一个或数个端口号。CPU采用了特殊的`in`和`out`指令来和端口通信,这些指令要求一个端口号和一个字节的数据作为参数(有些这种指令的变体也允许发送 `u16` 或是 `u32` 长度的数据)。
`isa-debug-exit` 设备使用的就是端口映射I/O。其中, `iobase` 参数指定了设备对应的端口地址(在x86中,`0xf4` 是一个[通常未被使用的端口][list of x86 I/O ports]),而 `iosize` 则指定了端口的大小(`0x04` 代表4字节)。
[list of x86 I/O ports]: https://wiki.osdev.org/I/O_Ports#The_list
### 使用退出(Exit)设备
`isa-debug-exit` 设备的功能非常简单。当一个 `value` 写入 `iobase` 指定的端口时,它会导致QEMU以**退出状态**([exit status])`(value << 1) | 1` 退出。也就是说,当我们向端口写入 `0` 时,QEMU将以退出状态 `(0 << 1) | 1 = 1` 退出,而当我们向端口写入`1`时,它将以退出状态 `(1 << 1) | 1 = 3` 退出。
[exit status]: https://en.wikipedia.org/wiki/Exit_status
这里我们使用 [`x86_64`] crate提供的抽象,而不是手动调用 `in` 或 `out` 指令。为了添加对该crate的依赖,我们可以将其添加到我们的 `Cargo.toml`中的 `dependencies` 小节中去:
[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/
```toml
# in Cargo.toml
[dependencies]
x86_64 = "0.14.2"
```
现在我们可以使用crate中提供的 [`Port`] 类型来创建一个 `exit_qemu` 函数了:
[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html
```rust
// in src/main.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
该函数在 `0xf4` 处创建了一个新的端口,该端口同时也是 `isa-debug-exit` 设备的 `iobase` 。然后它会向端口写入传递的退出代码。这里我们使用 `u32` 来传递数据,因为我们之前已经将 `isa-debug-exit` 设备的 `iosize` 指定为4字节了。上述两个操作都是 `unsafe` 的,因为I/O端口的写入操作通常会导致一些不可预知的行为。
为了指定退出状态,我们创建了一个 `QemuExitCode` 枚举。思路大体上是,如果所有的测试均成功,就以成功退出码退出;否则就以失败退出码退出。这个枚举类型被标记为 `#[repr(u32)]`,代表每个变量都是一个 `u32` 的整数类型。我们使用退出代码 `0x10` 代表成功,`0x11` 代表失败。 实际的退出代码并不重要,只要它们不与QEMU的默认退出代码冲突即可。 例如,使用退出代码0表示成功可能并不是一个好主意,因为它在转换后就变成了 `(0 << 1) | 1 = 1` ,而 `1` 是QEMU运行失败时的默认退出代码。 这样,我们就无法将QEMU错误与成功的测试运行区分开来了。
现在我们来更新 `test_runner` 的代码,让程序在运行所有测试完毕后退出QEMU:
```rust
// in src/main.rs
fn test_runner(tests: &[&dyn Fn()]) {
println!("Running {} tests", tests.len());
for test in tests {
test();
}
/// new
exit_qemu(QemuExitCode::Success);
}
```
当我们现在运行 `cargo test` 时,QEMU会在测试运行后立刻退出。现在的问题是,即使我们传递了表示成功(`Success`)的退出代码, `cargo test` 依然会将所有的测试都视为失败:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be
Building bootloader
Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader)
Finished release [optimized + debuginfo] target(s) in 1.07s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4,
iosize=0x04`
error: test failed, to rerun pass '--bin blog_os'
```
这里的问题在于,`cargo test` 会将所有非 `0` 的错误码都视为测试失败。
### 成功退出(Exit)代码
为了解决这个问题, `bootimage` 提供了一个 `test-success-exit-code` 配置项,可以将指定的退出代码映射到退出代码 `0`:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = […]
test-success-exit-code = 33 # (0x10 << 1) | 1
```
有了这个配置,`bootimage` 就会将我们的成功退出码映射到退出码0;这样一来, `cargo test` 就能正确地识别出测试成功的情况,而不会将其视为测试失败。
我们的 test runner 现在会在正确报告测试结果后自动关闭QEMU。我们可以看到QEMU的窗口只会显示很短的时间——我们很难看清测试的结果。如果测试结果会打印在控制台上而不是QEMU里,让我们能在QEMU退出后仍然能看到测试结果就好了。
## 打印到控制台
要在控制台上查看测试输出,我们需要以某种方式将数据从内核发送到宿主系统。 有多种方法可以实现这一点,例如通过TCP网络接口来发送数据。但是,设置网络堆栈是一项很复杂的任务,这里我们可以选择更简单的解决方案。
### 串口
发送数据的一个简单的方式是通过[串行端口][serial port],这是一个现代电脑中已经不存在的旧标准接口(译者注:玩过单片机的同学应该知道,其实译者上大学的时候有些同学的笔记本电脑还有串口的,没有串口的同学在烧录单片机程序的时候也都会需要usb转串口线,一般是51,像stm32有st-link,这个另说,不过其实也可以用串口来下载)。串口非常易于编程,QEMU可以将通过串口发送的数据重定向到宿主机的标准输出或是文件中。
[serial port]: https://en.wikipedia.org/wiki/Serial_port
用来实现串行接口的芯片被称为 [UARTs]。在x86上,有[很多UART模型][lots of UART models],但是幸运的是,它们之间仅有的那些不同之处都是我们用不到的高级功能。目前通用的UARTs都会兼容[16550 UART],所以我们在我们测试框架里采用该模型。
[UARTs]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter
[lots of UART models]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#Models
[16550 UART]: https://en.wikipedia.org/wiki/16550_UART
我们使用 [`uart_16550`] crate来初始化UART,并通过串口来发送数据。为了将该crate添加为依赖,我们需要将 `Cargo.toml` 和 `main.rs` 修改为如下:
[`uart_16550`]: https://docs.rs/uart_16550
```toml
# in Cargo.toml
[dependencies]
uart_16550 = "0.2.0"
```
`uart_16550` crate包含了一个代表UART寄存器的 `SerialPort` 结构体,但是我们仍然需要自己来创建一个相应的实例。我们使用以下代码来创建一个新的串口模块 `serial`:
```rust
// in src/main.rs
mod serial;
```
```rust
// in src/serial.rs
use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;
lazy_static! {
pub static ref SERIAL1: Mutex = {
let mut serial_port = unsafe { SerialPort::new(0x3F8) };
serial_port.init();
Mutex::new(serial_port)
};
}
```
就像[VGA文本缓冲区][vga lazy-static]一样,我们使用 `lazy_static` 和一个自旋锁来创建一个 `static` writer实例。通过使用 `lazy_static` ,我们可以保证 `init` 方法只会在该示例第一次被使用使被调用。
和 `isa-debug-exit` 设备一样,UART也是通过I/O端口进行编程的。由于UART相对来讲更加复杂,它使用多个I/O端口来对不同的设备寄存器进行编程。`unsafe` 的 `SerialPort::new` 函数需要UART的第一个I/O端口的地址作为参数,从该地址中可以计算出所有所需端口的地址。我们传递的端口地址为 `0x3F8` ,该地址是第一个串行接口的标准端口号。
[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
为了使串口更加易用,我们添加了 `serial_print!` 和 `serial_println!`宏:
```rust
// in src/serial.rs
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
use core::fmt::Write;
SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
}
/// Prints to the host through the serial interface.
#[macro_export]
macro_rules! serial_print {
($($arg:tt)*) => {
$crate::serial::_print(format_args!($($arg)*));
};
}
/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! serial_println {
() => ($crate::serial_print!("\n"));
($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));
($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(
concat!($fmt, "\n"), $($arg)*));
}
```
该实现和我们此前的 `print` 和 `println` 宏的实现非常类似。 由于 `SerialPort` 类型已经实现了 [`fmt::Write`] trait,所以我们不需要提供我们自己的实现了。
[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html
现在我们可以从测试代码里向串行接口打印而不是向VGA文本缓冲区打印了:
```rust
// in src/main.rs
#[cfg(test)]
fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
[…]
}
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
注意,由于我们使用了 `#[macro_export]` 属性, `serial_println` 宏直接位于根命名空间下,所以通过 `use crate::serial::serial_println` 来导入该宏是不起作用的。
### QEMU参数
为了查看QEMU的串行输出,我们需要使用 `-serial` 参数将输出重定向到stdout:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio"
]
```
现在,当我们运行 `cargo test` 时,我们可以直接在控制台里看到测试输出了:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [ok]
```
然而,当测试失败时,我们仍然会在QEMU内看到输出结果,因为我们的panic handler还是用了 `println`。为了模拟这个过程,我们将我们的 `trivial_assertion` test中的断言(assertion)修改为 `assert_eq!(0, 1)`:

可以看到,panic信息被打印到了VGA缓冲区里,而测试输出则被打印到串口上了。panic信息非常有用,所以我们希望能够在控制台中来查看它。
### 在panic时打印一个错误信息
为了在panic时使用错误信息来退出QEMU,我们可以使用**条件编译**([conditional compilation])在测试模式下使用(与非测试模式下)不同的panic处理方式:
[conditional compilation]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html
```rust
// in src/main.rs
// our existing panic handler
#[cfg(not(test))] // new attribute
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
// our panic handler in test mode
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
```
在我们的测试panic处理中,我们用 `serial_println` 来代替 `println` 并使用失败代码来退出QEMU。注意,在 `exit_qemu` 调用后,我们仍然需要一个无限循环的 `loop` 因为编译器并不知道 `isa-debug-exit` 设备会导致程序退出。
现在,即使在测试失败的情况下QEMU仍然会退出,并会将一些有用的错误信息打印到控制台:
```
> cargo test
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [failed]
Error: panicked at 'assertion failed: `(left == right)`
left: `0`,
right: `1`', src/main.rs:65:5
```
由于现在所有的测试都将输出到控制台上,我们不再需要让QEMU窗口弹出一小会儿了——我们完全可以把窗口藏起来。
### 隐藏 QEMU
由于我们使用 `isa-debug-exit` 设备和串行端口来报告完整的测试结果,所以我们不再需要QEMU的窗口了。我们可以通过向QEMU传递 `-display none` 参数来将其隐藏:
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-args = [
"-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio",
"-display", "none"
]
```
现在QEMU完全在后台运行,且没有任何窗口会被打开。这不仅很清爽,还允许我们的测试框架在没有图形界面的环境里,诸如CI服务器或是[SSH]连接里运行。
[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
### 超时
由于 `cargo test` 会等待test runner退出,如果一个测试永远不返回那么它就会一直阻塞test runner。幸运的是,在实际应用中这并不是一个大问题,因为无限循环通常是很容易避免的。在我们的这个例子里,无限循环会发生在以下几种不同的情况中:
- bootloader加载内核失败,导致系统不停重启;
- BIOS/UEFI固件加载bootloader失败,同样会导致无限重启;
- CPU在某些函数结束时进入一个 `loop {}` 语句,例如因为QEMU的exit设备无法正常工作而导致死循环;
- 硬件触发了系统重置,例如未捕获CPU异常时(后续的文章将会详细解释)。
由于无限循环可能会在各种情况中发生,因此, `bootimage` 工具默认为每个可执行测试设置了一个长度为5分钟的超时时间。如果测试未在此时间内完成,则将其标记为失败,并向控制台输出"Timed Out(超时)"错误。这个功能确保了那些卡在无限循环里的测试不会一直阻塞 `cargo test`。
你可以将`loop {}`语句添加到 `trivial_assertion` 测试中来进行尝试。当你运行 `cargo test` 时,你可以发现该测试会在五分钟后被标记为超时。超时持续的时间可以通过Cargo.toml中的 `test-timeout` 配置项来进行[配置][bootimage config]:
[bootimage config]: https://github.com/rust-osdev/bootimage#configuration
```toml
# in Cargo.toml
[package.metadata.bootimage]
test-timeout = 300 # (in seconds)
```
如果你不想为了观察 `trivial_assertion` 测试超时等待5分钟之久,你可以将这个配置数值调低一些。
### 自动添加打印语句
`trivial_assertion` 测试仅能使用 `serial_print!`/`serial_println!` 输出自己的状态信息:
```rust
#[test_case]
fn trivial_assertion() {
serial_print!("trivial assertion... ");
assert_eq!(1, 1);
serial_println!("[ok]");
}
```
为每一个测试手动添加固定的日志实在是太烦琐了,所以我们可以修改一下 `test_runner` 把这部分逻辑改进一下,使其可以自动添加日志输出。那么我们先建立一个 `Testable` trait:
```rust
// in src/main.rs
pub trait Testable {
fn run(&self) -> ();
}
```
下面这个 trick 将会实现上面书写的 trait,并约束只有满足 [`Fn()` trait] 的泛型可使用这个实现:
[`Fn()` trait]: https://doc.rust-lang.org/stable/core/ops/trait.Fn.html
```rust
// in src/main.rs
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
```
我们实现的 `run` 函数中,首先使用 [`any::type_name`] 输出了函数名,这个函数事实上是被编译器实现的,可以返回任意类型的字符串形式。对于函数而言,其类型的字符串形式就是它的函数名,而函数名也正是我们想要的测试用例名称。至于 `\t` 则代表 [制表符][tab character],其作用是为后面的 `[ok]` 输出增加一点左边距。
[`any::type_name`]: https://doc.rust-lang.org/stable/core/any/fn.type_name.html
[tab character]: https://en.wikipedia.org/wiki/Tab_character
输出函数名之后,我们通过 `self()` 调用了测试函数本身,该调用方式属于 `Fn()` trait 独有,如果测试函数顺利执行完毕,则 `[ok]` 也会被输出出来。
最后一步就是给 `test_runner` 的参数附加上 `Testable` trait:
```rust
// in src/main.rs
#[cfg(test)]
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run(); // new
}
exit_qemu(QemuExitCode::Success);
}
```
仅有的两处修改,就是将 `tests` 参数的类型从 `&[&dyn Fn()]` 改为了 `&[&dyn Testable]`,以及将函数调用方式从 `test()` 改成了 `test.run()`。
由于我们已经完成了首尾输出的自动化,所以 `trivial_assertion` 里那两行输出语句也就可以删掉了:
```rust
// in src/main.rs
#[test_case]
fn trivial_assertion() {
assert_eq!(1, 1);
}
```
现在 `cargo test` 的输出就变成了下面这样:
```
Running 1 tests
blog_os::trivial_assertion... [ok]
```
如你所见,自动生成的函数名包含了完整的内部路径,但是也因此可以区分不同模块下的同名函数。除此之外,其输出和之前看起来完全相同,我们也就不再需要在测试函数内部加输出语句了。
## 测试VGA缓冲区
现在我们已经有了一个可以工作的测试框架了,我们可以为我们的VGA缓冲区实现创建一些测试。首先,我们创建了一个非常简单的测试来验证 `println`是否正常运行而不会panic:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_simple() {
println!("test_println_simple output");
}
```
这个测试所做的仅仅是将一些内容打印到VGA缓冲区。如果它正常结束并且没有panic,也就意味着 `println` 调用也没有panic。
为了确保即使打印很多行且有些行超出屏幕的情况下也没有panic发生,我们可以创建另一个测试:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_many() {
for _ in 0..200 {
println!("test_println_many output");
}
}
```
我们还可以创建另一个测试函数,来验证打印的几行字符是否真的出现在了屏幕上:
```rust
// in src/vga_buffer.rs
#[test_case]
fn test_println_output() {
let s = "Some test string that fits on a single line";
println!("{}", s);
for (i, c) in s.chars().enumerate() {
let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
assert_eq!(char::from(screen_char.ascii_character), c);
}
}
```
该函数定义了一个测试字符串,并通过 `println`将其输出,然后遍历静态 `WRITER` 也就是vga字符缓冲区的屏幕字符。由于 `println` 在将字符串打印到屏幕上最后一行后会立刻附加一个新行(即输出完后有一个换行符),所以这个字符串应该会出现在第 `BUFFER_HEIGHT - 2`行。
通过使用[`enumerate`] ,我们统计了变量 `i` 的迭代次数,然后用它来加载对应于`c`的屏幕字符。 通过比较屏幕字符的 `ascii_character` 和 `c` ,我们可以确保字符串的每个字符确实出现在vga文本缓冲区中。
[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate
如你所想,我们可以创建更多的测试函数:例如一个用来测试当打印一个很长的且包装正确的行时是否会发生panic的函数,或是一个用于测试换行符、不可打印字符、非unicode字符是否能被正确处理的函数。
在这篇文章的剩余部分,我们还会解释如何创建一个 _集成测试_ 以测试不同组件之间的交互。
## 集成测试
在Rust中,**集成测试**([integration tests])的约定是将其放到项目根目录中的 `tests` 目录下(即 `src` 的同级目录)。无论是默认测试框架还是自定义测试框架都将自动获取并执行该目录下所有的测试。
[integration tests]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests
所有的集成测试都是它们自己的可执行文件,并且与我们的 `main.rs` 完全独立。这也就意味着每个测试都需要定义它们自己的函数入口点。让我们创建一个名为 `basic_boot` 的例子来看看集成测试的工作细节吧:
```rust
// in tests/basic_boot.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
#[unsafe(no_mangle)] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
fn test_runner(tests: &[&dyn Fn()]) {
unimplemented!();
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
loop {}
}
```
由于集成测试都是单独的可执行文件,所以我们需要再次提供所有的crate属性(`no_std`, `no_main`, `test_runner`, 等等)。我们还需要创建一个新的入口点函数 `_start`,用于调用测试入口函数 `test_main`。我们不需要任何的 `cfg(test)` 属性,因为集成测试的二进制文件在非测试模式下根本不会被编译构建。
这里我们采用[`unimplemented`]宏,充当 `test_runner` 暂未实现的占位符;添加简单的 `loop {}` 循环,作为 `panic` 处理器的内容。理想情况下,我们希望能向我们在 `main.rs` 里所做的一样使用 `serial_println` 宏和 `exit_qemu` 函数来实现这个函数。但问题是,由于这些测试的构建和我们的 `main.rs` 的可执行文件是完全独立的,我们没有办法使用这些函数。
[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html
如果现阶段你运行 `cargo test`,你将进入一个无限循环,因为目前panic的处理就是进入无限循环。你需要使用快捷键 `Ctrl+c`,才可以退出QEMU。
### 创建一个库
为了让这些函数能在我们的集成测试中使用,我们需要从我们的 `main.rs` 中分割出一个库,这个库应当可以被其他的crate和集成测试可执行文件使用。为了达成这个目的,我们创建了一个新文件,`src/lib.rs`:
```rust
// src/lib.rs
#![no_std]
```
和 `main.rs` 一样,`lib.rs` 也是一个可以被cargo自动识别的特殊文件。该库是一个独立的编译单元,所以我们需要再次指定 `#![no_std]` 属性。
为了让我们的库可以和 `cargo test` 一起协同工作,我们还需要移动以下测试函数和属性:
```rust
// in src/lib.rs
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub trait Testable {
fn run(&self) -> ();
}
impl Testable for T
where
T: Fn(),
{
fn run(&self) {
serial_print!("{}...\t", core::any::type_name::());
self();
serial_println!("[ok]");
}
}
pub fn test_runner(tests: &[&dyn Testable]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test.run();
}
exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {
serial_println!("[failed]\n");
serial_println!("Error: {}\n", info);
exit_qemu(QemuExitCode::Failed);
loop {}
}
/// Entry point for `cargo test`
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
test_panic_handler(info)
}
```
为了能在可执行文件和集成测试中使用 `test_runner`,我们不对其应用 `cfg(test)` 属性,并将其设置为public。同时,我们还将panic的处理程序分解为public函数 `test_panic_handler`,这样一来它也可以用于可执行文件了。
由于我们的 `lib.rs` 是独立于 `main.rs` 进行测试的,因此当该库实在测试模式下编译时我们需要添加一个 `_start` 入口点和一个panic处理程序。通过使用[`cfg_attr`] ,我们可以在这种情况下有条件地启用 `no_main` 属性。
[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute
我们还将 `QemuExitCode` 枚举和 `exit_qemu` 函数从main.rs移动过来,并将其设置为公有函数:
```rust
// in src/lib.rs
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
Success = 0x10,
Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {
use x86_64::instructions::port::Port;
unsafe {
let mut port = Port::new(0xf4);
port.write(exit_code as u32);
}
}
```
现在,可执行文件和集成测试都可以从库中导入这些函数,而不需要实现自己的定义。为了使 `println` 和 `serial_println` 可用,我们将以下的模块声明代码也移动到 `lib.rs` 中:
```rust
// in src/lib.rs
pub mod serial;
pub mod vga_buffer;
```
我们将这些模块设置为public(公有),这样一来我们在库的外部也一样能使用它们了。由于这两者都用了该模块内的 `_print` 函数,所以这也是让 `println` 和 `serial_println` 宏可用的必要条件。
现在我们修改我们的 `main.rs` 代码来使用该库:
```rust
// src/main.rs
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
use blog_os::println;
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
#[cfg(test)]
test_main();
loop {}
}
/// This function is called on panic.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
可以看到,这个库用起来就像一个普通的外部crate。它的调用方法与其它crate无异;在我们的这个例子中,位置可能为 `blog_os`。上述代码使用了 `test_runner` 属性中的 `blog_os::test_runner` 函数和 `cfg(test)` 的panic处理中的 `blog_os::test_panic_handler` 函数。它还导入了 `println` 宏,这样一来,我们可以在我们的 `_start` 和 `panic` 中使用它了。
与此同时,`cargo run` 和 `cargo test`可以再次正常工作了。当然了,`cargo test`仍然会进入无限循环(你可以通过`ctrl+c`来退出),接下来我们将在集成测试中通过所需要的库函数来修复这个问题。
### 完成集成测试
就像我们的 `src/main.rs`,我们的 `tests/basic_boot.rs` 可执行文件同样可以从我们的新库中导入类型。这也就意味着我们可以导入缺失的组件来完成我们的测试。
```rust
// in tests/basic_boot.rs
#![test_runner(blog_os::test_runner)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
blog_os::test_panic_handler(info)
}
```
这里我们使用我们的库中的 `test_runner` 函数,而不是重新实现一个test runner。至于panic处理,调用 `blog_os::test_panic_handler` 函数即可,就像我们之前在我们的 `main.rs` 里面做的一样。
现在,`cargo test`又可以正常退出了。当你运行该命令时,你会发现它为我们的 `lib.rs`, `main.rs`, 和 `basic_boot.rs` 分别构建并运行了测试。其中,对于 `main.rs` 和 `basic_boot` 的集成测试,它会报告"Running 0 tests"(正在运行0个测试),因为这些文件里面没有任何用 `#[test_case]`标注的函数。
现在我们可以在`basic_boot.rs`中添加测试了。举个例子,我们可以测试`println`是否能够正常工作而不panic,就像我们之前在vga缓冲区测试中做的那样:
```rust
// in tests/basic_boot.rs
use blog_os::println;
#[test_case]
fn test_println() {
println!("test_println output");
}
```
现在当我们运行`cargo test`时,我们可以看到它会寻找并执行这些测试函数。
由于该测试和vga缓冲区测试中的一个几乎完全相同,所以目前它看起来似乎没什么用。然而在将来,我们的 `main.rs` 和 `lib.rs` 中的 `_start` 函数的内容会不断增长,并且在运行 `test_main` 之前需要调用一系列的初始化进程,所以这两个测试将会运行在完全不同的环境中(译者注:也就是说虽然现在看起来差不多,但是在将来该测试和vga buffer中的测试会很不一样,有必要单独拿出来,这两者并没有重复)。
通过在 `basic_boot` 环境里不调用任何初始化例程的 `_start` 中测试 `println` 函数,我们可以确保 `println` 在启动(boot)后可以正常工作。这一点非常重要,因为我们有很多部分依赖于 `println`,例如打印panic信息。
### 未来的测试
集成测试的强大之处在于,它们可以被看成是完全独立的可执行文件;这也给了它们完全控制环境的能力,使得他们能够测试代码和CPU或是其他硬件的交互是否正确。
我们的 `basic_boot` 测试正是集成测试的一个非常简单的例子。在将来,我们的内核的功能会变得更多,和硬件交互的方式也会变得多种多样。通过添加集成测试,我们可以保证这些交互按预期工作(并一直保持工作)。下面是一些对于未来的测试的设想:
- **CPU异常**:当代码执行无效操作(例如除以零)时,CPU就会抛出异常。内核会为这些异常注册处理函数。集成测试可以验证在CPU异常时是否调用了正确的异常处理程序,或者在可解析的异常之后程序是否能正确执行;
- **页表**:页表定义了哪些内存区域是有效且可访问的。通过修改页表,可以重新分配新的内存区域,例如,当你启动一个软件的时候。我们可以在集成测试中调整 `_start` 函数中的一些页表项,并确认这些改动是否会对 `#[test_case]` 的函数产生影响;
- **用户空间程序**:用户空间程序是只能访问有限的系统资源的程序。例如,他们无法访问内核数据结构或是其他应用程序的内存。集成测试可以启动执行禁止操作的用户空间程序验证认内核是否会将这些操作全都阻止。
可以想象,还有更多的测试可以进行。通过添加各种各样的测试,我们确保在为我们的内核添加新功能或是重构代码时,不会意外地破坏他们。这一点在我们的内核变得更大和更复杂的时候显得尤为重要。
### 那些应该Panic的测试
标准库的测试框架支持 [`#[should_panic]` 属性][should_panic],这允许我们构造理应失败的测试。这个功能对于验证传递无效参数时函数是否会失败非常有用。不幸的是,这个属性需要标准库的支持,因此,在 `#[no_std]` 环境下无法使用。
[should_panic]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panics
尽管我们不能在我们的内核中使用 `#[should_panic]` 属性,但是通过创建一个集成测试我们可以达到类似的效果——该集成测试可以从panic处理程序中返回一个成功错误代码。接下来让我一起来创建一个如上所述名为 `should_panic` 的测试吧:
```rust
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{QemuExitCode, exit_qemu, serial_println};
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
这个测试还没有完成,因为它尚未定义 `_start` 函数或是其他自定义的test runner属性。让我们来补充缺少的内容吧:
```rust
// in tests/should_panic.rs
#![feature(custom_test_frameworks)]
#![test_runner(test_runner)]
#![reexport_test_harness_main = "test_main"]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
test_main();
loop {}
}
pub fn test_runner(tests: &[&dyn Fn()]) {
serial_println!("Running {} tests", tests.len());
for test in tests {
test();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
}
exit_qemu(QemuExitCode::Success);
}
```
这个测试定义了自己的 `test_runner` 函数,而不是复用 `lib.rs` 中的 `test_runner`,该函数会在测试没有panic而是正常退出时返回一个错误退出代码(因为这里我们希望测试会panic)。如果没有定义测试函数,runner就会以一个成功错误代码退出。由于这个runner总是在执行完单个的测试后就退出,因此定义超过一个 `#[test_case]` 的函数都是没有意义的。
现在我们来创建一个应该失败的测试:
```rust
// in tests/should_panic.rs
use blog_os::serial_print;
#[test_case]
fn should_fail() {
serial_print!("should_fail... ");
assert_eq!(0, 1);
}
```
该测试用 `assert_eq`来断言(assert)`0` 和 `1` 是否相等。毫无疑问,这当然会失败(`0` 当然不等于 `1`),所以我们的测试就会像我们想要的那样panic。
当我们通过 `cargo test --test should_panic` 运行该测试时,我们会发现测试成功,该测试如我们预期的那样panic了。当我们将断言部分(即 `assert_eq!(0, 1);`)注释掉后,我们就会发现测试失败,并返回了 _"test did not panic"_ 的信息。
这种方法的缺点是它只使用于单个的测试函数。对于多个 `#[test_case]` 函数,它只会执行第一个函数,因为程序无法在panic处理被调用后继续执行。我目前没有想到解决这个问题的方法,如果你有任何想法,请务必告诉我!
### 无约束测试
对于那些只有单个测试函数的集成测试而言(例如我们的 `should_panic` 测试),其实并不需要test runner。对于这种情况,我们可以完全禁用test runner,直接在 `_start` 函数中直接运行我们的测试。
这里的关键就是在 `Cargo.toml` 中为测试禁用 `harness` flag,这个标志(flag)定义了是否将test runner用于集成测试中。如果该标志位被设置为 `false`,那么默认的test runner和自定义的test runner功能都将被禁用,这样一来该测试就可以像一个普通的可执行程序一样运行了。
现在为我们的 `should_panic` 测试禁用 `harness` flag吧:
```toml
# in Cargo.toml
[[test]]
name = "should_panic"
harness = false
```
现在我们通过移除test runner相关的代码,大大简化了我们的 `should_panic` 测试。结果看起来如下:
```rust
// in tests/should_panic.rs
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use blog_os::{QemuExitCode, exit_qemu, serial_println, serial_print};
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
should_fail();
serial_println!("[test did not panic]");
exit_qemu(QemuExitCode::Failed);
loop{}
}
fn should_fail() {
serial_print!("should_fail... ");
assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
serial_println!("[ok]");
exit_qemu(QemuExitCode::Success);
loop {}
}
```
现在我们可以通过我们的 `_start` 函数来直接调用 `should_fail` 函数了,如果返回则返回一个失败退出代码并退出。现在当我们执行 `cargo test --test should_panic` 时,我们可以发现测试的行为和之前完全一样。
除了创建 `should_panic` 测试,禁用 `harness` 属性对复杂集成测试也很有用,例如,当单个测试函数会产生一些边际效应,需要通过特定的顺序执行时。
## 总结
测试是一种非常有用的技术,它能确保特定的部件拥有我们期望的行为。即使它们不能显示是否有bug,它们仍然是用来寻找bug的利器,尤其是用来避免回归。
本文讲述了如何为我们的Rust kernel创建一个测试框架。我们使用Rust的自定义框架功能为我们的裸机环境实现了一个简单的 `#[test_case]` 属性支持。通过使用QEMU的 `isa-debug-exit` 设备,我们的test runner可以在运行测试后退出QEMU并报告测试状态。我们还为串行端口实现了一个简单的驱动,使得错误信息可以被打印到控制台而不是VGA buffer中。
在为我们的 `println` 宏创建了一些测试后,我们在本文的后半部分还探索了集成测试。我们了解到它们位于 `tests` 目录中,并被视为完全独立的可执行文件。为了使他们能够使用 `exit_qemu` 函数和 `serial_println` 宏,我们将大部分代码移动到一个库里,使其能够被导入到所有可执行文件和集成测试中。由于集成测试在各自独立的环境中运行,所以能够测试与硬件的交互或是创建应该panic的测试。
我们现在有了一个在QEMU内部真实环境中运行的测试框架。在未来的文章里,我们会创建更多的测试,从而让我们的内核在变得更复杂的同时保持可维护性。
## 下期预告
在下一篇文章中,我们将会探索 _CPU异常_。这些异常将在一些非法事件发生时由CPU抛出,例如抛出除以零或是访问没有映射的内存页(通常也被称为 `page fault` 即页异常)。能够捕获和检查这些异常,对将来的调试来说是非常重要的。异常处理与键盘支持所需的硬件中断处理十分相似。
================================================
FILE: blog/content/edition-2/posts/05-cpu-exceptions/index.es.md
================================================
+++
title = "Excepciones de CPU"
weight = 5
path = "es/cpu-exceptions"
date = 2018-06-17
[extra]
chapter = "Interrupciones"
# GitHub usernames of the people that translated this post
translators = ["dobleuber"]
+++
Las excepciones de CPU ocurren en diversas situaciones erróneas, por ejemplo, al acceder a una dirección de memoria inválida o al dividir por cero. Para reaccionar ante ellas, tenemos que configurar una _tabla de descriptores de interrupción_ (IDT, por sus siglas en inglés) que proporcione funciones manejadoras. Al final de esta publicación, nuestro núcleo será capaz de capturar [excepciones de punto de interrupción] y reanudar la ejecución normal después.
[excepciones de punto de interrupción]: https://wiki.osdev.org/Exceptions#Breakpoint
Este blog se desarrolla abiertamente en [GitHub]. Si tiene algún problema o pregunta, por favor abra un problema allí. También puede dejar comentarios [al final]. El código fuente completo de esta publicación se puede encontrar en la rama [`post-05`][post branch].
[GitHub]: https://github.com/phil-opp/blog_os
[al final]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05
## Descripción general
Una excepción indica que algo está mal con la instrucción actual. Por ejemplo, la CPU emite una excepción si la instrucción actual intenta dividir por 0. Cuando se produce una excepción, la CPU interrumpe su trabajo actual y llama inmediatamente a una función manejadora de excepciones específica, dependiendo del tipo de excepción.
En x86, hay alrededor de 20 tipos diferentes de excepciones de CPU. Las más importantes son:
- **Fallo de página**: Un fallo de página ocurre en accesos a memoria ilegales. Por ejemplo, si la instrucción actual intenta leer de una página no mapeada o intenta escribir en una página de solo lectura.
- **Código de operación inválido**: Esta excepción ocurre cuando la instrucción actual es inválida, por ejemplo, cuando intentamos usar nuevas [instrucciones SSE] en una CPU antigua que no las soporta.
- **Fallo de protección general**: Esta es la excepción con el rango más amplio de causas. Ocurre en varios tipos de violaciones de acceso, como intentar ejecutar una instrucción privilegiada en código de nivel de usuario o escribir en campos reservados en registros de configuración.
- **Doble fallo**: Cuando ocurre una excepción, la CPU intenta llamar a la función manejadora correspondiente. Si ocurre otra excepción _mientras se llama a la función manejadora de excepciones_, la CPU genera una excepción de doble fallo. Esta excepción también ocurre cuando no hay una función manejadora registrada para una excepción.
- **Triple fallo**: Si ocurre una excepción mientras la CPU intenta llamar a la función manejadora de doble fallo, emite un _triple fallo_ fatal. No podemos capturar o manejar un triple fallo. La mayoría de los procesadores reaccionan reiniciándose y reiniciando el sistema operativo.
[instrucciones SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
Para ver la lista completa de excepciones, consulte la [wiki de OSDev][exceptions].
[exceptions]: https://wiki.osdev.org/Exceptions
### La tabla de descriptores de interrupción
Para poder capturar y manejar excepciones, tenemos que configurar una llamada _tabla de descriptores de interrupción_ (IDT). En esta tabla, podemos especificar una función manejadora para cada excepción de CPU. El hardware utiliza esta tabla directamente, por lo que necesitamos seguir un formato predefinido. Cada entrada debe tener la siguiente estructura de 16 bytes:
| Tipo | Nombre | Descripción |
| ---- | ------------------------- | ----------------------------------------------------------------------- |
| u16 | Puntero a función [0:15] | Los bits más bajos del puntero a la función manejadora. |
| u16 | Selector GDT | Selector de un segmento de código en la [tabla de descriptores global]. |
| u16 | Opciones | (ver abajo) |
| u16 | Puntero a función [16:31] | Los bits del medio del puntero a la función manejadora. |
| u32 | Puntero a función [32:63] | Los bits restantes del puntero a la función manejadora. |
| u32 | Reservado |
[tabla de descriptores global]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
El campo de opciones tiene el siguiente formato:
| Bits | Nombre | Descripción |
| ----- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| 0-2 | Índice de tabla de pila de interrupción | 0: No cambiar pilas, 1-7: Cambiar a la n-ésima pila en la Tabla de Pila de Interrupción cuando se llama a este manejador. |
| 3-7 | Reservado |
| 8 | 0: Puerta de interrupción, 1: Puerta de trampa | Si este bit es 0, las interrupciones están deshabilitadas cuando se llama a este manejador. |
| 9-11 | debe ser uno |
| 12 | debe ser cero |
| 13‑14 | Nivel de privilegio del descriptor (DPL) | El nivel mínimo de privilegio requerido para llamar a este manejador. |
| 15 | Presente |
Cada excepción tiene un índice de IDT predefinido. Por ejemplo, la excepción de código de operación inválido tiene índice de tabla 6 y la excepción de fallo de página tiene índice de tabla 14. Así, el hardware puede cargar automáticamente la entrada de IDT correspondiente para cada excepción. La [Tabla de Excepciones][exceptions] en la wiki de OSDev muestra los índices de IDT de todas las excepciones en la columna “Vector nr.”.
Cuando ocurre una excepción, la CPU realiza aproximadamente lo siguiente:
1. Empuja algunos registros en la pila, incluyendo el puntero de instrucción y el registro [RFLAGS]. (Usaremos estos valores más adelante en esta publicación.)
2. Lee la entrada correspondiente de la tabla de descriptores de interrupción (IDT). Por ejemplo, la CPU lee la 14ª entrada cuando ocurre un fallo de página.
3. Verifica si la entrada está presente y, si no, genera un doble fallo.
4. Deshabilita las interrupciones de hardware si la entrada es una puerta de interrupción (bit 40 no establecido).
5. Carga el selector [GDT] especificado en el CS (segmento de código).
6. Salta a la función manejadora especificada.
[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register
[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
No se preocupe por los pasos 4 y 5 por ahora; aprenderemos sobre la tabla de descriptores global y las interrupciones de hardware en publicaciones futuras.
## Un tipo de IDT
En lugar de crear nuestro propio tipo de IDT, utilizaremos la estructura [`InterruptDescriptorTable`] del crate `x86_64`, que luce así:
[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
``` rust
#[repr(C)]
pub struct InterruptDescriptorTable {
pub divide_by_zero: Entry,
pub debug: Entry,
pub non_maskable_interrupt: Entry,
pub breakpoint: Entry,
pub overflow: Entry,
pub bound_range_exceeded: Entry,
pub invalid_opcode: Entry,
pub device_not_available: Entry,
pub double_fault: Entry,
pub invalid_tss: Entry,
pub segment_not_present: Entry,
pub stack_segment_fault: Entry,
pub general_protection_fault: Entry,
pub page_fault: Entry,
pub x87_floating_point: Entry,
pub alignment_check: Entry,
pub machine_check: Entry,
pub simd_floating_point: Entry,
pub virtualization: Entry,
pub security_exception: Entry,
// algunos campos omitidos
}
```
Los campos tienen el tipo [`idt::Entry`], que es una estructura que representa los campos de una entrada de IDT (ver tabla anterior). El parámetro de tipo `F` define el tipo esperado de la función manejadora. Vemos que algunas entradas requieren un [`HandlerFunc`] y algunas entradas requieren un [`HandlerFuncWithErrCode`]. El fallo de página incluso tiene su propio tipo especial: [`PageFaultHandlerFunc`].
[`idt::Entry`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.Entry.html
[`HandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFunc.html
[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFuncWithErrCode.html
[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.PageFaultHandlerFunc.html
Veamos primero el tipo `HandlerFunc`:
```rust
type HandlerFunc = extern "x86-interrupt" fn(_: InterruptStackFrame);
```
Es un [alias de tipo] para un tipo de `extern "x86-interrupt" fn`. La palabra clave `extern` define una función con una [convención de llamada foránea] y se utiliza a menudo para comunicarse con código C (`extern "C" fn`). Pero, ¿cuál es la convención de llamada `x86-interrupt`?
[alias de tipo]: https://doc.rust-lang.org/book/ch20-03-advanced-types.html#creating-type-synonyms-with-type-aliases
[convención de llamada foránea]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions
## La convención de llamada de interrupción
Las excepciones son bastante similares a las llamadas a funciones: la CPU salta a la primera instrucción de la función llamada y la ejecuta. Después, la CPU salta a la dirección de retorno y continúa la ejecución de la función madre.
Sin embargo, hay una gran diferencia entre excepciones y llamadas a funciones: una llamada a función es invocada voluntariamente por una instrucción `call` insertada por el compilador, mientras que una excepción puede ocurrir en _cualquier_ instrucción. Para entender las consecuencias de esta diferencia, necesitamos examinar las llamadas a funciones en más detalle.
[Convenciones de llamada] especifican los detalles de una llamada a función. Por ejemplo, especifican dónde se colocan los parámetros de la función (por ejemplo, en registros o en la pila) y cómo se devuelven los resultados. En x86_64 Linux, se aplican las siguientes reglas para funciones C (especificadas en el [ABI de System V]):
[Convenciones de llamada]: https://en.wikipedia.org/wiki/Calling_convention
[ABI de System V]: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
- los primeros seis argumentos enteros se pasan en los registros `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`
- argumentos adicionales se pasan en la pila
- los resultados se devuelven en `rax` y `rdx`
Tenga en cuenta que Rust no sigue el ABI de C (de hecho, [ni siquiera hay un ABI de Rust todavía][rust abi]), por lo que estas reglas solo se aplican a funciones declaradas como `extern "C" fn`.
[rust abi]: https://github.com/rust-lang/rfcs/issues/600
### Registros preservados y de uso
La convención de llamada divide los registros en dos partes: registros _preservados_ y registros _de uso_.
Los valores de los registros _preservados_ deben permanecer sin cambios a través de llamadas a funciones. Por lo tanto, una función llamada (la _“llamada”_) solo puede sobrescribir estos registros si restaura sus valores originales antes de retornar. Por ello, estos registros se llaman _“guardados por el llamado”_. Un patrón común es guardar estos registros en la pila al inicio de la función y restaurarlos justo antes de retornar.
En contraste, una función llamada puede sobrescribir registros _de uso_ sin restricciones. Si el llamador quiere preservar el valor de un registro de uso a través de una llamada a función, necesita respaldarlo y restaurarlo antes de la llamada a la función (por ejemplo, empujándolo a la pila). Así, los registros de uso son _guardados por el llamador_.
En x86_64, la convención de llamada C especifica los siguientes registros preservados y de uso:
| registros preservados | registros de uso |
| ----------------------------------------------- | ----------------------------------------------------------- |
| `rbp`, `rbx`, `rsp`, `r12`, `r13`, `r14`, `r15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11` |
| _guardados por el llamado_ | _guardados por el llamador_ |
El compilador conoce estas reglas, por lo que genera el código en consecuencia. Por ejemplo, la mayoría de las funciones comienzan con un `push rbp`, que respalda `rbp` en la pila (porque es un registro guardado por el llamado).
### Preservando todos los registros
A diferencia de las llamadas a funciones, las excepciones pueden ocurrir en _cualquier_ instrucción. En la mayoría de los casos, ni siquiera sabemos en tiempo de compilación si el código generado causará una excepción. Por ejemplo, el compilador no puede saber si una instrucción provoca un desbordamiento de pila o un fallo de página.
Dado que no sabemos cuándo ocurrirá una excepción, no podemos respaldar ningún registro antes. Esto significa que no podemos usar una convención de llamada que dependa de registros guardados por el llamador para los manejadores de excepciones. En su lugar, necesitamos una convención de llamada que preserve _todos los registros_. La convención de llamada `x86-interrupt` es una de esas convenciones, por lo que garantiza que todos los valores de los registros se restauren a sus valores originales al retornar de la función.
Tenga en cuenta que esto no significa que todos los registros se guarden en la pila al ingresar la función. En su lugar, el compilador solo respalda los registros que son sobrescritos por la función. De esta manera, se puede generar un código muy eficiente para funciones cortas que solo utilizan unos pocos registros.
### El marco de pila de interrupción
En una llamada a función normal (usando la instrucción `call`), la CPU empuja la dirección de retorno antes de saltar a la función objetivo. Al retornar de la función (usando la instrucción `ret`), la CPU extrae esta dirección de retorno y salta a ella. Por lo tanto, el marco de pila de una llamada a función normal se ve así:

Sin embargo, para los manejadores de excepciones e interrupciones, empujar una dirección de retorno no sería suficiente, ya que los manejadores de interrupción a menudo se ejecutan en un contexto diferente (puntero de pila, flags de CPU, etc.). En cambio, la CPU realiza los siguientes pasos cuando ocurre una interrupción:
0. **Guardando el antiguo puntero de pila**: La CPU lee los valores del puntero de pila (`rsp`) y del registro del segmento de pila (`ss`) y los recuerda en un búfer interno.
1. **Alineando el puntero de pila**: Una interrupción puede ocurrir en cualquier instrucción, por lo que el puntero de pila también puede tener cualquier valor. Sin embargo, algunas instrucciones de CPU (por ejemplo, algunas instrucciones SSE) requieren que el puntero de pila esté alineado en un límite de 16 bytes, por lo que la CPU realiza tal alineación inmediatamente después de la interrupción.
2. **Cambiando de pilas** (en algunos casos): Se produce un cambio de pila cuando cambia el nivel de privilegio de la CPU, por ejemplo, cuando ocurre una excepción de CPU en un programa en modo usuario. También es posible configurar los cambios de pila para interrupciones específicas utilizando la llamada _Tabla de Pila de Interrupción_ (descrita en la próxima publicación).
3. **Empujando el antiguo puntero de pila**: La CPU empuja los valores `rsp` y `ss` del paso 0 a la pila. Esto hace posible restaurar el puntero de pila original al retornar de un manejador de interrupción.
4. **Empujando y actualizando el registro `RFLAGS`**: El registro [`RFLAGS`] contiene varios bits de control y estado. Al entrar en la interrupción, la CPU cambia algunos bits y empuja el antiguo valor.
5. **Empujando el puntero de instrucción**: Antes de saltar a la función manejadora de la interrupción, la CPU empuja el puntero de instrucción (`rip`) y el segmento de código (`cs`). Esto es comparable al empuje de la dirección de retorno de una llamada a función normal.
6. **Empujando un código de error** (para algunas excepciones): Para algunas excepciones específicas, como los fallos de página, la CPU empuja un código de error, que describe la causa de la excepción.
7. **Invocando el manejador de interrupción**: La CPU lee la dirección y el descriptor de segmento de la función manejadora de interrupción del campo correspondiente en la IDT. Luego, invoca este manejador cargando los valores en los registros `rip` y `cs`.
[`RFLAGS`]: https://en.wikipedia.org/wiki/FLAGS_register
Así, el _marco de pila de interrupción_ se ve así:

En el crate `x86_64`, el marco de pila de interrupción está representado por la estructura [`InterruptStackFrame`]. Se pasa a los manejadores de interrupción como `&mut` y se puede utilizar para recuperar información adicional sobre la causa de la excepción. La estructura no contiene un campo de código de error, ya que solo algunas pocas excepciones empujan un código de error. Estas excepciones utilizan el tipo de función separado [`HandlerFuncWithErrCode`], que tiene un argumento adicional `error_code`.
[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptStackFrame.html
### Detrás de las escenas {#demasiada-magia}
La convención de llamada `x86-interrupt` es una potente abstracción que oculta casi todos los detalles desordenados del proceso de manejo de excepciones. Sin embargo, a veces es útil saber lo que sucede tras el telón. Aquí hay un breve resumen de las cosas que la convención de llamada `x86-interrupt` maneja:
- **Recuperando los argumentos**: La mayoría de las convenciones de llamada esperan que los argumentos se pasen en registros. Esto no es posible para los manejadores de excepciones, ya que no debemos sobrescribir los valores de ningún registro antes de respaldarlos en la pila. En cambio, la convención de llamada `x86-interrupt` es consciente de que los argumentos ya están en la pila en un desplazamiento específico.
- **Retornando usando `iretq`**: Dado que el marco de pila de interrupción difiere completamente de los marcos de pila de llamadas a funciones normales, no podemos retornar de las funciones manejadoras a través de la instrucción `ret` normal. Así que en su lugar, se debe usar la instrucción `iretq`.
- **Manejando el código de error**: El código de error, que se empuja para algunas excepciones, hace que las cosas sean mucho más complejas. Cambia la alineación de la pila (vea el siguiente punto) y debe ser extraído de la pila antes de retornar. La convención de llamada `x86-interrupt` maneja toda esa complejidad. Sin embargo, no sabe qué función manejadora se utiliza para qué excepción, por lo que necesita deducir esa información del número de argumentos de función. Esto significa que el programador sigue siendo responsable de utilizar el tipo de función correcto para cada excepción. Afortunadamente, el tipo `InterruptDescriptorTable` definido por el crate `x86_64` asegura que se utilicen los tipos de función correctos.
- **Alineando la pila**: Algunas instrucciones (especialmente las instrucciones SSE) requieren que la pila esté alineada a 16 bytes. La CPU asegura esta alineación cada vez que ocurre una excepción, pero para algunas excepciones, puede destruirla de nuevo más tarde cuando empuja un código de error. La convención de llamada `x86-interrupt` se encarga de esto al realinear la pila en este caso.
Si está interesado en más detalles, también tenemos una serie de publicaciones que explican el manejo de excepciones utilizando [funciones desnudas] vinculadas [al final de esta publicación][too-much-magic].
[funciones desnudas]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
[too-much-magic]: #demasiada-magia
## Implementación
Ahora que hemos entendido la teoría, es hora de manejar las excepciones de CPU en nuestro núcleo. Comenzaremos creando un nuevo módulo de interrupciones en `src/interrupts.rs`, que primero crea una función `init_idt` que crea una nueva `InterruptDescriptorTable`:
``` rust
// en src/lib.rs
pub mod interrupts;
// en src/interrupts.rs
use x86_64::structures::idt::InterruptDescriptorTable;
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
}
```
Ahora podemos agregar funciones manejadoras. Comenzamos agregando un manejador para la [excepción de punto de interrupción]. La excepción de punto de interrupción es la excepción perfecta para probar el manejo de excepciones. Su único propósito es pausar temporalmente un programa cuando se ejecuta la instrucción de punto de interrupción `int3`.
[excepción de punto de interrupción]: https://wiki.osdev.org/Exceptions#Breakpoint
La excepción de punto de interrupción se utiliza comúnmente en depuradores: cuando el usuario establece un punto de interrupción, el depurador sobrescribe la instrucción correspondiente con la instrucción `int3` para que la CPU lance la excepción de punto de interrupción al llegar a esa línea. Cuando el usuario quiere continuar el programa, el depurador reemplaza la instrucción `int3` con la instrucción original nuevamente y continúa el programa. Para más detalles, vea la serie ["_Cómo funcionan los depuradores_"].
["_Cómo funcionan los depuradores_"]: https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
Para nuestro caso de uso, no necesitamos sobrescribir instrucciones. En su lugar, solo queremos imprimir un mensaje cuando la instrucción de punto de interrupción se ejecute y luego continuar el programa. Así que creemos una simple función `breakpoint_handler` y la agreguemos a nuestra IDT:
```rust
// en src/interrupts.rs
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
use crate::println;
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
}
extern "x86-interrupt" fn breakpoint_handler(
stack_frame: InterruptStackFrame)
{
println!("EXCEPCIÓN: PUNTO DE INTERRUPCIÓN\n{:#?}", stack_frame);
}
```
Nuestro manejador simplemente muestra un mensaje y imprime en formato bonito el marco de pila de interrupción.
Cuando intentamos compilarlo, ocurre el siguiente error:
```
error[E0658]: la ABI de x86-interrupt es experimental y está sujeta a cambios (ver issue #40180)
--> src/main.rs:53:1
|
53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {
54 | | println!("EXCEPCIÓN: PUNTO DE INTERRUPCIÓN\n{:#?}", stack_frame);
55 | | }
| |_^
|
= ayuda: añade #![feature(abi_x86_interrupt)] a los atributos del crate para habilitarlo
```
Este error ocurre porque la convención de llamada `x86-interrupt` sigue siendo inestable. Para utilizarla de todos modos, tenemos que habilitarla explícitamente añadiendo `#![feature(abi_x86_interrupt)]` en la parte superior de nuestro `lib.rs`.
### Cargando la IDT
Para que la CPU utilice nuestra nueva tabla de descriptores de interrupción, necesitamos cargarla usando la instrucción [`lidt`]. La estructura `InterruptDescriptorTable` del crate `x86_64` proporciona un método [`load`][InterruptDescriptorTable::load] para eso. Intentemos usarlo:
[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt
[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load
```rust
// en src/interrupts.rs
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt.load();
}
```
Cuando intentamos compilarlo ahora, ocurre el siguiente error:
```
error: `idt` no vive lo suficiente
--> src/interrupts/mod.rs:43:5
|
43 | idt.load();
| ^^^ no vive lo suficiente
44 | }
| - el valor prestado solo es válido hasta aquí
|
= nota: el valor prestado debe ser válido durante la vida estática...
```
Así que el método `load` espera un `&'static self`, es decir, una referencia válida para la duración completa del programa. La razón es que la CPU accederá a esta tabla en cada interrupción hasta que se cargue una IDT diferente. Por lo tanto, usar una vida más corta que `'static` podría llevar a errores de uso después de liberar.
De hecho, esto es exactamente lo que sucede aquí. Nuestra `idt` se crea en la pila, por lo que solo es válida dentro de la función `init`. Después, la memoria de la pila se reutiliza para otras funciones, por lo que la CPU podría interpretar una memoria aleatoria de la pila como IDT. Afortunadamente, el método `load` de `InterruptDescriptorTable` codifica este requisito de vida en su definición de función, para que el compilador de Rust pueda prevenir este posible error en tiempo de compilación.
Para solucionar este problema, necesitamos almacenar nuestra `idt` en un lugar donde tenga una vida `'static`. Para lograr esto, podríamos asignar nuestra IDT en el montón usando [`Box`] y luego convertirla en una referencia `'static`, pero estamos escribiendo un núcleo de sistema operativo y, por lo tanto, no tenemos un montón (todavía).
[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html
Como alternativa, podríamos intentar almacenar la IDT como una `static`:
```rust
static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
pub fn init_idt() {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
```
Sin embargo, hay un problema: las estáticas son inmutables, por lo que no podemos modificar la entrada de punto de interrupción desde nuestra función `init`. Podríamos resolver este problema utilizando un [`static mut`]:
[`static mut`]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
```rust
static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
pub fn init_idt() {
unsafe {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
}
```
Esta variante se compila sin errores, pero está lejos de ser idiomática. Las variables `static mut` son muy propensas a condiciones de carrera, por lo que necesitamos un bloque [`unsafe`] en cada acceso.
[`unsafe`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers
#### Las estáticas perezosas al rescate
Afortunadamente, existe el macro `lazy_static`. En lugar de evaluar una `static` en tiempo de compilación, el macro realiza la inicialización de cuando la `static` es referenciada por primera vez. Por lo tanto, podemos hacer casi todo en el bloque de inicialización e incluso ser capaces de leer valores en tiempo de ejecución.
Ya importamos el crate `lazy_static` cuando [creamos una abstracción para el búfer de texto VGA][vga text buffer lazy static]. Así que podemos utilizar directamente el macro `lazy_static!` para crear nuestra IDT estática:
[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
```rust
// en src/interrupts.rs
use lazy_static::lazy_static;
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt
};
}
pub fn init_idt() {
IDT.load();
}
```
Tenga en cuenta cómo esta solución no requiere bloques `unsafe`. El macro `lazy_static!` utiliza `unsafe` detrás de escena, pero está abstraído en una interfaz segura.
### Ejecutándolo
El último paso para hacer que las excepciones funcionen en nuestro núcleo es llamar a la función `init_idt` desde nuestro `main.rs`. En lugar de llamarla directamente, introducimos una función de inicialización general en nuestro `lib.rs`:
```rust
// en src/lib.rs
pub fn init() {
interrupts::init_idt();
}
```
Con esta función, ahora tenemos un lugar central para las rutinas de inicialización que se pueden compartir entre las diferentes funciones `_start` en nuestro `main.rs`, `lib.rs` y pruebas de integración.
Ahora podemos actualizar la función `_start` de nuestro `main.rs` para llamar a `init` y luego activar una excepción de punto de interrupción:
```rust
// en src/main.rs
#[no_mangle]
pub extern "C" fn _start() -> ! {
println!("¡Hola Mundo{}", "!");
blog_os::init(); // nueva
// invocar una excepción de punto de interrupción
x86_64::instructions::interrupts::int3(); // nueva
// como antes
#[cfg(test)]
test_main();
println!("¡No se bloqueó!");
loop {}
}
```
Cuando lo ejecutamos en QEMU ahora (usando `cargo run`), vemos lo siguiente:

¡Funciona! La CPU invoca exitosamente nuestro manejador de punto de interrupción, que imprime el mensaje, y luego devuelve a la función `_start`, donde se imprime el mensaje `¡No se bloqueó!`.
Vemos que el marco de pila de interrupción nos indica los punteros de instrucción y de pila en el momento en que ocurrió la excepción. Esta información es muy útil al depurar excepciones inesperadas.
### Agregando una prueba
Creemos una prueba que asegure que lo anterior sigue funcionando. Primero, actualizamos la función `_start` para que también llame a `init`:
```rust
// en src/lib.rs
/// Punto de entrada para `cargo test`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {
init(); // nueva
test_main();
loop {}
}
```
Recuerde, esta función `_start` se utiliza al ejecutar `cargo test --lib`, ya que Rust prueba el `lib.rs` completamente de forma independiente de `main.rs`. Necesitamos llamar a `init` aquí para configurar una IDT antes de ejecutar las pruebas.
Ahora podemos crear una prueba `test_breakpoint_exception`:
```rust
// en src/interrupts.rs
#[test_case]
fn test_breakpoint_exception() {
// invocar una excepción de punto de interrupción
x86_64::instructions::interrupts::int3();
}
```
La prueba invoca la función `int3` para activar una excepción de punto de interrupción. Al verificar que la ejecución continúa después, verificamos que nuestro manejador de punto de interrupción está funcionando correctamente.
Puedes probar esta nueva prueba ejecutando `cargo test` (todas las pruebas) o `cargo test --lib` (solo las pruebas de `lib.rs` y sus módulos). Deberías ver lo siguiente en la salida:
```
blog_os::interrupts::test_breakpoint_exception... [ok]
```
## ¿Demasiada magia?
La convención de llamada `x86-interrupt` y el tipo [`InterruptDescriptorTable`] hicieron que el proceso de manejo de excepciones fuera relativamente sencillo y sin dolor. Si esto fue demasiada magia para ti y te gusta aprender todos los detalles sucios del manejo de excepciones, tenemos cubiertos: Nuestra serie ["Manejo de Excepciones con Funciones Desnudas"] muestra cómo manejar excepciones sin la convención de llamada `x86-interrupt` y también crea su propio tipo de IDT. Históricamente, estas publicaciones eran las principales publicaciones sobre manejo de excepciones antes de que existieran la convención de llamada `x86-interrupt` y el crate `x86_64`. Tenga en cuenta que estas publicaciones se basan en la [primera edición] de este blog y pueden estar desactualizadas.
["Manejo de Excepciones con Funciones Desnudas"]: @/edition-1/extra/naked-exceptions/_index.md
[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
[primera edición]: @/edition-1/_index.md
## ¿Qué sigue?
¡Hemos capturado con éxito nuestra primera excepción y regresamos de ella! El siguiente paso es asegurarnos de que capturamos todas las excepciones porque una excepción no capturada causa un [triple fallo] fatal, lo que lleva a un reinicio del sistema. La próxima publicación explica cómo podemos evitar esto al capturar correctamente [dobles fallos].
[triple fallo]: https://wiki.osdev.org/Triple_Fault
[dobles fallos]: https://wiki.osdev.org/Double_Fault#Double_Fault
================================================
FILE: blog/content/edition-2/posts/05-cpu-exceptions/index.fa.md
================================================
+++
title = "استثناهای پردازنده"
weight = 5
path = "fa/cpu-exceptions"
date = 2018-06-17
[extra]
# Please update this when updating the translation
translation_based_on_commit = "a081faf3cced9aeb0521052ba91b74a1c408dcff"
# GitHub usernames of the people that translated this post
translators = ["hamidrezakp", "MHBahrampour"]
rtl = true
+++
استثناهای پردازنده در موقعیت های مختلف دارای خطا رخ می دهد ، به عنوان مثال هنگام دسترسی به آدرس حافظه نامعتبر یا تقسیم بر صفر. برای واکنش به آنها ، باید یک _جدول توصیف کننده وقفه_ تنظیم کنیم که توابع کنترل کننده را فراهم کند. در انتهای این پست ، هسته ما قادر به گرفتن [استثناهای breakpoint] و ادامه اجرای طبیعی پس از آن خواهد بود.
[استثناهای breakpoint]: https://wiki.osdev.org/Exceptions#Breakpoint
این بلاگ بصورت آزاد روی [گیتهاب] توسعه داده شده است. اگر مشکل یا سوالی دارید، لطفاً آنجا یک ایشو باز کنید. همچنین میتوانید [در زیر] این پست کامنت بگذارید. سورس کد کامل این پست را میتوانید در بِرَنچ [`post-05`][post branch] پیدا کنید.
[گیتهاب]: https://github.com/phil-opp/blog_os
[در زیر]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05
## بررسی اجمالی
یک استثنا نشان می دهد که مشکلی در دستورالعمل فعلی وجود دارد. به عنوان مثال ، اگر دستورالعمل فعلی بخواهد تقسیم بر 0 کند ، پردازنده یک استثنا صادر می کند. وقتی یک استثنا اتفاق می افتد ، پردازنده کار فعلی خود را رها کرده و بسته به نوع استثنا ، بلافاصله یک تابع خاص کنترل کننده استثنا را فراخوانی می کند.
در x86 حدود 20 نوع مختلف استثنا پردازنده وجود دارد. مهمترین آنها در زیر آمده اند:
- **خطای صفحه**: خطای صفحه در دسترسی غیرقانونی به حافظه رخ می دهد. به عنوان مثال ، اگر دستورالعمل فعلی بخواهد از یک صفحه نگاشت نشده بخواند یا بخواهد در یک صفحه فقط خواندنی بنویسد.
- **کد نامعتبر**: این استثنا وقتی رخ می دهد که دستورالعمل فعلی نامعتبر است ، به عنوان مثال وقتی می خواهیم از [دستورالعمل های SSE] جدیدتر بر روی یک پردازنده قدیمی استفاده کنیم که آنها را پشتیبانی نمی کند.
- **خطای محافظت عمومی**: این استثنا دارای بیشترین دامنه علل است. این مورد در انواع مختلف نقض دسترسی مانند تلاش برای اجرای یک دستورالعمل ممتاز در کد سطح کاربر یا نوشتن فیلدهای رزرو شده در ثبات های پیکربندی رخ می دهد.
- **خطای دوگانه**: هنگامی که یک استثنا رخ می دهد ، پردازنده سعی می کند تابع کنترل کننده مربوطه را اجرا کند. اگر یک استثنا دیگر رخ دهد _هنگام فراخوانی تابع کنترل کننده استثنا_ ، پردازنده یک استثنای خطای دوگانه ایجاد می کند. این استثنا همچنین زمانی اتفاق می افتد که هیچ تابع کنترل کننده ای برای یک استثنا ثبت نشده باشد.
- **خطای سهگانه**: اگر در حالی که پردازنده سعی می کند تابع کنترل کننده خطای دوگانه را فراخوانی کند استثنایی رخ دهد ، این یک خطای سهگانه است. ما نمی توانیم یک خطای سه گانه را بگیریم یا آن را کنترل کنیم. بیشتر پردازنده ها ریست کردن خود و راه اندازی مجدد سیستم عامل واکنش نشان می دهند.
[دستورالعمل های SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
برای مشاهده لیست کامل استثناها ، [ویکی OSDev][exceptions] را بررسی کنید.
[exceptions]: https://wiki.osdev.org/Exceptions
### جدول توصیف کننده وقفه
برای گرفتن و رسیدگی به استثناها ، باید اصطلاحاً _جدول توصیفگر وقفه_ (IDT) را تنظیم کنیم. در این جدول می توانیم برای هر استثنا پردازنده یک عملکرد تابع کننده مشخص کنیم. سخت افزار به طور مستقیم از این جدول استفاده می کند ، بنابراین باید از یک قالب از پیش تعریف شده پیروی کنیم. هر ورودی جدول باید ساختار 16 بایتی زیر را داشته باشد:
| Type | Name | Description |
| ---- | ------------------------ | ------------------------------------------------------------ |
| u16 | Function Pointer [0:15] | The lower bits of the pointer to the handler function. |
| u16 | GDT selector | Selector of a code segment in the [global descriptor table]. |
| u16 | Options | (see below) |
| u16 | Function Pointer [16:31] | The middle bits of the pointer to the handler function. |
| u32 | Function Pointer [32:63] | The remaining bits of the pointer to the handler function. |
| u32 | Reserved |
[global descriptor table]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
قسمت گزینه ها (Options) دارای قالب زیر است:
| Bits | Name | Description |
| ----- | -------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| 0-2 | Interrupt Stack Table Index | 0: Don't switch stacks, 1-7: Switch to the n-th stack in the Interrupt Stack Table when this handler is called. |
| 3-7 | Reserved |
| 8 | 0: Interrupt Gate, 1: Trap Gate | If this bit is 0, interrupts are disabled when this handler is called. |
| 9-11 | must be one |
| 12 | must be zero |
| 13‑14 | Descriptor Privilege Level (DPL) | The minimal privilege level required for calling this handler. |
| 15 | Present |
هر استثنا دارای یک اندیس از پیش تعریف شده در IDT است. به عنوان مثال استثنا کد نامعتبر دارای اندیس 6 و استثنا خطای صفحه دارای اندیس 14 است. بنابراین ، سخت افزار می تواند به طور خودکار عنصر مربوطه را برای هر استثنا بارگذاری کند. [جدول استثناها][exceptions] در ویکی OSDev ، اندیس های IDT کلیه استثناها را در ستون “Vector nr.” نشان داده است.
هنگامی که یک استثنا رخ می دهد ، پردازنده تقریباً موارد زیر را انجام می دهد:
1. برخی از ثباتها را به پشته وارد میکند، از جمله اشاره گر دستورالعمل و ثبات [RFLAGS]. (بعداً در این پست از این مقادیر استفاده خواهیم کرد.)
2. عنصر مربوط به آن (استثنا) را از جدول توصیف کننده وقفه (IDT) میخواند. به عنوان مثال ، پردازنده هنگام رخ دادن خطای صفحه ، عنصر چهاردهم را می خواند.
3. وجود عنصر را بررسی میکند. اگر اینگونه نباشد یک خطای دوگانه ایجاد میکند.
4. اگر عنصر یک گیت وقفه است (بیت 40 تنظیم نشده است) وقفه های سخت افزاری را غیرفعال میکند.
5. انتخابگر مشخص شده [GDT] را در سگمنت CS بارگذاری میکند.
6. به تابع کنترل کننده مشخص شده میرود.
[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register
[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
در حال حاضر نگران مراحل 4 و 5 نباشید ، ما در مورد جدول توصیف کننده گلوبال و وقفه های سخت افزاری در پست های بعدی خواهیم آموخت.
## یک نوع IDT
به جای ایجاد نوع IDT خود ، از [ساختمان `InterruptDescriptorTable`] کرت `x86_64` استفاده خواهیم کرد که به این شکل است:
[ساختمان `InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
``` rust
#[repr(C)]
pub struct InterruptDescriptorTable {
pub divide_by_zero: Entry,
pub debug: Entry,
pub non_maskable_interrupt: Entry,
pub breakpoint: Entry,
pub overflow: Entry,
pub bound_range_exceeded: Entry,
pub invalid_opcode: Entry,
pub device_not_available: Entry,
pub double_fault: Entry,
pub invalid_tss: Entry,
pub segment_not_present: Entry,
pub stack_segment_fault: Entry,
pub general_protection_fault: Entry,
pub page_fault: Entry,
pub x87_floating_point: Entry,
pub alignment_check: Entry,
pub machine_check: Entry,
pub simd_floating_point: Entry,
pub virtualization: Entry,
pub security_exception: Entry,
// some fields omitted
}
```
فیلدها از نوع [` src/main.rs:53:1
|
53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {
54 | | println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
55 | | }
| |_^
|
= help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable
```
این خطا به این دلیل رخ می دهد که قرارداد فراخوانی `x86-interrupt` هنوز ناپایدار است. به هر حال برای استفاده از آن ، باید صریحاً آن را با اضافه کردن `#![feature(abi_x86_interrupt)]` در بالای `lib.rs` فعال کنیم.
### بارگیری IDT
برای اینکه پردازنده از جدول توصیف کننده وقفه جدید ما استفاده کند ، باید آن را با استفاده از دستورالعمل [`lidt`] بارگیری کنیم. ساختمان `InterruptDescriptorTable` از کرت ` x86_64` متد [`load`][InterruptDescriptorTable::load] را برای این کار فراهم می کند. بیایید سعی کنیم از آن استفاده کنیم:
[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt
[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load
```rust
// in src/interrupts.rs
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt.load();
}
```
اکنون هنگامی که می خواهیم آن را کامپایل کنیم ، خطای زیر رخ می دهد:
```
error: `idt` does not live long enough
--> src/interrupts/mod.rs:43:5
|
43 | idt.load();
| ^^^ does not live long enough
44 | }
| - borrowed value only lives until here
|
= note: borrowed value must be valid for the static lifetime...
```
پس متد `load` انتظار دریافت یک `static self'&` را دارد، این مرجعی است که برای تمام مدت زمان اجرای برنامه معتبر است. دلیل این امر این است که پردازنده در هر وقفه به این جدول دسترسی پیدا می کند تا زمانی که IDT دیگری بارگیری کنیم. بنابراین استفاده از طول عمر کوتاه تر از `static'` می تواند منجر به باگ های استفاده-بعد-از-آزادسازی شود.
در واقع ، این دقیقاً همان چیزی است که در اینجا اتفاق می افتد. `idt` ما روی پشته ایجاد می شود ، بنابراین فقط در داخل تابع `init` معتبر است. پس از آن حافظه پشته برای توابع دیگر مورد استفاده مجدد قرار می گیرد ، بنابراین پردازنده حافظه پشته تصادفی را به عنوان IDT تفسیر می کند. خوشبختانه ، متد `InterruptDescriptorTable::load` این نیاز به طول عمر را در تعریف تابع خود اجباری می کند، بنابراین کامپایلر راست قادر است از این مشکل احتمالی در زمان کامپایل جلوگیری کند.
برای رفع این مشکل، باید `idt` را در مکانی ذخیره کنیم که طول عمر `static'` داشته باشد. برای رسیدن به این هدف می توانیم IDT را با استفاده از [`Box`] بر روی حافظه Heap ایجاد کنیم و سپس آن را به یک مرجع `static'` تبدیل کنیم، اما ما در حال نوشتن هسته سیستم عامل هستیم و بنابراین هنوز Heap نداریم.
[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html
به عنوان یک گزینه دیگر، می توانیم IDT را به صورت `static` ذخیره کنیم:
```rust
static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
pub fn init_idt() {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
```
با این وجود، یک مشکل وجود دارد: استاتیکها تغییرناپذیر هستند، پس نمی توانیم ورودی بریکپوینت را از تابع `init` تغییر دهیم. می توانیم این مشکل را با استفاده از [`static mut`] حل کنیم:
[`static mut`]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
```rust
static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
pub fn init_idt() {
unsafe {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
}
```
در این روش بدون خطا کامپایل می شود اما مشکلات دیگری به همراه دارد. `static mut` بسیار مستعد Data Race هستند، بنابراین در هر دسترسی به یک [بلوک `unsafe`] نیاز داریم.
[بلوک `unsafe`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers
#### Lazy Statics به نجات ما میآیند
خوشبختانه ماکرو `lazy_static` وجود دارد. ماکرو به جای ارزیابی یک `static` در زمان کامپایل ، مقداردهی اولیه آن را هنگام اولین ارجاع به آن انجام می دهد. بنابراین، می توانیم تقریباً همه کاری را در بلوک مقداردهی اولیه انجام دهیم و حتی قادر به خواندن مقادیر زمان اجرا هستیم.
ما قبلاً کرت `lazy_static` را وارد کردیم وقتی [یک انتزاع برای بافر متن VGA ایجاد کردیم][vga text buffer lazy static]. بنابراین می توانیم مستقیماً از ماکرو `!lazy_static` برای ایجاد IDT استاتیک استفاده کنیم:
[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
```rust
// in src/interrupts.rs
use lazy_static::lazy_static;
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt
};
}
pub fn init_idt() {
IDT.load();
}
```
توجه داشته باشید که چگونه این راه حل به هیچ بلوک `unsafe` نیاز ندارد. ماکرو `!lazy_static` از `unsafe` در پشت صحنه استفاده می کند ، اما در یک رابط امن به ما داده می شود.
### اجرای آن
آخرین مرحله برای کارکرد استثناها در هسته ما فراخوانی تابع `init_idt` از `main.rs` است. به جای فراخوانی مستقیم آن، یک تابع عمومی `init` را در `lib.rs` معرفی می کنیم:
```rust
// in src/lib.rs
pub fn init() {
interrupts::init_idt();
}
```
با استفاده از این تابع اکنون یک مکان اصلی برای روالهای اولیه داریم که می تواند بین توابع مختلف `start_` در `main.rs` ، `lib.rs` و تستهای یکپارچه به اشتراک گذاشته شود.
اکنون می توانیم تابع `start_` در `main.rs` را به روز کنیم تا `init` را فراخوانی کرده و سپس یک استثنا بریکپوینت ایجاد کند:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
blog_os::init(); // new
// invoke a breakpoint exception
x86_64::instructions::interrupts::int3(); // new
// as before
#[cfg(test)]
test_main();
println!("It did not crash!");
loop {}
}
```
اکنون هنگامی که آن را در QEMU اجرا می کنیم (با استفاده از `cargo run`) ، موارد زیر را مشاهده می کنیم:

کار می کند! پردازنده با موفقیت تابع کنترل کننده بریکپوینت ما را فراخوانی می کند ، که پیام را چاپ می کند و سپس به تابع `start_` برمی گردد ، جایی که پیام `!It did not crash` چاپ شده است.
می بینیم که قاب پشته وقفه، دستورالعمل و نشانگرهای پشته را در زمان وقوع استثنا به ما می گوید. این اطلاعات هنگام رفع اشکال استثناهای غیر منتظره بسیار مفید است.
### افزودن یک تست
بیایید یک تست ایجاد کنیم که از ادامه کار کد بالا اطمینان حاصل کند. ابتدا تابع `start_` را به روز می کنیم تا `init` را نیز فراخوانی کند:
```rust
// in src/lib.rs
/// Entry point for `cargo test`
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
init(); // new
test_main();
loop {}
}
```
بخاطر داشته باشید، این تابع `start_` هنگام اجرای`cargo test --lib` استفاده می شود، زیرا راست `lib.rs` را کاملاً مستقل از`main.rs` تست میکند. قبل از اجرای تستها باید برای راه اندازی IDT در اینجا `init` فراخوانی شود.
اکنون می توانیم یک تست `test_breakpoint_exception` ایجاد کنیم:
```rust
// in src/interrupts.rs
#[test_case]
fn test_breakpoint_exception() {
// invoke a breakpoint exception
x86_64::instructions::interrupts::int3();
}
```
این تست تابع `int3` را فراخوانی می کند تا یک استثنا بریکپوینت ایجاد کند. با بررسی اینکه اجرا پس از آن ادامه دارد ، تأیید می کنیم که کنترل کننده بریکپوینت ما به درستی کار می کند.
شما می توانید این تست جدید را با اجرای `cargo test` (همه تستها) یا` cargo test --lib` (فقط تست های `lib.rs` و ماژول های آن) امتحان کنید. باید موارد زیر را در خروجی مشاهده کنید:
```
blog_os::interrupts::test_breakpoint_exception... [ok]
```
## خیلی جادویی بود؟
قرارداد فراخوانی `x86-interrupt` و نوع [`InterruptDescriptorTable`] روند مدیریت استثناها را نسبتاً سر راست و بدون درد ساختهاند. اگر این برای شما بسیار جادویی بود و دوست دارید تمام جزئیات مهم مدیریت استثنا را بیاموزید، برای شما هم مطالبی داریم: مجموعه ["مدیریت استثناها با توابع برهنه"] ما، نحوه مدیریت استثناها بدون قرارداد فراخوانی`x86-interrupt` را نشان می دهد و همچنین نوع IDT خاص خود را ایجاد می کند. از نظر تاریخی، این پستها مهمترین پستهای مدیریت استثناها قبل از وجود قرارداد فراخوانی `x86-interrupt` و کرت `x86_64` بودند. توجه داشته باشید که این پستها بر اساس [نسخه اول] این وبلاگ هستند و ممکن است قدیمی باشند.
["مدیریت استثناها با توابع برهنه"]: @/edition-1/extra/naked-exceptions/_index.md
[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
[نسخه اول]: @/edition-1/_index.md
## مرحله بعدی چیست؟
ما اولین استثنای خود را با موفقیت گرفتیم و از آن بازگشتیم! گام بعدی اطمینان از این است که همه استثناها را می گیریم ، زیرا یک استثنا گرفته نشده باعث [خطای سهگانه] می شود که منجر به شروع مجدد سیستم می شود. پست بعدی توضیح می دهد که چگونه می توان با گرفتن صحیح [خطای دوگانه] از این امر جلوگیری کرد.
[خطای سهگانه]: https://wiki.osdev.org/Triple_Fault
[خطای دوگانه]: https://wiki.osdev.org/Double_Fault#Double_Fault
================================================
FILE: blog/content/edition-2/posts/05-cpu-exceptions/index.ja.md
================================================
+++
title = "CPU例外"
weight = 5
path = "ja/cpu-exceptions"
date = 2018-06-17
[extra]
# Please update this when updating the translation
translation_based_on_commit = "a8a6b725cff2e485bed76ff52ac1f18cec08cc7b"
# GitHub usernames of the people that translated this post
translators = ["swnakamura"]
+++
CPU例外は、例えば無効なメモリアドレスにアクセスしたときやゼロ除算したときなど、様々なミスによって発生します。それらに対処するために、ハンドラ関数を提供する **割り込み記述子表** を設定しなくてはなりません。この記事を読み終わる頃には、私達のカーネルは[ブレークポイント例外][breakpoint exceptions]を捕捉し、その後通常の実行を継続できるようになっているでしょう。
[breakpoint exceptions]: https://wiki.osdev.org/Exceptions#Breakpoint
このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-05` ブランチ][post branch]にあります。
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05
## 概要
例外とは、今実行している命令はなにかおかしいぞ、ということを示すものです。例えば、現在の命令がゼロ除算を実行しようとしているとき、CPUは例外を発します。例外が起こると、CPUは現在行われている作業に割り込み、例外の種類に従って、即座に特定の例外ハンドラ関数を呼びます。
x86には20種類のCPU例外があります。中でも重要なものは:
- **ページフォルト**: ページフォルトは不正なメモリアクセスの際に発生します。例えば、現在の命令がマップされていないページから読み込もうとしたり、読み込み専用のページに書き込もうとしたときに生じます。
- **無効な命令コード**: この例外は現在の命令が無効であるときに発生します。例えば、[SSE命令][SSE instructions]という新しい命令をサポートしていない旧式のCPU上でこれを実行しようとしたときに生じます。
- **一般保護違反**: これは、例外の中でも、最もいろいろな理由で発生しうるものです。ユーザーレベルのコードで特権命令を実行しようとしたときや、設定レジスタの保護領域に書き込もうとしたときなど、様々な種類のアクセス違反によって生じます。
- **ダブルフォルト**: 何らかの例外が起こったとき、CPUは対応するハンドラ関数を呼び出そうとします。 この例外ハンドラを **呼び出している間に** 別の例外が起こった場合、CPUはダブルフォルト例外を出します。この例外はまた、ある例外に対してハンドラ関数が登録されていないときにも起こります。
- **トリプルフォルト**: CPUがダブルフォルトのハンドラ関数を呼び出そうとしている間に例外が発生すると、CPUは **トリプルフォルト** という致命的な例外を発します。トリプルフォルトを捕捉したり処理したりすることはできません。これが起こると、多くのプロセッサは自らをリセットしてOSを再起動することで対応します。
[SSE instructions]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
例外の完全な一覧を見たい場合は、[OSDev wiki][exceptions]を見てください。
[exceptions]: https://wiki.osdev.org/Exceptions
### 割り込み記述子表
例外を捕捉し処理するためには、いわゆる割り込み記述子表 (Interrupt Descriptor Table, IDT) を設定しないといけません。この表にそれぞれのCPU例外に対するハンドラ関数を指定することができます。ハードウェアはこの表を直接使うので、決められたフォーマットに従わないといけません。それぞれのエントリは以下の16バイトの構造を持たなければなりません:
| 型 | 名前 | 説明 |
| --- | ------------------------ | ------------------------------------------------------------------------------------------------------ |
| u16 | 関数ポインタ [0:15] | ハンドラ関数へのポインタの下位ビット。 |
| u16 | GDTセレクタ | [大域記述子表 (Global Descriptor Table)][global descriptor table] におけるコードセグメントのセレクタ。 |
| u16 | オプション | (下を参照) |
| u16 | 関数ポインタ [16:31] | ハンドラ関数へのポインタの中位ビット。 |
| u32 | 関数ポインタ [32:63] | ハンドラ関数へのポインタの上位ビット。 |
| u32 | 予約済 |
[global descriptor table]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
オプション部は以下のフォーマットになっています:
| ビット | 名前 | 説明 |
| ------ | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| 0-2 | 割り込みスタックテーブルインデックス | 0ならスタックを変えない。1から7なら、ハンドラが呼ばれたとき、割り込みスタック表のその数字のスタックに変える。 |
| 3-7 | 予約済 |
| 8 | 0: 割り込みゲート、1: トラップゲート | 0なら、このハンドラが呼ばれたとき割り込みは無効化される。 |
| 9-11 | 1にしておかないといけない |
| 12 | 0にしておかないといけない |
| 13‑14 | 記述子の特権レベル (DPL) | このハンドラを呼ぶ際に必要になる最低限の特権レベル。 |
| 15 | Present |
それぞれの例外がIDTの何番目に対応するかは事前に定義されています。例えば、「無効な命令コード」の例外は6番目で、「ページフォルト」例外は14番目です。これにより、ハードウェアがそれぞれの例外に対応するIDTの設定を(特に設定の必要なく)自動的に読み出せるというわけです。OSDev wikiの[「例外表」][exceptions]の "Vector nr." 列に、すべての例外についてIDTの何番目かが記されています。
例外が起こると、ざっくりCPUは以下のことを行います:
1. 命令ポインタと[RFLAGS]レジスタ(これらの値は後で使います)を含むレジスタをスタックにプッシュする。
2. 割り込み記述子表から対応するエントリを読む。例えば、ページフォルトが起こったときはCPUは14番目のエントリを読む。
3. エントリが存在しているのかチェックする。そうでなければダブルフォルトを起こす。
4. エントリが割り込みゲートなら(40番目のビットが0なら)ハードウェア割り込みを無効にする。
5. 指定された[GDT]セレクタをCSセグメントに読み込む。
6. 指定されたハンドラ関数にジャンプする。
[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register
[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
ステップ4と5について今深く考える必要はありません。今後の記事で大域記述子表 (Global Descriptor Table, 略してGDT) とハードウェア割り込みについては学んでいきます。
## IDT型
自前でIDTの型を作る代わりに、`x86_64`クレートの[`InterruptDescriptorTable`構造体][`InterruptDescriptorTable` struct]を使います。こんな見た目をしています:
[`InterruptDescriptorTable` struct]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
``` rust
#[repr(C)]
pub struct InterruptDescriptorTable {
pub divide_by_zero: Entry,
pub debug: Entry,
pub non_maskable_interrupt: Entry,
pub breakpoint: Entry,
pub overflow: Entry,
pub bound_range_exceeded: Entry,
pub invalid_opcode: Entry,
pub device_not_available: Entry,
pub double_fault: Entry,
pub invalid_tss: Entry,
pub segment_not_present: Entry,
pub stack_segment_fault: Entry,
pub general_protection_fault: Entry,
pub page_fault: Entry,
pub x87_floating_point: Entry,
pub alignment_check: Entry,
pub machine_check: Entry,
pub simd_floating_point: Entry,
pub virtualization: Entry,
pub security_exception: Entry,
// いくつかのフィールドは省略している
}
```
この構造体のフィールドは[`idt::Entry`]という型を持っています。これはIDTのエントリのフィールド(上の表を見てください)を表す構造体です。型パラメータ`F`は、期待されるハンドラ関数の型を表します。エントリの中には、[`HandlerFunc`]型を要求するものや、[`HandlerFuncWithErrCode`]型を要求するものがあることがわかります。ページフォルトに至っては、[`PageFaultHandlerFunc`]という自分専用の型を要求していますね。
[`idt::Entry`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.Entry.html
[`HandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFunc.html
[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFuncWithErrCode.html
[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.PageFaultHandlerFunc.html
まず`HandlerFunc`型を見てみましょう:
```rust
type HandlerFunc = extern "x86-interrupt" fn(_: InterruptStackFrame);
```
これは、`extern "x86-interrupt" fn`型への[型エイリアス][type alias]です。`extern`は[外部呼び出し規約][foreign calling convention]に従う関数を定義するのに使われ、おもにC言語のコードと連携したいときに使われます (`extern "C" fn`) 。しかし、`x86-interrupt`呼び出し規約とは何なのでしょう?
[type alias]: https://doc.rust-lang.org/book/ch20-03-advanced-types.html#creating-type-synonyms-with-type-aliases
[foreign calling convention]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions
## 例外の呼び出し規約
例外は関数呼び出しと非常に似ています。CPUが呼び出された関数の最初の命令にジャンプし、それを実行します。その後、CPUはリターンアドレスにジャンプし、親関数の実行を続けます。
しかし、例外と関数呼び出しには大きな違いが一つあるのです:関数呼び出しはコンパイラによって挿入された`call`命令によって自発的に引き起こされますが、例外は **どんな命令の実行中でも** 起こる可能性があるのです。この違いの結果を理解するためには、関数呼び出しについてより詳しく見ていく必要があります。
[呼び出し規約][Calling conventions]は関数呼び出しについて事細かく指定しています。例えば、関数のパラメータがどこに置かれるべきか(例えば、レジスタなのかスタックなのか)や、結果がどのように返されるべきかを指定しています。x86_64上のLinuxでは、C言語の関数に関しては以下のルールが適用されます(これは[System V ABI]で指定されています):
[Calling conventions]: https://en.wikipedia.org/wiki/Calling_convention
[System V ABI]: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
- 最初の6つの整数引数は、レジスタ`rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9`で渡される
- 追加の引数はスタックで渡される
- 結果は`rax`と`rdx`で返される
注意してほしいのは、RustはC言語のABIに従っていない(実は、[RustにはABIすらまだありません][rust abi])ので、このルールは`extern "C" fn`と宣言された関数にしか適用されないということです。
[rust abi]: https://github.com/rust-lang/rfcs/issues/600
### PreservedレジスタとScratchレジスタ
呼び出し規約はレジスタを2種類に分けています:preservedレジスタとscratchレジスタです。
preservedレジスタの値は関数呼び出しの前後で変化してはいけません。ですので、呼び出された関数(訳注:callの受け身で"callee"と呼ばれます)は、リターンする前にその値をもとに戻す場合に限り、その値を上書きできます。そのため、これらのレジスタはcallee-savedと呼ばれます。よくとられる方法は、関数の最初でそのレジスタをスタックに保存し、リターンする直前にその値をもとに戻すことです。
それとは対照的に、呼び出された関数はscratchレジスタを何の制限もなく上書きすることができます。呼び出し元の関数がscratchレジスタの値を関数呼び出しの前後で保存したいなら、関数呼び出しの前に自分で(スタックにプッシュするなどして)バックアップしておいて、もとに戻す必要があります。なので、scratchレジスタはcaller-savedです。
x86_64においては、C言語の呼び出し規約は以下のpreservedレジスタとscratchレジスタを指定します:
| preservedレジスタ | scratchレジスタ |
| ----------------------------------------------- | ----------------------------------------------------------- |
| `rbp`, `rbx`, `rsp`, `r12`, `r13`, `r14`, `r15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11` |
| _callee-saved_ | _caller-saved_ |
コンパイラはこれらのルールを知っているので、それにしたがってコードを生成します。例えば、ほとんどの関数は`push rbp`から始まるのですが、これは`rbp`をスタックにバックアップしているのです(`rbp`はcallee-savedなレジスタであるため)。
### すべてのレジスタを保存する
関数呼び出しとは対象的に、例外は **どんな命令の最中にも** 起きる可能性があります。多くの場合、生成されたコードが例外を引き起こすのかどうかは、コンパイル時には見当も付きません。例えば、コンパイラはある命令がスタックオーバーフローやページフォルトを起こすのか知ることができません。
いつ例外が起きるのかわからない以上、レジスタを事前にバックアップしておくことは不可能です。つまり、caller-savedレジスタを利用する呼び出し規約は、例外ハンドラには使えないということです。代わりに、 **すべてのレジスタを** 保存する規約を使わないといけません。`x86-interrupt`呼び出し規約はそのような呼び出し規約なので、関数が戻るときにすべてのレジスタが元の値に戻されることを保証してくれるというわけです。
これは、関数の初めにすべてのレジスタがスタックに保存されるということを意味しないことに注意してください。その代わりに、コンパイラは関数によって上書きされてしまうレジスタのみをバックアップします。こうすれば、数個のレジスタしか使わない短い関数に対して、とても効率的なコードが生成できるでしょう。
### 割り込み時のスタックフレーム
通常の関数呼び出し(`call`命令を使います)においては、CPUは対象の関数にジャンプする前にリターンアドレスをプッシュします。関数がリターンするとき(`ret`命令を使います)、CPUはこのリターンアドレスをポップし、そこにジャンプします。そのため、通常の関数呼び出しの際のスタックフレームは以下のようになっています:

しかし、例外と割り込みハンドラについては、リターンアドレスをプッシュするだけではだめです。なぜなら、割り込みハンドラはしばしば(スタックポインタや、CPUフラグなどが)異なる状況で実行されるからです。ですので、代わりに、CPUは割り込みが起こると以下の手順を実行します。
1. **スタックポインタをアラインする**: 割り込みはあらゆる命令において発生しうるので、スタックポインタもあらゆる値を取る可能性があります。しかし、CPU命令のうちいくつか(例えばSSE命令の一部など)はスタックポインタが16バイトの倍数になっていることを要求するので、そうなるようにCPUは割り込みの直後にスタックポインタを揃えます。
2. (場合によっては)**スタックを変更する**: スタックの変更は、例えばCPU例外がユーザーモードのプログラムで起こった場合のような、CPUの特権レベルを変更するときに起こります。いわゆる割り込みスタック表を使うことで、特定の割り込みに対しスタックを変更するよう設定することも可能です。割り込みスタック表については次の記事で説明します。
3. **古いスタックポインタをプッシュする**: CPUは、割り込みが発生した際の(アラインされる前の)スタックポインタレジスタ(`rsp`)とスタックセグメントレジスタ(`ss`)の値をプッシュします。これにより、割り込みハンドラからリターンしてきたときにもとのスタックポインタを復元することが可能になります。
4. **`RFLAGS`レジスタをプッシュして更新する**: [`RFLAGS`]レジスタは状態や制御のための様々なビットを保持しています。割り込みに入るとき、CPUはビットのうちいくつかを変更し古い値をプッシュしておきます。
5. **命令ポインタをプッシュする**: 割り込みハンドラ関数にジャンプする前に、CPUは命令ポインタ(`rip`)とコードセグメント(`cs`)をプッシュします。これは通常の関数呼び出しにおける戻り値のプッシュに対応します。
6. (例外によっては)**エラーコードをプッシュする**: ページフォルトのような特定の例外の場合、CPUはエラーコードをプッシュします。これは、例外の原因を説明するものです。
7. **割り込みハンドラを呼び出す**: CPUは割り込みハンドラ関数のアドレスとセグメント記述子をIDTの対応するフィールドから読み出します。そして、この値を`rip`と`cs`レジスタに書き出してから、ハンドラを呼び出します。
[`RFLAGS`]: https://en.wikipedia.org/wiki/FLAGS_register
ですので、割り込み時のスタックフレーム (interrupt stack frame) は以下のようになります:

`x86_64`クレートにおいては、割り込み時のスタックフレームは[`InterruptStackFrame`]構造体によって表現されます。これは割り込みハンドラに`&mut`として渡されるため、これを使うことで例外の原因に関して追加で情報を手に入れることができます。例外のすべてがエラーコードをプッシュするわけではないので、この構造体にはエラーコードのためのフィールドはありません。これらの例外は[`HandlerFuncWithErrCode`]という別の関数型を使いますが、これらは追加で`error_code`引数を持ちます。
[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptStackFrame.html
### 舞台裏では何が
`x86-interrupt`呼び出し規約は、この例外処理プロセスのややこしいところをほぼ全て隠蔽してくれる、強力な抽象化です。しかし、その後ろで何が起こっているのかを知っておいたほうが良いこともあるでしょう。以下に、`x86-interrupt`呼び出し規約がやってくれることを簡単なリストにして示しました。
- **引数を取得する**: 多くの呼び出し規約においては、引数はレジスタを使って渡されることを想定しています。例外ハンドラにおいては、スタックにバックアップする前にレジスタの値を上書きしてはいけないので、これは不可能です。その代わり、`x86-interrupt`呼び出し規約は、引数が既に特定のオフセットでスタック上にあることを認識しています。
- **`iretq`を使ってリターンする**: 割り込み時のスタックフレームは通常の関数呼び出しのスタックフレームとは全く異なるため、通常の `ret` 命令を使ってハンドラ関数から戻ることはできません。その代わりに、`iretq` 命令を使う必要があります。
- **エラーコードを処理する**: いくつかの例外の場合、エラーコードがプッシュされるのですが、これが状況をより複雑にします。エラーコードはスタックのアラインメントを変更し(次の箇条を参照)、リターンする前にスタックからポップされる必要があるのです。`x86-interrupt`呼び出し規約は、このややこしい仕組みをすべて処理してくれます。しかし、どのハンドラ関数がどの例外に使われているかは呼び出し規約側にはわからないので、関数の引数の数からその情報を推測する必要があります。つまり、プログラマはやはりそれぞれの例外に対して正しい関数型を使う責任があるということです。幸いにも、`x86_64`クレートで定義されている`InterruptDescriptorTable`型が、正しい関数型が確実に使われるようにしてくれます。
- **スタックをアラインする**: 一部の命令(特にSSE命令)には、16バイトのスタックアラインメントを必要とするものがあります。CPUは例外が発生したときには必ずこのようにスタックが整列されることを保証しますが、例外の中には、エラーコードをプッシュして再びスタックの整列を壊してしまうものもあります。この場合、`x86-interrupt`の呼び出し規約は、スタックを再整列させることでこの問題を解決します。
もしより詳しく知りたい場合は、例外の処理について[naked function][naked functions]を使って説明する一連の記事があります。[この記事の最下部][too-much-magic]にそこへのリンクがあります。
[naked functions]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
[too-much-magic]: #sasuganijian-dan-sugi
## 実装
理屈は理解したので、私達のカーネルでCPUの例外を実際に処理していきましょう。まず、`src/interrupts.rs`に割り込みのための新しいモジュールを作ります。このモジュールはまず、`init_idt`関数という、新しい`InterruptDescriptorTable`を作る関数を定義します。
``` rust
// in src/lib.rs
pub mod interrupts;
// in src/interrupts.rs
use x86_64::structures::idt::InterruptDescriptorTable;
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
}
```
これで、ハンドラ関数を追加していくことができます。まず、[ブレークポイント例外][breakpoint exception]のハンドラを追加するところから始めましょう。ブレークポイント例外は、例外処理のテストをするのにうってつけの例外なのです。この例外の唯一の目的は、ブレークポイント命令`int3`が実行された時、プログラムを一時停止させることです。
[breakpoint exception]: https://wiki.osdev.org/Exceptions#Breakpoint
ブレークポイント例外はよくデバッガによって使われます。ユーザーがブレークポイントを設定すると、デバッガが対応する命令を`int3`命令で置き換え、その行に到達したときにCPUがブレークポイント例外を投げるようにするのです。ユーザがプログラムを続行したい場合は、デバッガは`int3`命令をもとの命令に戻してプログラムを再開します。より詳しく知るには、[How debuggers work]["_How debuggers work_"]というシリーズ記事を読んでください。
["_How debuggers work_"]: https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
今回の場合、命令を上書きしたりする必要はありません。ブレークポイント命令が実行された時、メッセージを表示したうえで実行を継続したいだけです。ですので、単純な`breakpoint_handler`関数を作ってIDTに追加してみましょう。
```rust
// in src/interrupts.rs
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
use crate::println;
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
}
extern "x86-interrupt" fn breakpoint_handler(
stack_frame: InterruptStackFrame)
{
println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
}
```
私達のハンドラは、ただメッセージを出力し、割り込みスタックフレームを整形して出力するだけです。
これをコンパイルしようとすると、以下のエラーが起こります:
```
error[E0658]: x86-interrupt ABI is experimental and subject to change (see issue #40180)
--> src/main.rs:53:1
|
53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {
54 | | println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
55 | | }
| |_^
|
= help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable
```
このエラーは、`x86-interrupt`呼び出し規約がまだ不安定なために発生します。これを強制的に使うためには、`lib.rs`の最初に`#![feature(abi_x86_interrupt)]`を追記して、この機能を明示的に有効化してやる必要があります。
### IDTを読み込む
CPUがこの割り込みディスクリプタテーブル(IDT)を使用するためには、[`lidt`]命令を使ってこれを読み込む必要があります。`x86_64`の`InterruptDescriptorTable`構造体には、そのための[`load`][InterruptDescriptorTable::load]というメソッド関数が用意されています。それを使ってみましょう:
[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt
[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load
```rust
// in src/interrupts.rs
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt.load();
}
```
これをコンパイルしようとすると、以下のエラーが発生します:
```
error: `idt` does not live long enough
--> src/interrupts/mod.rs:43:5
|
43 | idt.load();
| ^^^ does not live long enough
44 | }
| - borrowed value only lives until here
|
= note: borrowed value must be valid for the static lifetime...
```
`load`メソッドは(`idt`に)`&'static self`、つまりプログラムの実行されている間ずっと有効な参照を期待しています。これは、私達が別のIDTを読み込まない限り、CPUは割り込みのたびにこの表にアクセスするからです。そのため、`'static`より短いライフタイムの場合、use-after-freeバグが発生する可能性があります。
実際、これはまさにここで起こっていることです。私達の`idt`はスタック上に生成されるので、`init`関数の中でしか有効ではないのです。この関数が終わると、このスタックメモリは他の関数に使い回されるので、CPUはどこかもわからないスタックメモリをIDTとして解釈してしまうのです。幸運にも、`InterruptDescriptorTable::load`メソッドは関数定義にこのライフタイムの要件を組み込んでいるので、Rustコンパイラはこのバグをコンパイル時に未然に防ぐことができたというわけです。
この問題を解決するには、`idt`を`'static`なライフタイムの場所に格納する必要があります。これを達成するには、[`Box`]を使ってIDTをヒープに割当て、続いてそれを`'static`な参照に変換すればよいです。しかし、私達はOSのカーネルを書いている途中であり、(まだ)ヒープを持っていません。
[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html
別の方法として、IDTを`static`として保存してみましょう:
```rust
static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
pub fn init_idt() {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
```
しかし、問題が発生します:staticは不変なので、`init`関数でエントリを変更することができません。これは[`static mut`]を使って解決できそうです:
[`static mut`]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
```rust
static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
pub fn init_idt() {
unsafe {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
}
```
このように変更するとエラーなくコンパイルできますが、このような書き方は全く慣用的ではありません。`static mut`はデータ競合を非常に起こしやすいので、アクセスするたびに[unsafeブロック][`unsafe` block]が必要になります。
[`unsafe` block]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers
#### Lazy Staticsにおまかせ
幸いにも、例の`lazy_static`マクロが存在します。このマクロは`static`をコンパイル時に評価する代わりに、最初に参照されたときに初期化を行います。このため、初期化時にはほとんどすべてのことができ、実行時にのみ決定する値を読み込むこともできます。
[VGAテキストバッファの抽象化をした][vga text buffer lazy static]ときに、すでに`lazy_static`クレートはインポートしました。そのため、すぐに`lazy_static!`マクロを使って静的なIDTを作ることができます。
[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.ja.md#dai-keta-lazy-jing-de-bian-shu
```rust
// in src/interrupts.rs
use lazy_static::lazy_static;
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt
};
}
pub fn init_idt() {
IDT.load();
}
```
この方法では`unsafe`ブロックが必要ないことに注目してください。`lazy_static!`マクロはその内部で`unsafe`を使ってはいるのですが、これは安全なインターフェースの中に抽象化されているのです。
### 実行する
カーネルで例外を動作させるための最後のステップは、`main.rs`から`init_idt`関数を呼び出すことです。直接呼び出す代わりに、より一般的な`init`関数を`lib.rs`に導入します:
```rust
// in src/lib.rs
pub fn init() {
interrupts::init_idt();
}
```
この関数により、`main.rs`、`lib.rs`および結合テストにおける、異なる`_start`関数で共有される、初期化ルーチンの「中央広場」ができました。
`main.rs`内の`_start`関数を更新して、`init`を呼び出し、そのあとブレークポイント例外を発生させるようにしてみましょう:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
blog_os::init(); // new
// invoke a breakpoint exception
x86_64::instructions::interrupts::int3(); // new
// as before
#[cfg(test)]
test_main();
println!("It did not crash!");
loop {}
}
```
(`cargo run`を使って)QEMU内でこれを実行すると、以下のようになります

うまくいきました!CPUは私達のブレークポイントハンドラを呼び出すのに成功し、これがメッセージを出力し、そのあと`_start`関数に戻って、`It did not crash!`のメッセージを出力しました。
割り込みスタックフレームは、例外が発生した時の命令とスタックポインタを教えてくれることがわかります。これは、予期せぬ例外をデバッグする際に非常に便利です。
### テストを追加する
上記の動作が継続することを確認するテストを作成してみましょう。まず、`_start` 関数を更新して `init` を呼び出すようにします。
```rust
// in src/lib.rs
/// Entry point for `cargo test`
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
init(); // new
test_main();
loop {}
}
```
Rustのテストでは、`main.rs`とは全く無関係に`lib.rs`をテストするので、この`_start`関数は`cargo test --lib`を実行する際に使用されることを思い出してください。テストを実行する前にIDTを設定するために、ここで`init`を呼び出す必要があります。
では、`test_breakpoint_exception`テストを作ってみましょう:
```rust
// in src/interrupts.rs
#[test_case]
fn test_breakpoint_exception() {
// invoke a breakpoint exception
x86_64::instructions::interrupts::int3();
}
```
このテストでは、`int3`関数を呼び出してブレークポイント例外を発生させます。その後も実行が続くことを確認することで、ブレークポイントハンドラが正しく動作していることを保証します。
この新しいテストを試すには、`cargo test`(すべてのテストを試したい場合)または`cargo test --lib`(`lib.rs`とそのモジュールのテストのみの場合)を実行すればよいです。出力は以下のようになるはずです:
```
blog_os::interrupts::test_breakpoint_exception... [ok]
```
## さすがに簡単すぎ?
`x86-interrupt`呼び出し規約と[`InterruptDescriptorTable`]型のおかげで、例外処理のプロセスは比較的わかりやすく、面倒なところはありませんでした。「これではさすがに簡単すぎる、例外処理の闇をすべて学び尽くしたい」というあなた向けの記事もあります:私達の[Handling Exceptions with Naked Functions][“Handling Exceptions with Naked Functions”]シリーズ(未訳)では、`x86-interrupt`呼び出し規約を使わずに例外を処理する方法を学び、さらには独自のIDT型を定義します。`x86-interrupt`呼び出し規約や、`x86_64`クレートが存在する前は、これらの記事が主な例外処理に関する記事でした。なお、これらの記事はこのブログの[第1版][first edition]をもとにしているので、内容が古くなっている可能性があることに注意してください。
[“Handling Exceptions with Naked Functions”]: @/edition-1/extra/naked-exceptions/_index.md
[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
[first edition]: @/edition-1/_index.md
## 次は?
例外を捕捉し、そこから戻ってくることに成功しました!次のステップは、すべての例外を捕捉できるようにすることです。なぜなら、補足されなかった例外は致命的な[トリプルフォルト][triple fault]を引き起こし、これはシステムリセットにつながってしまうからです。次の記事では、[ダブルフォルト][double faults]を正しく捕捉することで、これを回避できることを説明します。
[triple fault]: https://wiki.osdev.org/Triple_Fault
[double faults]: https://wiki.osdev.org/Double_Fault#Double_Fault
================================================
FILE: blog/content/edition-2/posts/05-cpu-exceptions/index.ko.md
================================================
+++
title = "CPU 예외 (Exception)"
weight = 5
path = "ko/cpu-exceptions"
date = 2018-06-17
[extra]
# Please update this when updating the translation
translation_based_on_commit = "1c9b5edd6a5a667e282ca56d6103d3ff1fd7cfcb"
# GitHub usernames of the people that translated this post
translators = ["JOE1994"]
# GitHub usernames of the people that contributed to this translation
translation_contributors = ["KimWang906"]
+++
CPU 예외 (exception)는 유효하지 않은 메모리 주소에 접근하거나 분모가 0인 나누기 연산을 하는 등 허용되지 않은 작업 실행 시 발생합니다. CPU 예외를 처리할 수 있으려면 예외 처리 함수 정보를 제공하는 _인터럽트 서술자 테이블 (interrupt descriptor table; IDT)_ 을 설정해 두어야 합니다. 이 글에서는 커널이 [breakpoint 예외][breakpoint exceptions]를 처리한 후 정상 실행을 재개할 수 있도록 구현할 것입니다.
[breakpoint exceptions]: https://wiki.osdev.org/Exceptions#Breakpoint
이 블로그는 [GitHub 저장소][GitHub]에서 오픈 소스로 개발되고 있으니, 문제나 문의사항이 있다면 저장소의 'Issue' 기능을 이용해 제보해주세요. [페이지 맨 아래][at the bottom]에 댓글을 남기실 수도 있습니다. 이 포스트와 관련된 모든 소스 코드는 저장소의 [`post-05 브랜치`][post branch]에서 확인하실 수 있습니다.
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05
## 개요
예외 (exception)는 현재 실행 중인 CPU 명령어에 문제가 있음을 알립니다. 예를 들면, 분모가 0인 나누기 연산을 CPU 명령어가 하려고 하면 CPU가 예외를 발생시킵니다. 예외가 발생하게 되면 CPU는 진행 중인 작업을 일시 중단한 후 즉시 예외 처리 함수 (exception handler)를 호출합니다 (발생한 예외의 종류에 따라 호출될 예외 처리 함수가 결정됩니다).
x86 아키텍처에는 20가지 정도의 CPU 예외가 존재합니다. 그 중 제일 중요한 것들은 아래와 같습니다:
- **페이지 폴트 (Page Fault)**: 접근이 허용되지 않은 메모리에 접근을 시도하는 경우 페이지 폴트가 발생하게 됩니다. 예를 들면, CPU가 실행하려는 명령어가 (1) 매핑되지 않은 페이지로부터 데이터를 읽어오려고 하거나, (2) 읽기 전용 페이지에 데이터를 쓰려고 하는 경우에 페이지 폴트가 발생합니다.
- **유효하지 않은 Opcode**: CPU에 주어진 명령어의 Opcode를 CPU가 지원하지 않을 때 발생합니다. 새로 출시된 [SSE 명령어][SSE instructions]를 구식 CPU에서 실행하려 하면 예외가 발생하게 됩니다.
- **General Protection Fault**: 이 예외는 가장 광범위한 원인을 가진 예외입니다. 사용자 레벨 코드에서 권한 수준이 높은 명령어 (privileged instruction)를 실행하거나 configuration 레지스터를 덮어 쓰는 등 다양한 접근 권한 위반 상황에 발생합니다.
- **더블 폴트 (Double Fault)**: 예외 발생 시 CPU는 알맞은 예외 처리 함수의 호출을 시도합니다. _예외 처리 함수를 호출하는 도중에_ 또 예외가 발생하는 경우, CPU는 더블 폴트 (double fault) 예외를 발생시킵니다. 또한 예외를 처리할 예외 처리 함수가 등록되지 않은 경우에도 더블 폴트 예외가 발생합니다.
- **트리플 폴트 (Triple Fault)** : CPU가 더블 폴트 예외 처리 함수를 호출하려고 하는 사이에 예외가 발생하는 경우, CPU는 치명적인 _트리플 폴트 (triple fault)_ 예외를 발생시킵니다. 트리플 폴트 예외를 처리하는 것은 불가능 하므로 대부분의 프로세서들은 트리플 폴트 발생 시 프로세서를 초기화하고 운영체제를 재부팅합니다.
[SSE instructions]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
모든 CPU 예외들의 목록을 보시려면 [OSDev wiki][exceptions]를 확인해주세요.
[exceptions]: https://wiki.osdev.org/Exceptions
### 인터럽트 서술사 테이블 (Interrupt Descriptor Table) {#the-interrupt-descriptor-table}
예외 발생을 포착하고 대응할 수 있으려면 _인터럽트 서술자 테이블 (Interrupt Descriptor Table; IDT)_ 이 필요합니다.
이 테이블을 통해 우리는 각각의 CPU 예외를 어떤 예외 처리 함수가 처리할지 지정합니다. 하드웨어에서 이 테이블을 직접 사용하므로 테이블의 형식은 정해진 표준에 따라야 합니다. 테이블의 각 엔트리는 아래와 같은 16 바이트 구조를 따릅니다:
| 타입 | 이름 | 설명 |
| ---- | ------------------------ | ------------------------------------------------------------------------------------------------------- |
| u16 | Function Pointer [0:15] | 예외 처리 함수에 대한 64비트 포인터의 하위 16비트 |
| u16 | GDT selector | [전역 서술자 테이블 (global descriptor table)][global descriptor table]에서 코드 세그먼트를 선택하는 값 |
| u16 | Options | (표 아래의 설명 참조) |
| u16 | Function Pointer [16:31] | 예외 처리 함수에 대한 64비트 포인터의 2번째 하위 16비트 |
| u32 | Function Pointer [32:63] | 예외 처리 함수에 대한 64비트 포인터의 상위 32비트 |
| u32 | Reserved | 사용 보류 중인 영역 |
[global descriptor table]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
Options 필드는 아래의 형식을 갖습니다:
| 비트 구간 | 이름 | 설명 |
| --------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| 0-2 | Interrupt Stack Table Index | 0: 스택을 교체하지 않는다, 1-7: 이 인터럽트 처리 함수가 호출된 경우 Interrupt Stack Table의 n번째 스택으로 교체한다. |
| 3-7 | Reserved | 사용 보류 중인 영역 |
| 8 | 0: Interrupt Gate, 1: Trap Gate | 비트가 0이면 이 예외 처리 함수가 호출 이후 인터럽트 발생 억제 |
| 9-11 | must be one | 각 비트는 언제나 1 |
| 12 | must be zero | 언제나 0 |
| 13‑14 | Descriptor Privilege Level (DPL) | 이 예외 처리 함수를 호출하는 데에 필요한 최소 특권 레벨 |
| 15 | Present |
각 예외마다 IDT에서의 인덱스가 배정되어 있습니다. invalid opcode 예외는 테이블 인덱스 6이 배정되어 있고, 페이지 폴트 예외는 테이블 인덱스 14가 배정되어 있습니다. 하드웨어는 미리 배정된 인덱스를 이용해 각 예외에 대응하는 IDT 엔트리를 자동으로 불러올 수 있습니다. OSDev 위키의 [Exception Table][exceptions]의 “Vector nr.”로 명명된 열을 보시면 모든 예외 및 배정된 인덱스를 확인하실 수 있습니다.
예외가 발생하면 CPU는 대략 아래의 작업들을 순서대로 진행합니다:
1. Instruction Pointer 레지스터와 [RFLAGS] 레지스터를 비롯해 몇몇 레지스터들의 값을 스택에 push (저장)합니다 (나중에 이 값들을 사용할 것입니다).
2. 발생한 예외의 엔트리를 인터럽트 서술사 테이블 (IDT)로부터 읽어옵니다. 예를 들면, 페이지 폴트 발생 시 CPU는 IDT의 14번째 엔트리를 읽어옵니다.
3. 등록된 엔트리가 없을 경우, 더블 폴트 예외를 발생시킵니다.
4. 해당 엔트리가 인터럽트 게이트인 경우 (40번 비트 = 0), 하드웨어 인터럽트 발생을 억제합니다.
5. 지정된 [GDT] 선택자를 CS 세그먼트로 읽어옵니다.
6. 지정된 예외 처리 함수로 점프합니다.
[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register
[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
위의 4단계와 5단계가 잘 이해되지 않아도 걱정 마세요. 전역 서술자 테이블 (Global Descriptor Table; GDT)과 하드웨어 인터럽트는 이후에 다른 글에서 더 설명할 것입니다.
## IDT 타입
IDT를 나타내는 타입을 직접 구현하지 않고 `x86_64` 크레이트의 [`InterruptDescriptorTable` 구조체][`InterruptDescriptorTable` struct] 타입을 사용합니다:
[`InterruptDescriptorTable` struct]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
``` rust
#[repr(C)]
pub struct InterruptDescriptorTable {
pub divide_by_zero: Entry,
pub debug: Entry,
pub non_maskable_interrupt: Entry,
pub breakpoint: Entry,
pub overflow: Entry,
pub bound_range_exceeded: Entry,
pub invalid_opcode: Entry,
pub device_not_available: Entry,
pub double_fault: Entry,
pub invalid_tss: Entry,
pub segment_not_present: Entry,
pub stack_segment_fault: Entry,
pub general_protection_fault: Entry,
pub page_fault: Entry,
pub x87_floating_point: Entry,
pub alignment_check: Entry,
pub machine_check: Entry,
pub simd_floating_point: Entry,
pub virtualization: Entry,
pub security_exception: Entry,
// 일부 필드는 생략했습니다
}
```
구조체의 각 필드는 IDT의 엔트리를 나타내는 `idt::Entry` 타입을 가집니다. 타입 인자 `F`는 사용될 예외 처리 함수의 타입을 정의합니다. 어떤 엔트리는 `F`에 [`HandlerFunc`]를 또는 `F`에 [`HandlerFuncWithErrCode`]를 필요로 하며 페이지 폴트는 [`PageFaultHandlerFunc`]를 필요로 합니다.
[`idt::Entry`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.Entry.html
[`HandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFunc.html
[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFuncWithErrCode.html
[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.PageFaultHandlerFunc.html
`HandlerFunc` 타입을 먼저 살펴보겠습니다:
```rust
type HandlerFunc = extern "x86-interrupt" fn(_: InterruptStackFrame);
```
`HandlerFunc`는 함수 타입 `extern "x86-interrupt" fn`의 [타입 별칭][type alias]입니다. `extern` 키워드는 [외부 함수 호출 규약 (foreign calling convention)][foreign calling convention]을 사용하는 함수를 정의할 때 쓰이는데, 주로 C 함수와 상호작용하는 경우에 쓰입니다 (`extern "C" fn`). `x86-interrupt` 함수 호출 규약은 무엇일까요?
[type alias]: https://doc.rust-lang.org/book/ch20-03-advanced-types.html#creating-type-synonyms-with-type-aliases
[foreign calling convention]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions
## 인터럽트 호출 규약
예외는 함수 호출과 유사한 점이 많습니다: 호출된 함수의 첫 명령어로 CPU가 점프한 후 함수 안의 명령어들을 차례대로 실행합니다. 그 후 CPU가 반환 주소로 점프하고, 기존에 실행 중이었던 함수의 실행을 이어갑니다.
하지만 예외와 함수 호출 사이에 중요한 차이점이 있습니다: 일반 함수의 경우 컴파일러가 삽입한 `call` 명령어를 통해 호출하지만, 예외는 _어떤 명령어 실행 도중에라도_ 발생할 수 있습니다. 이 차이점의 중대성을 이해하려면 함수 호출 과정을 더 면밀히 살펴봐야 합니다.
[함수 호출 규약][Calling conventions]은 함수 호출 과정의 세부 사항들을 규정합니다. 예를 들면, 함수 인자들이 어디에 저장되는지 (레지스터 또는 스택), 함수의 반환 값을 어떻게 전달할지 등을 정합니다. x86_64 리눅스에서 C 함수 호출 시 [System V ABI]가 규정하는 아래의 규칙들이 적용됩니다:
[Calling conventions]: https://en.wikipedia.org/wiki/Calling_convention
[System V ABI]: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
- 함수의 첫 여섯 인자들은 `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9` 레지스터에 저장합니다
- 7번째 함수 인자부터는 모두 스택에 저장합니다
- 함수의 반환 값은 `rax`와 `rdx` 레지스터에 저장됩니다
참고로 Rust는 C ABI를 따르지 않기에 (사실, [Rust는 규정된 ABI가 존재하지 않습니다][rust abi]), 이 법칙들은 `extern "C" fn`으로 정의된 함수들에만 적용됩니다.
[rust abi]: https://github.com/rust-lang/rfcs/issues/600
### Preserved 레지스터와 Scratch 레지스터
함수 호출 규약은 레지스터들을 크게 두 가지 (_preserved_ 레지스터와 _scratch_ 레지스터)로 분류합니다.
_preserved_ 레지스터들의 값은 함수 호출 전/후에 보존되어야 합니다. 호출된 함수 (callee)가 이 레지스터들에 다른 값을 저장해 사용하려면 반환 직전에 이 레지스터들에 원래 저장되어 있던 값을 복원해 놓아야 합니다. preserved 레지스터는 _“callee-saved”_ 레지스터라고도 불립니다. 함수 실행 시작 시 이 레지스터들의 값들을 스택에 저장했다가 함수 반환 직전에 복구하는 것이 일반적입니다.
반면, 호출된 함수가 _scratch_ 레지스터들의 값을 자유롭게 덮어 쓰는 것은 괜찮습니다. 함수 호출 전/후로 scratch 레지스터의 값을 보존하고 싶다면, 호출하는 측 (caller)이 함수 호출 전에 레지스터의 값을 스택에 저장해뒀다가 함수의 실행이 끝난 후 레지스터의 값을 본래 값으로 복원해야 합니다. scratch 레지스터는 _“caller-saved”_ 레지스터라고도 불립니다.
x86_64에서는 C 함수 호출 규약이 preserved 레지스터와 scratch 레지스터를 아래와 같이 정합니다:
| preserved 레지스터 | scratch 레지스터 |
| ----------------------------------------------- | ----------------------------------------------------------- |
| `rbp`, `rbx`, `rsp`, `r12`, `r13`, `r14`, `r15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11` |
| _callee-saved_ | _caller-saved_ |
컴파일러는 이 규칙들에 따라 코드를 컴파일 합니다. 예를 들면 대부분의 함수들은 `push rbp` 로 시작하는데, 이는 callee-saved 레지스터인 `rbp`를 스택에 저장합니다.
### 모든 레지스터들의 값 보존하기
함수 호출과 달리 예외는 _어떤_ 명령어가 실행 중이든 관계 없이 발생할 수 있습니다. 대체로 컴파일 시간에는 컴파일 결과 생성된 코드가 예외를 발생시킬지의 유무를 장담하기 어렵습니다. 예를 들어, 컴파일러는 임의의 명령어가 스택 오버플로우 또는 페이지 폴트를 일으킬지 판별하기 어렵습니다.
예외가 언제 발생할지 알 수 없다보니 레지스터에 저장된 값들을 미리 백업해놓을 수가 없습니다. 즉, 예외 처리 함수 구현 시 caller-saved 레지스터에 의존하는 함수 호출 규약을 사용할 수가 없습니다. 예외 처리 함수 구현 시 _모든 레지스터_ 들의 값을 보존하는 함수 호출 규약을 사용해야 합니다. 예시로 `x86-interrupt` 함수 호출 규약은 함수 반환 시 모든 레지스터들의 값이 함수 호출 이전과 동일하게 복원되도록 보장합니다.
함수 실행 시작 시 모든 레지스터들의 값이 스택에 저장된다는 뜻은 아닙니다. 호출된 함수가 덮어 쓸 레지스터들만을 컴파일러가 스택에 백업합니다. 이렇게 하면 적은 수의 레지스터를 사용하는 함수를 컴파일 할 때 짧고 효율적인 코드를 생성할 수 있습니다.
### 인터럽트 스택 프레임 {# the-interrupt-stack-frame}
일반적인 함수 호출 시 (`call` 명령어 이용), CPU는 호출된 함수로 제어 흐름을 넘기기 전에 반환 주소를 스택에 push (저장)합니다. 함수 반환 시 (`ret` 명령어 이용), CPU는 스택에 저장해뒀던 반환 주소를 읽어온 후 해당 주소로 점프합니다. 일반적인 함수 호출 시 스택 프레임의 모습은 아래와 같습니다:

예외 및 인터럽트 처리 함수의 경우, 일반 함수가 실행되는 CPU 컨텍스트 (스택 포인터, CPU 플래그 등)가 아닌 별개의 CPU 컨텍스트에서 실행됩니다. 따라서 단순히 스택에 반환 주소를 push하는 것보다 더 복잡한 사전 처리가 필요합니다. 인터럽트 발생 시 CPU가 아래의 작업들을 처리합니다.
1. **스택 포인터 정렬**: 인터럽트는 어느 명령어의 실행 중에도 발생할 수 있고, 따라서 스택 포인터 또한 임의의 값을 가질 수 있습니다. 하지만 특정 CPU 명령어들 (예: 일부 SSE 명령어)은 스택 포인터가 16바이트 단위 경계에 정렬되어 있기를 요구합니다. 따라서 CPU는 인터럽트 발생 직후에 스택 포인터를 알맞게 정렬합니다.
2. **스택 교체** (경우에 따라서): CPU의 특권 레벨 (privilege level)이 바뀌는 경우에 스택 교체가 일어납니다 (예: 사용자 모드 프로그램에서 CPU 예외가 발생할 때). 또한 _인터럽트 스택 테이블 (Interrupt Stack Table)_ 을 이용해 특정 인터럽트 발생 시 스택 교체가 이뤄지도록 설정하는 것 또한 가능합니다 (이후 다른 글에서 설명할 내용입니다).
3. **이전의 스택 포인터 push**: 인터럽트 발생 시, CPU는 스택 포인터를 정렬하기에 앞서 스택 포인터 (`rsp`)와 스택 세그먼트 (`ss`) 레지스터들을 저장 (push)합니다. 이로써 인터럽트 처리 함수로부터 반환 시 이전의 스택 포인터를 복원할 수 있습니다.
4. **`RFLAGS` 레지스터 push 및 수정**: [`RFLAGS`] 레지스터는 CPU의 다양한 제어 및 상태 비트들을 저장합니다. 인터럽트 발생 시 CPU는 기존 값을 push한 후 일부 비트들의 값을 변경합니다.
5. **instruction pointer push**: 인터럽트 처리 함수로 점프하기 전에, CPU는 instruction pointer (`rip`)와 code segment (`cs`) 레지스터들을 push합니다. 이는 일반 함수 호출 시 반환 주소를 push하는 것과 유사합니다.
6. **오류 코드 push** (일부 예외만 해당): 페이지 폴트 같은 일부 예외의 경우, CPU는 예외의 원인을 설명하는 오류 코드를 push합니다.
7. **인터럽트 처리 함수 호출**: CPU는 IDT로부터 인터럽트 처리 함수의 주소와 세그먼트 서술자 (segment descriptor)를 읽어옵니다. 읽어온 값들을 각각 `rip` 레지스터와 `cs` 레지스터에 저장함으로써 인터럽트 처리 함수를 호출합니다.
[`RFLAGS`]: https://en.wikipedia.org/wiki/FLAGS_register
_인터럽트 스택 프레임_ 은 아래와 같은 모습을 가집니다:

`x86_64` 크레이트에서는 [`InterruptStackFrame`] 구조체 타입을 통해 인터럽트 스택 프레임을 구현합니다. 예외 처리 함수들은 `&mut InterruptStackFrame`를 인자로 받아서 예외 발생 원인에 대한 추가 정보를 얻을 수 있습니다. 이 구조체는 오류 코드를 저장하는 필드를 갖고 있지 않은데, 그 이유는 아주 일부의 예외들만이 오류 코드를 반환하기 때문입니다. 오류 코드를 반환하는 예외들은 [`HandlerFuncWithErrCode`] 함수 타입을 사용하는데, 이 함수 타입은 추가적으로 `error_code` 인자를 받습니다.
[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptStackFrame.html
### 무대 뒤의 상황
함수 호출 규약 `x86-interrupt`는 예외 처리 과정의 세부적인 사항들을 대부분 숨겨주는 강력한 추상화 계층입니다. 하지만 때로는 추상화 계층 안에서 무슨 일이 일어나는지 알아두는 것이 도움이 됩니다. 아래는 함수 호출 규약 `x86-interrupt`가 처리하는 작업들의 개요입니다.
- **함수 인자 읽어오기**: 대부분의 함수 호출 규약들은 함수 인자들이 레지스터를 통해 전달되는 것으로 생각합니다. 그러나 예외 처리 함수는 그렇게 할 수가 없습니다. 그 이유는 스택에 레지스터들의 값들을 백업하기 전에는 어떤 레지스터도 덮어 쓸 수 없기 때문입니다. 함수 호출 규약 `x86-interrupt`는 함수 인자들이 레지스터가 아니라 스택의 특정 위치에 저장되어 있다고 가정합니다.
- **`iretq`를 통해 반환**: 인터럽트 스택 프레임은 일반 함수 호출 시 사용되는 스택 프레임과는 완전히 별개의 것이라서 `ret` 명령어를 사용해서는 인터럽트 처리 함수로부터 제대로 반환할 수 없습니다. 대신 `iretq` 명령어를 사용하여 반환합니다.
- **오류 코드 처리**: 일부 예외에 한해 push되는 오류 코드는 일을 번거롭게 합니다. 이 오류 코드로 인해 스택 정렬이 망가뜨려지며 (아래 '스택 정렬' 항목 참고), 예외 처리 함수로부터 반환하기 전에 오류 코드를 스택으로부터 pop (제거)해야 합니다. 함수 호출 규약 `x86-interrupt`가 오류 코드로 인한 번거로움을 대신 감당해줍니다. `x86-interrupt`는 어떤 예외 처리 함수가 어떤 예외에 대응하는지 알지 못하기에, 함수의 인자 개수를 통해 해당 정보를 유추합니다. 따라서 개발자는 오류 코드가 push되는 예외와 그렇지 않은 예외에 대해 각각 정확한 함수 타입을 사용해야만 합니다. 다행히 `x86_64` 크레이트가 제공하는 `InterruptDescriptorTable` 타입이 각 경우에 정확한 함수 타입이 사용되도록 보장합니다.
- **스택 정렬**: 일부 명령어들 (특히 SSE 명령어)은 스택이 16 바이트 경계에 정렬되어 있기를 요구합니다. 예외 발생 시 CPU는 해당 정렬이 맞춰져 있도록 보장하지만, 일부 예외의 경우에는 오류 코드를 push하면서 맞춰져 있던 정렬을 망가뜨립니다. 함수 호출 규약 `x86-interrupt`는 해당 상황에서 망가진 정렬을 다시 맞춰줍니다.
더 자세한 내용이 궁금하시다면, [naked 함수][naked functions]를 사용한 예외 처리 과정을 설명하는 저희 블로그의 또다른 글 시리즈를 참고하세요 (링크는 [이 글의 맨 마지막][too-much-magic]을 참조).
[naked functions]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md
[too-much-magic]: #too-much-magic
## 구현
이론적 배경 설명은 끝났고, 이제 CPU 예외 처리 기능을 커널에 구현해보겠습니다. 새로운 모듈 `interrupts`를 `src/interrupts.rs`에 만든 후, 새로운 `InterruptDescriptorTable`을 생성하는 함수 `init_idt`를 작성합니다.
``` rust
// in src/lib.rs
pub mod interrupts;
// in src/interrupts.rs
use x86_64::structures::idt::InterruptDescriptorTable;
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
}
```
이제 예외 처리 함수들을 추가할 수 있습니다. [breakpoint 예외][breakpoint exception]를 위한 예외 처리 함수부터 작성해보겠습니다. breakpoint 예외는 예외 처리를 테스트하는 용도에 안성맞춤입니다. breakpoint 예외의 유일한 용도는 breakpoint 명령어 `int3`가 실행되었을 때 실행 중인 프로그램을 잠시 멈추는 것입니다.
[breakpoint exception]: https://wiki.osdev.org/Exceptions#Breakpoint
breakpoint 예외는 디버거 (debugger)에서 자주 사용됩니다: 사용자가 breakpoint를 설정하면 디버거는 breakpoint에 대응되는 명령어를 `int3` 명령어로 치환하는데, 이로써 해당 명령어에 도달했을 때 CPU가 breakpoint 예외를 발생시킵니다. 사용자가 프로그램 실행을 재개하면 디버거는 `int3` 명령어를 원래의 명령어로 다시 교체한 후 프로그램 실행을 재개합니다. 더 자세한 내용이 궁금하시면 ["_How debuggers work_"] 시리즈를 읽어보세요.
["_How debuggers work_"]: https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints
지금 우리가 breakpoint 예외를 사용하는 상황에서는 명령어를 덮어쓸 필요가 전혀 없습니다. 우리는 breakpoint 예외가 발생했을 때 그저 메시지를 출력한 후 프로그램 실행을 재개하기만 하면 됩니다. 간단한 예외 처리 함수 `breakpoint_handler`를 만들고 IDT에 추가합니다:
```rust
// in src/interrupts.rs
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
use crate::println;
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
}
extern "x86-interrupt" fn breakpoint_handler(
stack_frame: InterruptStackFrame)
{
println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
}
```
이 예외 처리 함수는 간단한 메시지와 함께 인터럽트 스택 프레임의 정보를 출력합니다.
컴파일을 시도하면 아래와 같은 오류 메시지가 출력됩니다:
```
error[E0658]: x86-interrupt ABI is experimental and subject to change (see issue #40180)
--> src/main.rs:53:1
|
53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {
54 | | println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
55 | | }
| |_^
|
= help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable
```
이 오류는 함수 호출 규약 `x86-interrupt`가 아직 unstable 하여 발생합니다. `lib.rs`의 맨 위에 `#![feature(abi_x86_interrupt)]` 속성을 추가하여 함수 호출 규약 `x86-interrupt`의 사용을 강제합니다.
### IDT 불러오기
우리가 만든 인터럽트 서술사 테이블을 CPU가 사용하도록 하려면, 먼저 [`lidt`] 명령어를 통해 해당 테이블을 불러와야 합니다. `x86_64` 크레이트가 제공하는 `InterruptDescriptorTable` 구조체의 함수 [`load`][InterruptDescriptorTable::load]를 통해 테이블을 불러옵니다:
[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt
[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load
```rust
// in src/interrupts.rs
pub fn init_idt() {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt.load();
}
```
컴파일 시 아래와 같은 오류가 발생합니다:
```
error: `idt` does not live long enough
--> src/interrupts/mod.rs:43:5
|
43 | idt.load();
| ^^^ does not live long enough
44 | }
| - borrowed value only lives until here
|
= note: borrowed value must be valid for the static lifetime...
```
`load` 함수는 `&'static self` 타입의 인자를 받는데, 이 타입은 프로그램 실행 시간 전체 동안 유효한 레퍼런스 타입입니다. 우리가 새로운 IDT를 로드하지 않는 이상 프로그램 실행 중 인터럽트가 발생할 때마다 CPU가 이 테이블에 접근할 것이기에, `'static` 라이프타임보다 짧은 라이프타임을 사용하면 use-after-free 버그가 발생할 수 있습니다.
`idt`는 스택에 생성되어 `init` 함수 안에서만 유효합니다. `init` 함수를 벗어나면 해당 스택 메모리는 다른 함수에 의해 재사용되므로 해당 메모리를 IDT로서 간주하고 참조한다면 임의의 함수의 스택 메모리로부터 데이터를 읽어오게 됩니다.
다행히 `InterruptDescriptorTable::load` 함수 정의에 라이프타임 요구 사항이 포함되어 있어 Rust 컴파일러가 잠재적인 use-after-free 버그를 컴파일 도중에 막아줍니다.
이 문제를 해결하려면 `idt`를 `'static` 라이프타임을 갖는 곳에 저장해야 합니다. [`Box`]를 통해 IDT를 힙 (heap) 메모리에 할당한 뒤 Box 에 저장된 IDT에 대한 `'static` 레퍼런스를 얻는 것은 해결책이 되지 못합니다. 그 이유는 아직 우리가 커널에 힙 메모리를 구현하지 않았기 때문입니다.
[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html
대안으로 `IDT`를 `static` 변수에 저장하는 것을 시도해보겠습니다:
```rust
static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
pub fn init_idt() {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
```
문제는 static 변수의 값은 변경할 수가 없어서, `init` 함수 실행 시 breakpoint 예외에 대응하는 IDT 엔트리를 수정할 수 없습니다.
대신 `IDT`를 [`static mut`] 변수에 저장해보겠습니다:
[`static mut`]: https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
```rust
static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();
pub fn init_idt() {
unsafe {
IDT.breakpoint.set_handler_fn(breakpoint_handler);
IDT.load();
}
}
```
이제 컴파일 오류가 발생하지는 않지만, Rust에서 `static mut`의 사용은 권장되지 않습니다. `static mut`는 데이터 레이스 (data race)를 일으키기 쉽기에, `static mut` 변수에 접근할 때마다 [`unsafe` 블록][`unsafe` block]을 반드시 사용해야 합니다.
[`unsafe` block]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers
#### 초기화 지연이 가능한 Static 변수 (Lazy Statics)
다행히 `lazy_static` 매크로를 사용하면 `static` 변수의 초기화를 컴파일 도중이 아니라 프로그램 실행 중 해당 변수가 처음 읽어지는 시점에 일어나게 할 수 있습니다. 따라서 프로그램 실행 시간에 다른 변수의 값을 읽어오는 등 거의 모든 작업을 변수 초기화 블록 안에서 제약 없이 진행할 수 있습니다.
이전에 [VGA 텍스트 버퍼에 대한 추상 인터페이스][vga text buffer lazy static]를 구현 시 의존 크레이트 목록에 `lazy_static`을 이미 추가했습니다. `lazy_static!` 매크로를 바로 사용하여 static 타입의 IDT를 생성합니다:
[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
```rust
// in src/interrupts.rs
use lazy_static::lazy_static;
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
idt
};
}
pub fn init_idt() {
IDT.load();
}
```
이 코드에서는 `unsafe` 블록이 필요하지 않습니다. `lazy_static!` 매크로의 내부 구현에서는 `unsafe`가 사용되지만, 안전한 추상 인터페이스 덕분에 `unsafe`가 외부로 드러나지 않습니다.
### 실행하기
마지막으로 `main.rs`에서 `init_idt` 함수를 호출하면 커널에서 예외 발생 및 처리가 제대로 작동합니다.
직접 `init_idt` 함수를 호출하는 대신 범용 초기화 함수 `init`을 `lib.rs`에 추가합니다:
```rust
// in src/lib.rs
pub fn init() {
interrupts::init_idt();
}
```
`main.rs`와 `lib.rs` 및 통합 테스트들의 `_start` 함수들에서 공용으로 사용하는 초기화 루틴들의 호출은 앞으로 이 `init` 함수에 한데 모아 관리할 것입니다.
`main.rs`의 `_start_` 함수가 `init` 함수를 호출한 후 breakpoint exception을 발생시키도록 코드를 추가합니다:
```rust
// in src/main.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
blog_os::init(); // 새로 추가한 코드
// invoke a breakpoint exception
x86_64::instructions::interrupts::int3(); // 새로 추가한 코드
// as before
#[cfg(test)]
test_main();
println!("It did not crash!");
loop {}
}
```
`cargo run`을 통해 QEMU에서 커널을 실행하면 아래의 출력 내용을 얻습니다:

성공입니다! CPU가 성공적으로 예외 처리 함수 `breakpoint_handler`를 호출했고, 예외 처리 함수가 메시지를 출력했으며, 그 후 `_start` 함수로 제어 흐름이 돌아와 `It did not crash!` 메시지도 출력됐습니다.
예외가 발생한 시점의 명령어 및 스택 포인터들을 인터럽트 스택 프레임이 알려줍니다. 이 정보는 예상치 못한 예외를 디버깅할 때 매우 유용합니다.
### 테스트 추가하기
위에서 확인한 동작을 위한 테스트를 작성해봅시다. 우선 `_start` 함수가 `init` 함수를 호출하도록 수정합니다:
```rust
// in src/lib.rs
/// Entry point for `cargo test`
#[cfg(test)]
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
init(); // 새로 추가한 코드
test_main();
loop {}
}
```
Rust는 `lib.rs`를 `main.rs`와는 독립적으로 테스트하기 때문에 이 `_start` 함수는 `cargo test --lib` 실행 시에만 사용된다는 것을 기억하세요. 테스트 실행 전에 `init` 함수를 먼저 호출하여 IDT를 만들고 테스트 실행 시 사용되도록 설정합니다.
이제 `test_breakpoint_exception` 테스트를 생성할 수 있습니다:
```rust
// in src/interrupts.rs
#[test_case]
fn test_breakpoint_exception() {
// invoke a breakpoint exception
x86_64::instructions::interrupts::int3();
}
```
테스트는 `int3` 함수를 호출하여 breakpoint 예외를 발생시킵니다. 예외 처리 후, 이전에 실행 중이었던 프로그램의 실행이 재개함을 확인함으로써 breakpoint handler가 제대로 작동하는지 점검합니다.
`cargo test` (모든 테스트 실행) 혹은 `cargo test --lib` (`lib.rs` 및 그 하위 모듈의 테스트만 실행) 커맨드를 통해 이 새로운 테스트를 실행해보세요. 테스트 실행 결과가 아래처럼 출력될 것입니다:
```
blog_os::interrupts::test_breakpoint_exception... [ok]
```
## 더 자세히 파헤치고 싶은 분들께 {#too-much-magic}
`x86-interrupt` 함수 호출 규약과 [`InterruptDescriptorTable`] 타입 덕분에 비교적 쉽게 예외 처리를 구현할 수 있었습니다. 예외 처리 시 우리가 이용한 추상화 단계 아래에서 일어나는 일들을 자세히 알고 싶으신가요? 그런 분들을 위해 준비했습니다: 저희 블로그의 또다른 글 시리즈 [“Handling Exceptions with Naked Functions”]는 `x86-interrupt` 함수 호출 규약 없이 예외 처리를 구현하는 과정을 다루며, IDT 타입을 직접 구현하여 사용합니다. 해당 글 시리즈는 `x86-interrupt` 함수 호출 규약 및 `x86_64` 크레이트가 생기기 이전에 작성되었습니다. 해당 시리즈는 이 블로그의 [첫 번째 버전][first edition]에 기반하여 작성되었기에 오래되어 더 이상 유효하지 않은 정보가 포함되어 있을 수 있으니 참고 부탁드립니다.
[“Handling Exceptions with Naked Functions”]: @/edition-1/extra/naked-exceptions/_index.md
[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
[first edition]: @/edition-1/_index.md
## 다음 단계는 무엇일까요?
이번 포스트에서 예외 (exception)를 발생시키고 처리한 후 예외로부터 반환하는 것까지 성공했습니다. 다음 단계는 우리의 커널이 모든 예외를 처리할 수 있게 하는 것입니다. 제대로 처리되지 않은 예외는 치명적인 [트리플 폴트 (triple fault)][triple fault]를 발생시켜 시스템이 리셋하도록 만듭니다. 다음 포스트에서는 트리플 폴트가 발생하지 않도록 [더블 폴트 (double fault)][double faults]를 처리하는 방법을 다뤄보겠습니다.
[triple fault]: https://wiki.osdev.org/Triple_Fault
[double faults]: https://wiki.osdev.org/Double_Fault#Double_Fault
================================================
FILE: blog/content/edition-2/posts/05-cpu-exceptions/index.md
================================================
+++
title = "CPU Exceptions"
weight = 5
path = "cpu-exceptions"
date = 2018-06-17
[extra]
chapter = "Interrupts"
+++
CPU exceptions occur in various erroneous situations, for example, when accessing an invalid memory address or when dividing by zero. To react to them, we have to set up an _interrupt descriptor table_ that provides handler functions. At the end of this post, our kernel will be able to catch [breakpoint exceptions] and resume normal execution afterward.
[breakpoint exceptions]: https://wiki.osdev.org/Exceptions#Breakpoint
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-05`][post branch] branch.
[GitHub]: https://github.com/phil-opp/blog_os
[at the bottom]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-05
## Overview
An exception signals that something is wrong with the current instruction. For example, the CPU issues an exception if the current instruction tries to divide by 0. When an exception occurs, the CPU interrupts its current work and immediately calls a specific exception handler function, depending on the exception type.
On x86, there are about 20 different CPU exception types. The most important are:
- **Page Fault**: A page fault occurs on illegal memory accesses. For example, if the current instruction tries to read from an unmapped page or tries to write to a read-only page.
- **Invalid Opcode**: This exception occurs when the current instruction is invalid, for example, when we try to use new [SSE instructions] on an old CPU that does not support them.
- **General Protection Fault**: This is the exception with the broadest range of causes. It occurs on various kinds of access violations, such as trying to execute a privileged instruction in user-level code or writing reserved fields in configuration registers.
- **Double Fault**: When an exception occurs, the CPU tries to call the corresponding handler function. If another exception occurs _while calling the exception handler_, the CPU raises a double fault exception. This exception also occurs when there is no handler function registered for an exception.
- **Triple Fault**: If an exception occurs while the CPU tries to call the double fault handler function, it issues a fatal _triple fault_. We can't catch or handle a triple fault. Most processors react by resetting themselves and rebooting the operating system.
[SSE instructions]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions
For the full list of exceptions, check out the [OSDev wiki][exceptions].
[exceptions]: https://wiki.osdev.org/Exceptions
### The Interrupt Descriptor Table
In order to catch and handle exceptions, we have to set up a so-called _Interrupt Descriptor Table_ (IDT). In this table, we can specify a handler function for each CPU exception. The hardware uses this table directly, so we need to follow a predefined format. Each entry must have the following 16-byte structure:
| Type | Name | Description |
| ---- | ------------------------ | ------------------------------------------------------------ |
| u16 | Function Pointer [0:15] | The lower bits of the pointer to the handler function. |
| u16 | GDT selector | Selector of a code segment in the [global descriptor table]. |
| u16 | Options | (see below) |
| u16 | Function Pointer [16:31] | The middle bits of the pointer to the handler function. |
| u32 | Function Pointer [32:63] | The remaining bits of the pointer to the handler function. |
| u32 | Reserved |
[global descriptor table]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
The options field has the following format:
| Bits | Name | Description |
| ----- | -------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| 0-2 | Interrupt Stack Table Index | 0: Don't switch stacks, 1-7: Switch to the n-th stack in the Interrupt Stack Table when this handler is called. |
| 3-7 | Reserved |
| 8 | 0: Interrupt Gate, 1: Trap Gate | If this bit is 0, interrupts are disabled when this handler is called. |
| 9-11 | must be one |
| 12 | must be zero |
| 13‑14 | Descriptor Privilege Level (DPL) | The minimal privilege level required for calling this handler. |
| 15 | Present |
Each exception has a predefined IDT index. For example, the invalid opcode exception has table index 6 and the page fault exception has table index 14. Thus, the hardware can automatically load the corresponding IDT entry for each exception. The [Exception Table][exceptions] in the OSDev wiki shows the IDT indexes of all exceptions in the “Vector nr.” column.
When an exception occurs, the CPU roughly does the following:
1. Push some registers on the stack, including the instruction pointer and the [RFLAGS] register. (We will use these values later in this post.)
2. Read the corresponding entry from the Interrupt Descriptor Table (IDT). For example, the CPU reads the 14th entry when a page fault occurs.
3. Check if the entry is present and, if not, raise a double fault.
4. Disable hardware interrupts if the entry is an interrupt gate (bit 40 not set).
5. Load the specified [GDT] selector into the CS (code segment).
6. Jump to the specified handler function.
[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register
[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table
Don't worry about steps 4 and 5 for now; we will learn about the global descriptor table and hardware interrupts in future posts.
## An IDT Type
Instead of creating our own IDT type, we will use the [`InterruptDescriptorTable` struct] of the `x86_64` crate, which looks like this:
[`InterruptDescriptorTable` struct]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html
``` rust
#[repr(C)]
pub struct InterruptDescriptorTable {
pub divide_by_zero: Entry,
pub debug: Entry,
pub non_maskable_interrupt: Entry