Rename second-edition subfolder to `edition-2
@@ -1,4 +1,5 @@
|
||||
+++
|
||||
title = "Second Edition"
|
||||
template = "redirect-to-frontpage.html"
|
||||
aliases = ["second-edition/index.html"]
|
||||
+++
|
||||
@@ -3,5 +3,5 @@ title = "Extra Content"
|
||||
insert_anchor_links = "left"
|
||||
render = false
|
||||
sort_by = "weight"
|
||||
page_template = "second-edition/extra.html"
|
||||
page_template = "edition-2/extra.html"
|
||||
+++
|
||||
|
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 287 KiB |
@@ -1,7 +1,7 @@
|
||||
+++
|
||||
title = "Building on Android"
|
||||
weight = 3
|
||||
|
||||
aliases = ["second-edition/extra/building-on-android/index.html"]
|
||||
+++
|
||||
|
||||
I finally managed to get `blog_os` building on my Android phone using [termux](https://termux.com/). This post explains the necessary steps to set it up.
|
||||
@@ -523,4 +523,4 @@ cargo rustc -- -C link-args="-e __start -static -nostartfiles"
|
||||
|
||||
[پست بعدی] مراحل مورد نیاز برای تبدیل باینری مستقل به حداقل هسته سیستمعامل را توضیح میدهد. که شامل ایجاد یک هدف سفارشی، ترکیب اجرایی ما با بوتلودر و یادگیری نحوه چاپ چیزی در صفحه است.
|
||||
|
||||
[پست بعدی]: @/second-edition/posts/02-minimal-rust-kernel/index.fa.md
|
||||
[پست بعدی]: @/edition-2/posts/02-minimal-rust-kernel/index.fa.md
|
||||
@@ -528,4 +528,4 @@ cargo rustc -- -C link-args="-e __start -static -nostartfiles"
|
||||
|
||||
[次の記事][next post]では、この独立したバイナリを最小限の OS カーネルにするために必要なステップを説明しています。カスタムターゲットの作成、実行可能ファイルとブートローダの組み合わせ、画面に何か文字を表示する方法について説明しています。
|
||||
|
||||
[next post]: @/second-edition/posts/02-minimal-rust-kernel/index.md
|
||||
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md
|
||||
@@ -516,4 +516,4 @@ Note that this is just a minimal example of a freestanding Rust binary. This bin
|
||||
|
||||
The [next post] explains the steps needed for turning our freestanding binary into a minimal operating system kernel. This includes creating a custom target, combining our executable with a bootloader, and learning how to print something to the screen.
|
||||
|
||||
[next post]: @/second-edition/posts/02-minimal-rust-kernel/index.md
|
||||
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md
|
||||
@@ -296,4 +296,4 @@ cargo build --target thumbv7em-none-eabihf
|
||||
|
||||
下一篇文章要做的事情基于我们这篇文章的成果,它将详细讲述编写一个最小的操作系统内核需要的步骤:如何配置特定的编译目标,如何将可执行程序与引导程序拼接,以及如何把一些特定的字符串打印到屏幕上。
|
||||
|
||||
[next post]: @/second-edition/posts/02-minimal-rust-kernel/index.md
|
||||
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md
|
||||
@@ -516,4 +516,4 @@ cargo rustc -- -C link-args="-e __start -static -nostartfiles"
|
||||
|
||||
[下一篇文章][next post] 將會講解如何將我們的獨立執行檔轉成最小的作業系統核心。這包含建立自訂目標、用啟動程式組合我們的執行檔,還有學習如何輸出一些東西到螢幕上。
|
||||
|
||||
[next post]: @/second-edition/posts/02-minimal-rust-kernel/index.md
|
||||
[next post]: @/edition-2/posts/02-minimal-rust-kernel/index.md
|
||||
@@ -2,7 +2,7 @@
|
||||
title = "Disable the Red Zone"
|
||||
weight = 1
|
||||
path = "red-zone"
|
||||
template = "second-edition/extra.html"
|
||||
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 its stack frame without adjusting the stack pointer:
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -2,7 +2,7 @@
|
||||
title = "Disable SIMD"
|
||||
weight = 2
|
||||
path = "disable-simd"
|
||||
template = "second-edition/extra.html"
|
||||
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:
|
||||
@@ -15,7 +15,7 @@ rtl = true
|
||||
|
||||
در این پست ما برای معماری x86 یک هسته مینیمال ۶۴ بیتی به زبان راست میسازیم. با استفاده از باینری مستقل Rust از پست قبل، یک دیسک ایمیج قابل بوت میسازیم، که متنی را در صفحه چاپ کند.
|
||||
|
||||
[باینری مستقل Rust]: @/second-edition/posts/01-freestanding-rust-binary/index.md
|
||||
[باینری مستقل Rust]: @/edition-2/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
<!-- more -->
|
||||
|
||||
@@ -181,7 +181,7 @@ rtl = true
|
||||
|
||||
ما در حال نوشتن یک هسته هستیم، بنابراین بالاخره باید وقفهها را مدیریت کنیم. برای انجام ایمن آن، باید بهینهسازی اشارهگر پشتهای خاصی به نام _“red zone”_ (ترجمه: منطقه قرمز) را غیرفعال کنیم، زیرا در غیر این صورت باعث خراب شدن پشته میشود. برای اطلاعات بیشتر، به پست جداگانه ما در مورد [غیرفعال کردن منطقه قرمز] مراجعه کنید.
|
||||
|
||||
[غیرفعال کردن منطقه قرمز]: @/second-edition/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
[غیرفعال کردن منطقه قرمز]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
|
||||
```json
|
||||
"features": "-mmx,-sse,+soft-float",
|
||||
@@ -195,7 +195,7 @@ rtl = true
|
||||
|
||||
یک مشکل در غیرفعال کردن SIMD این است که عملیاتهای مُمَیزِ شناور (ترجمه: floating point) در `x86_64` به طور پیش فرض به ثباتهای SIMD نیاز دارد. برای حل این مشکل، ویژگی `soft-float` را اضافه میکنیم، که از طریق عملکردهای نرمافزاری مبتنی بر اعداد صحیح عادی، تمام عملیات مُمَیزِ شناور را شبیهسازی میکند.
|
||||
|
||||
For more information, see our post on [disabling SIMD](@/second-edition/posts/02-minimal-rust-kernel/disable-simd/index.md).
|
||||
For more information, see our post on [disabling SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
|
||||
|
||||
#### کنار هم قرار دادن
|
||||
فایل مشخصات هدف ما اکنون به این شکل است:
|
||||
@@ -222,7 +222,7 @@ For more information, see our post on [disabling SIMD](@/second-edition/posts/02
|
||||
|
||||
عملیات کامپایل کردن برای هدف جدید ما از قراردادهای لینوکس استفاده خواهد کرد (کاملاً مطمئن نیستم که چرا، تصور میکنم این فقط پیش فرض LLVM باشد). این بدان معنی است که ما به یک نقطه ورود به نام `start_` نیاز داریم همانطور که در [پست قبلی] توضیح داده شد:
|
||||
|
||||
[پست قبلی]: @/second-edition/posts/01-freestanding-rust-binary/index.md
|
||||
[پست قبلی]: @/edition-2/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
@@ -14,7 +14,7 @@ translators = ["woodyZootopia", "JohnTitor"]
|
||||
|
||||
この記事では、Rustで最小限の64bitカーネルを作ります。前の記事で作った[フリースタンディングなRustバイナリ][freestanding Rust binary]を下敷きにして、何かを画面に出力する、ブータブルディスクイメージを作ります。
|
||||
|
||||
[freestanding Rust binary]: @/second-edition/posts/01-freestanding-rust-binary/index.ja.md
|
||||
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.ja.md
|
||||
|
||||
<!-- more -->
|
||||
|
||||
@@ -175,7 +175,7 @@ Cargoは`--target`パラメータを使ってさまざまなターゲットを
|
||||
|
||||
カーネルを書いている以上、ある時点で<ruby>割り込み<rp> (</rp><rt>interrupt</rt><rp>) </rp></ruby>を処理しなければならなくなるでしょう。これを安全に行うために、 **"red zone"** と呼ばれる、ある種のスタックポインタ最適化を無効化する必要があります。こうしないと、スタックの<ruby>破損<rp> (</rp><rt>corruption</rt><rp>) </rp></ruby>を引き起こしてしまう恐れがあるためです。より詳しくは、[red zoneの無効化][disabling the red zone]という別記事をご覧ください。
|
||||
|
||||
[disabling the red zone]: @/second-edition/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
|
||||
```json
|
||||
"features": "-mmx,-sse,+soft-float",
|
||||
@@ -189,7 +189,7 @@ Cargoは`--target`パラメータを使ってさまざまなターゲットを
|
||||
|
||||
SIMDを無効化することによる問題に、`x86_64`における浮動小数点演算は標準ではSIMDレジスタを必要とするということがあります。この問題を解決するため、`soft-float`機能を追加します。これは、すべての浮動小数点演算を通常の整数に基づいたソフトウェア上の関数を使ってエミュレートするというものです。
|
||||
|
||||
より詳しくは、[SIMDを無効化する](@/second-edition/posts/02-minimal-rust-kernel/disable-simd/index.md)ことに関する私達の記事を読んでください。
|
||||
より詳しくは、[SIMDを無効化する](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md)ことに関する私達の記事を読んでください。
|
||||
|
||||
#### まとめると
|
||||
私達のターゲット仕様ファイルは今このようになっているはずです。
|
||||
@@ -215,7 +215,7 @@ SIMDを無効化することによる問題に、`x86_64`における浮動小
|
||||
### カーネルをビルドする
|
||||
私達の新しいターゲットのコンパイルにはLinuxの慣習に倣います(理由は知りません、LLVMのデフォルトであるというだけではないでしょうか)。つまり、[前の記事][previous post]で説明したように`_start`という名前のエントリポイントが要るということです。
|
||||
|
||||
[previous post]: @/second-edition/posts/01-freestanding-rust-binary/index.ja.md
|
||||
[previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.ja.md
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
@@ -10,7 +10,7 @@ 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]: @/second-edition/posts/01-freestanding-rust-binary/index.md
|
||||
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
<!-- more -->
|
||||
|
||||
@@ -172,7 +172,7 @@ This setting specifies that the target doesn't support [stack unwinding] on pani
|
||||
|
||||
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 corruptions otherwise. For more information, see our separate post about [disabling the red zone].
|
||||
|
||||
[disabling the red zone]: @/second-edition/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
[disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
|
||||
|
||||
```json
|
||||
"features": "-mmx,-sse,+soft-float",
|
||||
@@ -186,7 +186,7 @@ The `mmx` and `sse` features determine support for [Single Instruction Multiple
|
||||
|
||||
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](@/second-edition/posts/02-minimal-rust-kernel/disable-simd/index.md).
|
||||
For more information, see our post on [disabling SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md).
|
||||
|
||||
#### Putting it Together
|
||||
Our target specification file now looks like this:
|
||||
@@ -212,7 +212,7 @@ Our target specification file now looks like this:
|
||||
### Building our Kernel
|
||||
Compiling for our new target will use Linux conventions (I'm not quite sure why, I assume that it's just LLVM's default). This means that we need an entry point named `_start` as described in the [previous post]:
|
||||
|
||||
[previous post]: @/second-edition/posts/01-freestanding-rust-binary/index.md
|
||||
[previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
```rust
|
||||
// src/main.rs
|
||||
@@ -13,7 +13,7 @@ translators = ["luojia65", "Rustin-Liu"]
|
||||
|
||||
在这篇文章中,我们将基于 **x86架构**(the x86 architecture),使用 Rust 语言,编写一个最小化的 64 位内核。我们将从上一章中构建的独立式可执行程序开始,构建自己的内核;它将向显示器打印字符串,并能被打包为一个能够引导启动的**磁盘映像**(disk image)。
|
||||
|
||||
[freestanding Rust binary]: @/second-edition/posts/01-freestanding-rust-binary/index.md
|
||||
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.md
|
||||
|
||||
<!-- more -->
|
||||
|
||||
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
@@ -29,11 +29,11 @@ rtl = true
|
||||
|
||||
این پست جایگزین (حالا منسوخ شده) پستهای [_Unit Testing_] و [_Integration Tests_] میشود. فرض بر این است که شما پست [_یک کرنل مینیمال با Rust_] را پس از 27-09-2019 دنبال کردهاید. اساساً نیاز است که شما یک فایل `.cargo/config.toml` داشته باشید که [یک هدف پیشفرض مشخص میکند] و [یک اجرا کننده قابل اجرا تعریف میکند].
|
||||
|
||||
[_Unit Testing_]: @/second-edition/posts/deprecated/04-unit-testing/index.md
|
||||
[_Integration Tests_]: @/second-edition/posts/deprecated/05-integration-tests/index.md
|
||||
[_یک کرنل مینیمال با Rust_]: @/second-edition/posts/02-minimal-rust-kernel/index.md
|
||||
[یک هدف پیشفرض مشخص میکند]: @/second-edition/posts/02-minimal-rust-kernel/index.md#set-a-default-target
|
||||
[یک اجرا کننده قابل اجرا تعریف میکند]: @/second-edition/posts/02-minimal-rust-kernel/index.md#using-cargo-run
|
||||
[_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
|
||||
|
||||
@@ -163,7 +163,7 @@ test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
|
||||
|
||||
برای برقراری ارتباط بین پردازنده و سخت افزار جانبی در x86، دو رویکرد مختلف وجود دارد،**memory-mapped I/O** و **port-mapped I/O**. ما قبلاً برای دسترسی به [بافر متن VGA] از طریق آدرس حافظه `0xb8000` از memory-mapped I/O استفاده کردهایم. این آدرس به RAM مپ (ترسیم) نشده است، بلکه به برخی از حافظههای دستگاه VGA مپ شده است.
|
||||
|
||||
[بافر متن VGA]: @/second-edition/posts/03-vga-text-buffer/index.md
|
||||
[بافر متن VGA]: @/edition-2/posts/03-vga-text-buffer/index.md
|
||||
|
||||
در مقابل، port-mapped I/O از یک گذرگاه I/O جداگانه برای ارتباط استفاده میکند. هر قسمت جانبی متصل دارای یک یا چند شماره پورت است. برای برقراری ارتباط با چنین پورت I/O، دستورالعملهای CPU خاصی وجود دارد که `in` و `out` نامیده میشوند، که یک عدد پورت و یک بایت داده را میگیرند (همچنین این دستورات تغییراتی دارند که اجازه می دهد یک `u16` یا `u32` ارسال کنید).
|
||||
|
||||
@@ -315,7 +315,7 @@ lazy_static! {
|
||||
|
||||
مانند دستگاه `isa-debug-exit`، UART با استفاده از پورت I/O برنامه نویسی میشود. از آنجا که UART پیچیدهتر است، از چندین پورت I/O برای برنامه نویسی رجیسترهای مختلف دستگاه استفاده میکند. تابع ناامن `SerialPort::new` انتظار دارد که آدرس اولین پورت I/O از UART به عنوان آرگومان باشد، که از آن میتواند آدرس تمام پورتهای مورد نیاز را محاسبه کند. ما در حال عبور دادنِ آدرس پورت `0x3F8` هستیم که شماره پورت استاندارد برای اولین رابط سریال است.
|
||||
|
||||
[vga lazy-static]: @/second-edition/posts/03-vga-text-buffer/index.md#lazy-statics
|
||||
[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
|
||||
|
||||
برای اینکه پورت سریال به راحتی قابل استفاده باشد، ماکروهای `serial_print!` و `serial_println!` را اضافه میکنیم:
|
||||
|
||||
@@ -24,11 +24,11 @@ This blog is openly developed on [GitHub]. If you have any problems or questions
|
||||
|
||||
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_]: @/second-edition/posts/deprecated/04-unit-testing/index.md
|
||||
[_Integration Tests_]: @/second-edition/posts/deprecated/05-integration-tests/index.md
|
||||
[_A Minimal Rust Kernel_]: @/second-edition/posts/02-minimal-rust-kernel/index.md
|
||||
[sets a default target]: @/second-edition/posts/02-minimal-rust-kernel/index.md#set-a-default-target
|
||||
[defines a runner executable]: @/second-edition/posts/02-minimal-rust-kernel/index.md#using-cargo-run
|
||||
[_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
|
||||
|
||||
@@ -158,7 +158,7 @@ Together with the device name (`isa-debug-exit`), we pass the two parameters `io
|
||||
|
||||
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]: @/second-edition/posts/03-vga-text-buffer/index.md
|
||||
[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 an `u16` or `u32`).
|
||||
|
||||
@@ -310,7 +310,7 @@ Like with the [VGA text buffer][vga lazy-static], we use `lazy_static` and a spi
|
||||
|
||||
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 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]: @/second-edition/posts/03-vga-text-buffer/index.md#lazy-statics
|
||||
[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:
|
||||
|
||||
@@ -27,11 +27,11 @@ translators = ["luojia65", "Rustin-Liu"]
|
||||
|
||||
这篇文章替换了此前的(现在已经过时了) [_单元测试(Unit Testing)_] 和 [_集成测试(Integration Tests)_] 两篇文章。这里我将假定你是在2019-04-27日后阅读的[_最小Rust内核_]一文。总而言之,本文要求你已经有一个[设置默认目标]的 `.cargo/config` 文件且[定义了一个runner可执行文件]。
|
||||
|
||||
[_单元测试(Unit Testing)_]: @/second-edition/posts/deprecated/04-unit-testing/index.md
|
||||
[_集成测试(Integration Tests)_]: @/second-edition/posts/deprecated/05-integration-tests/index.md
|
||||
[_最小Rust内核_]: @/second-edition/posts/02-minimal-rust-kernel/index.md
|
||||
[设置默认目标]: @/second-edition/posts/02-minimal-rust-kernel/index.md#set-a-default-target
|
||||
[定义了一个runner可执行文件]: @/second-edition/posts/02-minimal-rust-kernel/index.md#using-cargo-run
|
||||
[_单元测试(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
|
||||
[定义了一个runner可执行文件]: @/edition-2/posts/02-minimal-rust-kernel/index.md#using-cargo-run
|
||||
|
||||
## Rust中的测试
|
||||
|
||||
@@ -156,7 +156,7 @@ test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"]
|
||||
### I/O 端口
|
||||
在x86平台上,CPU和外围硬件通信通常有两种方式,**内存映射I/O**和**端口映射I/O**。之前,我们已经使用内存映射的方式,通过内存地址`0xb8000`访问了[VGA文本缓冲区]。该地址并没有映射到RAM,而是映射到了VGA设备的一部分内存上。
|
||||
|
||||
[VGA text buffer]: @/second-edition/posts/03-vga-text-buffer/index.md
|
||||
[VGA text buffer]: @/edition-2/posts/03-vga-text-buffer/index.md
|
||||
|
||||
与内存映射不同,端口映射I/O使用独立的I/O总线来进行通信。每个外围设备都有一个或数个端口号。CPU采用了特殊的`in`和`out`指令来和端口通信,这些指令要求一个端口号和一个字节的数据作为参数(有些这种指令的变体也允许发送`u16`或是`u32`长度的数据)。
|
||||
|
||||
@@ -309,7 +309,7 @@ lazy_static! {
|
||||
|
||||
和 `isa-debug-exit`设备一样,UART也是用过I/O端口进行编程的。由于UART相对来讲更加复杂,它使用多个I/O端口来对不同的设备寄存器进行编程。不安全的`SerialPort::new`函数需要UART的第一个I/O端口的地址作为参数,从该地址中可以计算出所有所需端口的地址。我们传递的端口地址为`0x3F8` ,该地址是第一个串行接口的标准端口号。
|
||||
|
||||
[vga lazy-static]: @/second-edition/posts/03-vga-text-buffer/index.md#lazy-statics
|
||||
[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
|
||||
|
||||
为了使串口更加易用,我们添加了 `serial_print!` 和 `serial_println!`宏:
|
||||
|
||||
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
@@ -347,7 +347,7 @@ Fortunately the `lazy_static` macro exists. Instead of evaluating a `static` at
|
||||
|
||||
We already imported the `lazy_static` crate when we [created an abstraction for the VGA text buffer][vga text buffer lazy static]. So we can directly use the `lazy_static!` macro to create our static IDT:
|
||||
|
||||
[vga text buffer lazy static]: @/second-edition/posts/03-vga-text-buffer/index.md#lazy-statics
|
||||
[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -23,7 +23,7 @@ This blog is openly developed on [GitHub]. If you have any problems or questions
|
||||
## What is a Double Fault?
|
||||
In simplified terms, a double fault is a special exception that occurs when the CPU fails to invoke an exception handler. For example, it occurs when a page fault is triggered but there is no page fault handler registered in the [Interrupt Descriptor Table][IDT] (IDT). So it's kind of similar to catch-all blocks in programming languages with exceptions, e.g. `catch(...)` in C++ or `catch(Exception e)` in Java or C#.
|
||||
|
||||
[IDT]: @/second-edition/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table
|
||||
[IDT]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table
|
||||
|
||||
A double fault behaves like a normal exception. It has the vector number `8` and we can define a normal handler function for it in the IDT. It is really important to provide a double fault handler, because if a double fault is unhandled a fatal _triple fault_ occurs. Triple faults can't be caught and most hardware reacts with a system reset.
|
||||
|
||||
@@ -157,7 +157,7 @@ A guard page is a special memory page at the bottom of a stack that makes it pos
|
||||
|
||||
When a page fault occurs the CPU looks up the page fault handler in the IDT and tries to push the [interrupt stack frame] onto the stack. However, the current stack pointer still points to the non-present guard page. Thus, a second page fault occurs, which causes a double fault (according to the above table).
|
||||
|
||||
[interrupt stack frame]: @/second-edition/posts/05-cpu-exceptions/index.md#the-interrupt-stack-frame
|
||||
[interrupt stack frame]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-stack-frame
|
||||
|
||||
So the CPU tries to call the _double fault handler_ now. However, on a double fault the CPU tries to push the exception stack frame, too. The stack pointer still points to the guard page, so a _third_ page fault occurs, which causes a _triple fault_ and a system reboot. So our current double fault handler can't avoid a triple fault in this case.
|
||||
|
||||
@@ -200,7 +200,7 @@ struct InterruptStackTable {
|
||||
|
||||
For each exception handler, we can choose a stack from the IST through the `stack_pointers` field in the corresponding [IDT entry]. For example, we could use the first stack in the IST for our double fault handler. Then the CPU would automatically switch to this stack whenever a double fault occurs. This switch would happen before anything is pushed, so it would prevent the triple fault.
|
||||
|
||||
[IDT entry]: @/second-edition/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table
|
||||
[IDT entry]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table
|
||||
|
||||
### The IST and TSS
|
||||
The Interrupt Stack Table (IST) is part of an old legacy structure called _[Task State Segment]_ \(TSS). The TSS used to hold various information (e.g. processor register state) about a task in 32-bit mode and was for example used for [hardware context switching]. However, hardware context switching is no longer supported in 64-bit mode and the format of the TSS changed completely.
|
||||
@@ -442,7 +442,7 @@ name = "stack_overflow"
|
||||
harness = false
|
||||
```
|
||||
|
||||
[without a test harness]: @/second-edition/posts/04-testing/index.md#no-harness-tests
|
||||
[without a test harness]: @/edition-2/posts/04-testing/index.md#no-harness-tests
|
||||
|
||||
Now `cargo test --test stack_overflow` should compile successfully. The test fails of course, since the `unimplemented` macro panics.
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -68,7 +68,7 @@ This graphic shows the typical assignment of interrupt lines. We see that most o
|
||||
|
||||
Each controller can be configured through two [I/O ports], one “command” port and one “data” port. For the primary controller these ports are `0x20` (command) and `0x21` (data). For the secondary controller they are `0xa0` (command) and `0xa1` (data). For more information on how the PICs can be configured see the [article on osdev.org].
|
||||
|
||||
[I/O ports]: @/second-edition/posts/04-testing/index.md#i-o-ports
|
||||
[I/O ports]: @/edition-2/posts/04-testing/index.md#i-o-ports
|
||||
[article on osdev.org]: https://wiki.osdev.org/8259_PIC
|
||||
|
||||
### Implementation
|
||||
@@ -257,7 +257,7 @@ We now have a form of concurrency in our kernel: The timer interrupts occur asyn
|
||||
|
||||
We can already provoke a deadlock in our kernel. Remember, our `println` macro calls the `vga_buffer::_print` function, which [locks a global `WRITER`][vga spinlock] using a spinlock:
|
||||
|
||||
[vga spinlock]: @/second-edition/posts/03-vga-text-buffer/index.md#spinlocks
|
||||
[vga spinlock]: @/edition-2/posts/03-vga-text-buffer/index.md#spinlocks
|
||||
|
||||
```rust
|
||||
// in src/vga_buffer.rs
|
||||
@@ -557,7 +557,7 @@ We now see that a `k` appears on the screen when we press a key. However, this o
|
||||
|
||||
To find out _which_ key was pressed, we need to query the keyboard controller. We do this by reading from the data port of the PS/2 controller, which is the [I/O port] with number `0x60`:
|
||||
|
||||
[I/O port]: @/second-edition/posts/04-testing/index.md#i-o-ports
|
||||
[I/O port]: @/edition-2/posts/04-testing/index.md#i-o-ports
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
@@ -253,7 +253,7 @@ It is important to remember flushing the TLB on each page table modification bec
|
||||
|
||||
One thing that we did not mention yet: **Our kernel already runs on paging**. The bootloader that we added in the ["A minimal Rust Kernel"] post already set up a 4-level paging hierarchy that maps every page of our kernel to a physical frame. The bootloader does this because paging is mandatory in 64-bit mode on x86_64.
|
||||
|
||||
["A minimal Rust kernel"]: @/second-edition/posts/02-minimal-rust-kernel/index.md#creating-a-bootimage
|
||||
["A minimal Rust kernel"]: @/edition-2/posts/02-minimal-rust-kernel/index.md#creating-a-bootimage
|
||||
|
||||
This means that every memory address that we used in our kernel was a virtual address. Accessing the VGA buffer at address `0xb8000` only worked because the bootloader _identity mapped_ that memory page, which means that it mapped the virtual page `0xb8000` to the physical frame `0xb8000`.
|
||||
|
||||
@@ -263,7 +263,7 @@ Paging makes our kernel already relatively safe, since every memory access that
|
||||
|
||||
Let's try to cause a page fault by accessing some memory outside of our kernel. First, we create a page fault handler and register it in our IDT, so that we see a page fault exception instead of a generic [double fault] :
|
||||
|
||||
[double fault]: @/second-edition/posts/06-double-faults/index.md
|
||||
[double fault]: @/edition-2/posts/06-double-faults/index.md
|
||||
|
||||
```rust
|
||||
// in src/interrupts.rs
|
||||
@@ -303,7 +303,7 @@ The [`CR2`] register is automatically set by the CPU on a page fault and contain
|
||||
[`Cr2::read`]: https://docs.rs/x86_64/0.12.1/x86_64/registers/control/struct.Cr2.html#method.read
|
||||
[`PageFaultErrorCode`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/idt/struct.PageFaultErrorCode.html
|
||||
[LLVM bug]: https://github.com/rust-lang/rust/issues/57270
|
||||
[`hlt_loop`]: @/second-edition/posts/07-hardware-interrupts/index.md#the-hlt-instruction
|
||||
[`hlt_loop`]: @/edition-2/posts/07-hardware-interrupts/index.md#the-hlt-instruction
|
||||
|
||||
Now we can try to access some memory outside our kernel:
|
||||
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
@@ -24,11 +24,11 @@ This blog is openly developed on [GitHub]. If you have any problems or questions
|
||||
|
||||
The [previous post] gave an introduction to the concept of paging. It motivated paging by comparing it with segmentation, explained how paging and page tables work, and then introduced the 4-level page table design of `x86_64`. We found out that the bootloader already set up a page table hierarchy for our kernel, which means that our kernel already runs on virtual addresses. This improves safety since illegal memory accesses cause page fault exceptions instead of modifying arbitrary physical memory.
|
||||
|
||||
[previous post]: @/second-edition/posts/08-paging-introduction/index.md
|
||||
[previous post]: @/edition-2/posts/08-paging-introduction/index.md
|
||||
|
||||
The post ended with the problem that we [can't access the page tables from our kernel][end of previous post] because they are stored in physical memory and our kernel already runs on virtual addresses. This post continues at this point and explores different approaches of making the page table frames accessible to our kernel. We will discuss the advantages and drawbacks of each approach and then decide for an approach for our kernel.
|
||||
|
||||
[end of previous post]: @/second-edition/posts/08-paging-introduction/index.md#accessing-the-page-tables
|
||||
[end of previous post]: @/edition-2/posts/08-paging-introduction/index.md#accessing-the-page-tables
|
||||
|
||||
To implement the approach, we will need support from the bootloader, so we'll configure it first. Afterward, we will implement a function that traverses the page table hierarchy in order to translate virtual to physical addresses. Finally, we learn how to create new mappings in the page tables and how to find unused memory frames for creating new page tables.
|
||||
|
||||
@@ -55,7 +55,7 @@ In this example, we see various identity-mapped page table frames. This way the
|
||||
However, it clutters the virtual address space and makes it more difficult to find continuous memory regions of larger sizes. For example, imagine that we want to create a virtual memory region of size 1000 KiB in the above graphic, e.g. for [memory-mapping a file]. We can't start the region at `28 KiB` because it would collide with the already mapped page at `1004 KiB`. So we have to look further until we find a large enough unmapped area, for example at `1008 KiB`. This is a similar fragmentation problem as with [segmentation].
|
||||
|
||||
[memory-mapping a file]: https://en.wikipedia.org/wiki/Memory-mapped_file
|
||||
[segmentation]: @/second-edition/posts/08-paging-introduction/index.md#fragmentation
|
||||
[segmentation]: @/edition-2/posts/08-paging-introduction/index.md#fragmentation
|
||||
|
||||
Equally, it makes it much more difficult to create new page tables, because we need to find physical frames whose corresponding pages aren't already in use. For example, let's assume that we reserved the _virtual_ 1000 KiB memory region starting at `1008 KiB` for our memory-mapped file. Now we can't use any frame with a _physical_ address between `1000 KiB` and `2008 KiB` anymore, because we can't identity map it.
|
||||
|
||||
@@ -182,7 +182,7 @@ Whereas `AAA` is the level 4 index, `BBB` the level 3 index, `CCC` the level 2 i
|
||||
|
||||
`SSSSSS` are sign extension bits, which means that they are all copies of bit 47. This is a special requirement for valid addresses on the x86_64 architecture. We explained it in the [previous post][sign extension].
|
||||
|
||||
[sign extension]: @/second-edition/posts/08-paging-introduction/index.md#paging-on-x86-64
|
||||
[sign extension]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64
|
||||
|
||||
We use [octal] numbers for representing the addresses since each octal character represents three bits, which allows us to clearly separate the 9-bit indexes of the different page table levels. This isn't possible with the hexadecimal system where each character represents four bits.
|
||||
|
||||
@@ -370,7 +370,7 @@ For the module we create an empty `src/memory.rs` file.
|
||||
|
||||
At the [end of the previous post], we tried to take a look at the page tables our kernel runs on, but failed since we couldn't access the physical frame that the `CR3` register points to. We're now able to continue from there by creating an `active_level_4_table` function that returns a reference to the active level 4 page table:
|
||||
|
||||
[end of the previous post]: @/second-edition/posts/08-paging-introduction/index.md#accessing-the-page-tables
|
||||
[end of the previous post]: @/edition-2/posts/08-paging-introduction/index.md#accessing-the-page-tables
|
||||
|
||||
```rust
|
||||
// in src/memory.rs
|
||||
@@ -593,7 +593,7 @@ When we run it, we see the following output:
|
||||
|
||||
As expected, the identity-mapped address `0xb8000` translates to the same physical address. The code page and the stack page translate to some arbitrary physical addresses, which depend on how the bootloader created the initial mapping for our kernel. It's worth noting that the last 12 bits always stay the same after translation, which makes sense because these bits are the [_page offset_] and not part of the translation.
|
||||
|
||||
[_page offset_]: @/second-edition/posts/08-paging-introduction/index.md#paging-on-x86-64
|
||||
[_page offset_]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64
|
||||
|
||||
Since each physical address can be accessed by adding the `physical_memory_offset`, the translation of the `physical_memory_offset` address itself should point to physical address `0`. However, the translation fails because the mapping uses huge pages for efficiency, which is not supported in our implementation yet.
|
||||
|
||||
@@ -741,7 +741,7 @@ The [`map_to`] method is unsafe because the caller must ensure that the frame is
|
||||
|
||||
In addition to the `page` and the `unused_frame`, the `map_to` method takes a set of flags for the mapping and a reference to the `frame_allocator`, which will be explained in a moment. For the flags, we set the `PRESENT` flag because it is required for all valid entries and the `WRITABLE` flag to make the mapped page writable. For a list of all possible flags, see the [_Page Table Format_] section of the previous post.
|
||||
|
||||
[_Page Table Format_]: @/second-edition/posts/08-paging-introduction/index.md#page-table-format
|
||||
[_Page Table Format_]: @/edition-2/posts/08-paging-introduction/index.md#page-table-format
|
||||
|
||||
The [`map_to`] function can fail, so it returns a [`Result`]. Since this is just some example code that does not need to be robust, we just use [`expect`] to panic when an error occurs. On success, the function returns a [`MapperFlush`] type that provides an easy way to flush the newly mapped page from the translation lookaside buffer (TLB) with its [`flush`] method. Like `Result`, the type uses the [`#[must_use]`][must_use] attribute to emit a warning when we accidentally forget to use it.
|
||||
|
||||
@@ -784,7 +784,7 @@ Additionally, the graphic shows the physical frame of the VGA text buffer in red
|
||||
|
||||
The graphic shows two canditate pages in the virtual address space, both marked in yellow. One page is at address `0x803fdfd000`, which is 3 pages before the mapped page (in blue). While the level 4 and level 3 page table indices are the same as for the blue page, the level 2 and level 1 indices are different (see the [previous post][page-table-indices]). The different index into the level 2 table means that a different level 1 table is used for this page. Since this level 1 table does not exist yet, we would need to create it if we chose that page for our example mapping, which would require an additional unused physical frame. In contrast, the second candidate page at address `0x803fe02000` does not have this problem because it uses the same level 1 page table than the blue page. Thus, all required page tables already exist.
|
||||
|
||||
[page-table-indices]: @/second-edition/posts/08-paging-introduction/index.md#paging-on-x86-64
|
||||
[page-table-indices]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64
|
||||
|
||||
In summary, the difficulty of creating a new mapping depends on the virtual page that we want to map. In the easiest case, the level 1 page table for the page already exists and we just need to write a single entry. In the most difficult case, the page is in a memory region for that no level 3 exists yet so that we need to create new level 3, level 2 and level 1 page tables first.
|
||||
|
||||
@@ -823,7 +823,7 @@ We first create the mapping for the page at address `0` by calling our `create_e
|
||||
|
||||
Then we convert the page to a raw pointer and write a value to offset `400`. We don't write to the start of the page because the top line of the VGA buffer is directly shifted off the screen by the next `println`. We write the value `0x_f021_f077_f065_f04e`, which represents the string _"New!"_ on white background. As we learned [in the _“VGA Text Mode”_ post], writes to the VGA buffer should be volatile, so we use the [`write_volatile`] method.
|
||||
|
||||
[in the _“VGA Text Mode”_ post]: @/second-edition/posts/03-vga-text-buffer/index.md#volatile
|
||||
[in the _“VGA Text Mode”_ post]: @/edition-2/posts/03-vga-text-buffer/index.md#volatile
|
||||
[`write_volatile`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write_volatile
|
||||
|
||||
When we run it in QEMU, we see the following output:
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -52,7 +52,7 @@ fn inner(i: usize) -> &'static u32 {
|
||||
|
||||
While returning a reference makes no sense in this example, there are cases where we want a variable to live longer than the function. We already saw such a case in our kernel when we tried to [load an interrupt descriptor table] and had to use a `static` variable to extend the lifetime.
|
||||
|
||||
[load an interrupt descriptor table]: @/second-edition/posts/05-cpu-exceptions/index.md#loading-the-idt
|
||||
[load an interrupt descriptor table]: @/edition-2/posts/05-cpu-exceptions/index.md#loading-the-idt
|
||||
|
||||
### Static Variables
|
||||
|
||||
@@ -64,14 +64,14 @@ When the `inner` function returns in the above example, it's part of the call st
|
||||
|
||||
Apart from the `'static` lifetime, static variables also have the useful property that their location is known at compile time, so that no reference is needed for accessing it. We utilized that property for our `println` macro: By using a [static `Writer`] internally there is no `&mut Writer` reference needed to invoke the macro, which is very useful in [exception handlers] where we don't have access to any additional variables.
|
||||
|
||||
[static `Writer`]: @/second-edition/posts/03-vga-text-buffer/index.md#a-global-interface
|
||||
[exception handlers]: @/second-edition/posts/05-cpu-exceptions/index.md#implementation
|
||||
[static `Writer`]: @/edition-2/posts/03-vga-text-buffer/index.md#a-global-interface
|
||||
[exception handlers]: @/edition-2/posts/05-cpu-exceptions/index.md#implementation
|
||||
|
||||
However, this property of static variables brings a crucial drawback: They are read-only by default. Rust enforces this because a [data race] would occur if e.g. two threads modify a static variable at the same time. The only way to modify a static variable is to encapsulate it in a [`Mutex`] type, which ensures that only a single `&mut` reference exists at any point in time. We already used a `Mutex` for our [static VGA buffer `Writer`][vga mutex].
|
||||
|
||||
[data race]: https://doc.rust-lang.org/nomicon/races.html
|
||||
[`Mutex`]: https://docs.rs/spin/0.5.2/spin/struct.Mutex.html
|
||||
[vga mutex]: @/second-edition/posts/03-vga-text-buffer/index.md#spinlocks
|
||||
[vga mutex]: @/edition-2/posts/03-vga-text-buffer/index.md#spinlocks
|
||||
|
||||
## Dynamic Memory
|
||||
|
||||
@@ -389,7 +389,7 @@ The error handler is called because the `Box::new` function implicitly calls the
|
||||
|
||||
Before we can create a proper allocator, we first need to create a heap memory region from which the allocator can allocate memory. To do this, we need to define a virtual memory range for the heap region and then map this region to physical frames. See the [_"Introduction To Paging"_] post for an overview of virtual memory and page tables.
|
||||
|
||||
[_"Introduction To Paging"_]: @/second-edition/posts/08-paging-introduction/index.md
|
||||
[_"Introduction To Paging"_]: @/edition-2/posts/08-paging-introduction/index.md
|
||||
|
||||
The first step is to define a virtual memory region for the heap. We can choose any virtual address range that we like, as long as it is not already used for a different memory region. Let's define it as the memory starting at address `0x_4444_4444_0000` so that we can easily recognize a heap pointer later:
|
||||
|
||||
@@ -404,8 +404,8 @@ We set the heap size to 100 KiB for now. If we need more space in the future, we
|
||||
|
||||
If we tried to use this heap region now, a page fault would occur since the virtual memory region is not mapped to physical memory yet. To resolve this, we create an `init_heap` function that maps the heap pages using the [`Mapper` API] that we introduced in the [_"Paging Implementation"_] post:
|
||||
|
||||
[`Mapper` API]: @/second-edition/posts/09-paging-implementation/index.md#using-offsetpagetable
|
||||
[_"Paging Implementation"_]: @/second-edition/posts/09-paging-implementation/index.md
|
||||
[`Mapper` API]: @/edition-2/posts/09-paging-implementation/index.md#using-offsetpagetable
|
||||
[_"Paging Implementation"_]: @/edition-2/posts/09-paging-implementation/index.md
|
||||
|
||||
```rust
|
||||
// in src/allocator.rs
|
||||
@@ -474,7 +474,7 @@ The implementation can be broken down into two parts:
|
||||
[`Option::ok_or`]: https://doc.rust-lang.org/core/option/enum.Option.html#method.ok_or
|
||||
[question mark operator]: https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/the-question-mark-operator-for-easier-error-handling.html
|
||||
[`MapperFlush`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.MapperFlush.html
|
||||
[_translation lookaside buffer_]: @/second-edition/posts/08-paging-introduction/index.md#the-translation-lookaside-buffer
|
||||
[_translation lookaside buffer_]: @/edition-2/posts/08-paging-introduction/index.md#the-translation-lookaside-buffer
|
||||
[`flush`]: https://docs.rs/x86_64/0.12.1/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush
|
||||
|
||||
The final step is to call this function from our `kernel_main`:
|
||||
@@ -683,7 +683,7 @@ fn panic(info: &PanicInfo) -> ! {
|
||||
|
||||
We reuse the `test_runner` and `test_panic_handler` functions from our `lib.rs`. Since we want to test allocations, we enable the `alloc` crate through the `extern crate alloc` statement. For more information about the test boilerplate check out the [_Testing_] post.
|
||||
|
||||
[_Testing_]: @/second-edition/posts/04-testing/index.md
|
||||
[_Testing_]: @/edition-2/posts/04-testing/index.md
|
||||
|
||||
The implementation of the `main` function looks like this:
|
||||
|
||||
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -24,9 +24,9 @@ This blog is openly developed on [GitHub]. If you have any problems or questions
|
||||
|
||||
In the [previous post] we added basic support for heap allocations to our kernel. For that, we [created a new memory region][map-heap] in the page tables and [used the `linked_list_allocator` crate][use-alloc-crate] to manage that memory. While we have a working heap now, we left most of the work to the allocator crate without trying to understand how it works.
|
||||
|
||||
[previous post]: @/second-edition/posts/10-heap-allocation/index.md
|
||||
[map-heap]: @/second-edition/posts/10-heap-allocation/index.md#creating-a-kernel-heap
|
||||
[use-alloc-crate]: @/second-edition/posts/10-heap-allocation/index.md#using-an-allocator-crate
|
||||
[previous post]: @/edition-2/posts/10-heap-allocation/index.md
|
||||
[map-heap]: @/edition-2/posts/10-heap-allocation/index.md#creating-a-kernel-heap
|
||||
[use-alloc-crate]: @/edition-2/posts/10-heap-allocation/index.md#using-an-allocator-crate
|
||||
|
||||
In this post, we will show how to create our own heap allocator from scratch instead of relying on an existing allocator crate. We will discuss different allocator designs, including a simplistic _bump allocator_ and a basic _fixed-size block allocator_, and use this knowledge to implement an allocator with improved performance (compared to the `linked_list_allocator` crate).
|
||||
|
||||
@@ -120,7 +120,7 @@ We chose to create a separate `init` function instead of performing the initiali
|
||||
|
||||
As [explained in the previous post][global-alloc], all heap allocators need to implement the [`GlobalAlloc`] trait, which is defined like this:
|
||||
|
||||
[global-alloc]: @/second-edition/posts/10-heap-allocation/index.md#the-allocator-interface
|
||||
[global-alloc]: @/edition-2/posts/10-heap-allocation/index.md#the-allocator-interface
|
||||
[`GlobalAlloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html
|
||||
|
||||
```rust
|
||||
@@ -192,12 +192,12 @@ Note that the compiler suggestion to change `&self` to `&mut self` in the method
|
||||
|
||||
Before we look at a possible solution to this mutability problem, let's try to understand why the `GlobalAlloc` trait methods are defined with `&self` arguments: As we saw [in the previous post][global-allocator], the global heap allocator is defined by adding the `#[global_allocator]` attribute to a `static` that implements the `GlobalAlloc` trait. Static variables are immutable in Rust, so there is no way to call a method that takes `&mut self` on the static allocator. For this reason, all the methods of `GlobalAlloc` only take an immutable `&self` reference.
|
||||
|
||||
[global-allocator]: @/second-edition/posts/10-heap-allocation/index.md#the-global-allocator-attribute
|
||||
[global-allocator]: @/edition-2/posts/10-heap-allocation/index.md#the-global-allocator-attribute
|
||||
|
||||
Fortunately there is a way how to get a `&mut self` reference from a `&self` reference: We can use synchronized [interior mutability] by wrapping the allocator in a [`spin::Mutex`] spinlock. This type provides a `lock` method that performs [mutual exclusion] and thus safely turns a `&self` reference to a `&mut self` reference. We already used the wrapper type multiple times in our kernel, for example for the [VGA text buffer][vga-mutex].
|
||||
|
||||
[interior mutability]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
|
||||
[vga-mutex]: @/second-edition/posts/03-vga-text-buffer/index.md#spinlocks
|
||||
[vga-mutex]: @/edition-2/posts/03-vga-text-buffer/index.md#spinlocks
|
||||
[`spin::Mutex`]: https://docs.rs/spin/0.5.0/spin/struct.Mutex.html
|
||||
[mutual exclusion]: https://en.wikipedia.org/wiki/Mutual_exclusion
|
||||
|
||||
@@ -370,7 +370,7 @@ We don't need to change the `ALLOCATOR.lock().init(HEAP_START, HEAP_SIZE)` call
|
||||
|
||||
Now our kernel uses our bump allocator! Everything should still work, including the [`heap_allocation` tests] that we created in the previous post:
|
||||
|
||||
[`heap_allocation` tests]: @/second-edition/posts/10-heap-allocation/index.md#adding-a-test
|
||||
[`heap_allocation` tests]: @/edition-2/posts/10-heap-allocation/index.md#adding-a-test
|
||||
|
||||
```
|
||||
> cargo test --test heap_allocation
|
||||
@@ -441,7 +441,7 @@ While both of these approaches work to fix the test, they are no general solutio
|
||||
|
||||
As we learned [in the previous post][heap-intro], allocations can live arbitrarily long and can be freed in an arbitrary order. This means that we need to keep track of a potentially unbounded number of non-continuous, unused memory regions, as illustrated by the following example:
|
||||
|
||||
[heap-intro]: @/second-edition/posts/10-heap-allocation/index.md#dynamic-memory
|
||||
[heap-intro]: @/edition-2/posts/10-heap-allocation/index.md#dynamic-memory
|
||||
|
||||

|
||||
|
||||
@@ -696,7 +696,7 @@ The function performs a less obvious check after that. This check is necessary b
|
||||
|
||||
With the fundamental operations provided by the `add_free_region` and `find_region` methods, we can now finally implement the `GlobalAlloc` trait. As with the bump allocator, we don't implement the trait directly for the `LinkedListAllocator`, but only for a wrapped `Locked<LinkedListAllocator>`. The [`Locked` wrapper] adds interior mutability through a spinlock, which allows us to modify the allocator instance even though the `alloc` and `dealloc` methods only take `&self` references.
|
||||
|
||||
[`Locked` wrapper]: @/second-edition/posts/11-allocator-designs/index.md#a-locked-wrapper-type
|
||||
[`Locked` wrapper]: @/edition-2/posts/11-allocator-designs/index.md#a-locked-wrapper-type
|
||||
|
||||
The implementation looks like this:
|
||||
|
||||
@@ -1180,7 +1180,7 @@ On the implementation side, there are various things that we could improve in ou
|
||||
- Instead of falling back to a linked list allocator, we could a special allocator for allocations greater than 4KiB. The idea is to utilize [paging], which operates on 4KiB pages, to map a continuous block of virtual memory to non-continuous physical frames. This way, fragmentation of unused memory is no longer a problem for large allocations.
|
||||
- With such a page allocator, it might make sense to add block sizes up to 4KiB and drop the linked list allocator completely. The main advantages of this would be reduced fragmentation and improved performance predictability, i.e. better worse-case performance.
|
||||
|
||||
[paging]: @/second-edition/posts/08-paging-introduction/index.md
|
||||
[paging]: @/edition-2/posts/08-paging-introduction/index.md
|
||||
|
||||
It's important to note that the implementation improvements outlined above are only suggestions. Allocators used in operating system kernels are typically highly optimized to the specific workload of the kernel, which is only possible through extensive profiling.
|
||||
|
||||
@@ -1214,21 +1214,21 @@ The advantage of this merge process is that [external fragmentation] is reduced
|
||||
|
||||
This post gave an overview over different allocator designs. We learned how to implement a basic [bump allocator], which hands out memory linearly by increasing a single `next` pointer. While bump allocation is very fast, it can only reuse memory after all allocations have been freed. For this reason, it is rarely used as a global allocator.
|
||||
|
||||
[bump allocator]: @/second-edition/posts/11-allocator-designs/index.md#bump-allocator
|
||||
[bump allocator]: @/edition-2/posts/11-allocator-designs/index.md#bump-allocator
|
||||
|
||||
Next, we created a [linked list allocator] that uses the freed memory blocks itself to create a linked list, the so-called [free list]. This list makes it possible to store an arbitrary number of freed blocks of different sizes. While no memory waste occurs, the approach suffers from poor performance because an allocation request might require a complete traversal of the list. Our implementation also suffers from [external fragmentation] because it does not merge adjacent freed blocks back together.
|
||||
|
||||
[linked list allocator]: @/second-edition/posts/11-allocator-designs/index.md#linked-list-allocator
|
||||
[linked list allocator]: @/edition-2/posts/11-allocator-designs/index.md#linked-list-allocator
|
||||
[free list]: https://en.wikipedia.org/wiki/Free_list
|
||||
|
||||
To fix the performance problems of the linked list approach, we created a [fixed-size block allocator] that predefines a fixed set of block sizes. For each block size, a separate [free list] exists so that allocations and deallocations only need to insert/pop at the front of the list and are thus very fast. Since each allocation is rounded up to the next larger block size, some memory is wasted due to [internal fragmentation].
|
||||
|
||||
[fixed-size block allocator]: @/second-edition/posts/11-allocator-designs/index.md#fixed-size-block-allocator
|
||||
[fixed-size block allocator]: @/edition-2/posts/11-allocator-designs/index.md#fixed-size-block-allocator
|
||||
|
||||
There are many more allocator designs with different tradeoffs. [Slab allocation] works well to optimize the allocation of common fixed-size structures, but is not applicable in all situations. [Buddy allocation] uses a binary tree to merge freed blocks back together, but wastes a large amount of memory because it only supports power-of-2 block sizes. It's also important to remember that each kernel implementation has a unique workload, so there is no "best" allocator design that fits all cases.
|
||||
|
||||
[Slab allocation]: @/second-edition/posts/11-allocator-designs/index.md#slab-allocator
|
||||
[Buddy allocation]: @/second-edition/posts/11-allocator-designs/index.md#buddy-allocator
|
||||
[Slab allocation]: @/edition-2/posts/11-allocator-designs/index.md#slab-allocator
|
||||
[Buddy allocation]: @/edition-2/posts/11-allocator-designs/index.md#buddy-allocator
|
||||
|
||||
|
||||
## What's next?
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
@@ -42,7 +42,7 @@ The following graphic illustrates the task switching process on a hardware inter
|
||||
|
||||
In the first row, the CPU is executing task `A1` of program `A`. All other tasks are paused. In the second row, a hardware interrupt arrives at the CPU. As described in the [_Hardware Interrupts_] post, the CPU immediately stops the execution of task `A1` and jumps to the interrupt handler defined in the interrupt descriptor table (IDT). Through this interrupt handler, the operating system now has control of the CPU again, which allows it to switch to task `B1` instead of continuing task `A1`.
|
||||
|
||||
[_Hardware Interrupts_]: @/second-edition/posts/07-hardware-interrupts/index.md
|
||||
[_Hardware Interrupts_]: @/edition-2/posts/07-hardware-interrupts/index.md
|
||||
|
||||
#### Saving State
|
||||
|
||||
@@ -543,7 +543,7 @@ Because of its principle to provide _zero cost abstractions_, which means that a
|
||||
|
||||
The first observation is that [heap allocated] values already have a fixed memory address most of the time. They are created using a call to `allocate` and then referenced by a pointer type such as `Box<T>`. While moving the pointer type is possible, the heap value that the pointer points to stays at the same memory address until it is freed through a `deallocate` call again.
|
||||
|
||||
[heap allocated]: @/second-edition/posts/10-heap-allocation/index.md
|
||||
[heap allocated]: @/edition-2/posts/10-heap-allocation/index.md
|
||||
|
||||
Using heap allocation, we can try to create a self-referential struct:
|
||||
|
||||
@@ -697,7 +697,7 @@ fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>
|
||||
|
||||
The reason that this method takes `self: Pin<&mut Self>` instead of the normal `&mut self` is that future instances created from async/await are often self-referential, as we saw [above][self-ref-async-await]. By wrapping `Self` into `Pin` and letting the compiler opt-out of `Unpin` for self-referential futures generated from async/await, it is guaranteed that the futures are not moved in memory between `poll` calls. This ensures that all internal references are still valid.
|
||||
|
||||
[self-ref-async-await]: @/second-edition/posts/12-async-await/index.md#self-referential-structs
|
||||
[self-ref-async-await]: @/edition-2/posts/12-async-await/index.md#self-referential-structs
|
||||
|
||||
It is worth noting that moving futures before the first `poll` call is fine. This is a result of the fact that futures are lazy and do nothing until they're polled for the first time. The `start` state of the generated state machines therefore only contains the function arguments, but no internal references. In order to call `poll`, the caller must wrap the future into `Pin` first, which ensures that the future cannot be moved in memory anymore. Since stack pinning is more difficult to get right, I recommend to always use [`Box::pin`] combined with [`Pin::as_mut`] for this.
|
||||
|
||||
@@ -1023,7 +1023,7 @@ Our simple executor does not utilize the `Waker` notifications and simply loops
|
||||
|
||||
We already have some kind of asynchronicity in our system that we can use for this: hardware interrupts. As we learned in the [_Interrupts_] post, hardware interrupts can occur at arbitrary points in time, determined by some external device. For example, a hardware timer sends an interrupt to the CPU after some predefined time elapsed. When the CPU receives an interrupt, it immediately transfers control to the corresponding handler function defined in the interrupt descriptor table (IDT).
|
||||
|
||||
[_Interrupts_]: @/second-edition/posts/07-hardware-interrupts/index.md
|
||||
[_Interrupts_]: @/edition-2/posts/07-hardware-interrupts/index.md
|
||||
|
||||
In the following, we will create an asynchronous task based on the keyboard interrupt. The keyboard interrupt is a good candidate for this because it is both non-deterministic and latency-critical. Non-deteministic means that there is no way to predict when the next key press will occur because it is entirely dependent on the user. Latency-critical means that we want to handle the keyboard input in a timely manner, otherwise the user will feel a lag. To support such a task in an efficient way, it will be essential that the executor has proper support for `Waker` notifications.
|
||||
|
||||
@@ -1367,7 +1367,7 @@ pub async fn print_keypresses() {
|
||||
|
||||
The code is very similar to the code we had in our [keyboard interrupt handler] before we modified it in this post. The only difference is that, instead of reading the scancode from an I/O port, we take it from the `ScancodeStream`. For this, we first create a new `Scancode` stream and then repeatedly use the [`next`] method provided by the [`StreamExt`] trait to get a `Future` that resolves to the next element in the stream. By using the `await` operator on it, we asynchronously wait for the result of the future.
|
||||
|
||||
[keyboard interrupt handler]: @/second-edition/posts/07-hardware-interrupts/index.md#interpreting-the-scancodes
|
||||
[keyboard interrupt handler]: @/edition-2/posts/07-hardware-interrupts/index.md#interpreting-the-scancodes
|
||||
[`next`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html#method.next
|
||||
[`StreamExt`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html
|
||||
|
||||
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |