Rename second-edition subfolder to `edition-2

This commit is contained in:
Philipp Oppermann
2020-12-16 14:56:36 +01:00
parent 4a9ea6c503
commit f692c5b377
148 changed files with 130 additions and 128 deletions

View File

@@ -0,0 +1,6 @@
+++
title = "Extra Posts for Minimal Rust Kernel"
sort_by = "weight"
insert_anchor_links = "left"
render = false
+++

View File

@@ -0,0 +1,29 @@
+++
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 its 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
<!-- more -->
![stack frame with red zone](red-zone.svg)
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:
![red zone overwritten by exception handler](red-zone-overwrite.svg)
The CPU and the exception handler overwrite the data in 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.

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
<svg width="17cm" height="11cm" viewBox="-60 -21 340 212" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<text font-size="9.03111" style="fill: #000000;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="90" y="65.9389">
<tspan x="90" y="65.9389"></tspan>
</text>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="204" y1="70" x2="176.236" y2="70"/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" points="185.118,65 175.118,70 185.118,75 "/>
</g>
<text font-size="7.90222" style="fill: #000000;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="208" y="72.75">
<tspan x="208" y="72.75">Old Stack Pointer</tspan>
</text>
<text font-size="7.90222" style="fill: #000000;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="75" y="230">
<tspan x="75" y="230"></tspan>
</text>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="-20" y1="70" x2="-4" y2="70"/>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="-20" y1="190" x2="-4" y2="190"/>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke-dasharray: 4; stroke: #000000" x1="-12" y1="72.2361" x2="-12" y2="187.764"/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" points="-7,81.118 -12,71.118 -17,81.118 "/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" points="-17,178.882 -12,188.882 -7,178.882 "/>
</g>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke-dasharray: 4; stroke: #000000" x1="0" y1="-20" x2="0" y2="0"/>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke-dasharray: 4; stroke: #000000" x1="170" y1="-20" x2="170" y2="0"/>
<g>
<rect style="fill: #00ff00" x="0" y="0" width="170" height="20"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x="0" y="0" width="170" height="20"/>
</g>
<text font-size="9.03097" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="85" y="13.125">
<tspan x="85" y="13.125">Return Address</tspan>
</text>
<text font-size="9.03097" style="fill: #000000;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="84" y="4">
<tspan x="84" y="4"></tspan>
</text>
<g>
<rect style="fill: #dddddd" x="0" y="20" width="170" height="20"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x="0" y="20" width="170" height="20"/>
</g>
<text font-size="9.03111" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="85" y="33.125">
<tspan x="85" y="33.125">Local Variable 1</tspan>
</text>
<g>
<rect style="fill: #cccccc" x="0" y="40" width="170" height="32"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke-dasharray: 4; stroke: #000000" x="0" y="40" width="170" height="32"/>
</g>
<text font-size="9.03097" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="85" y="59.125">
<tspan x="85" y="59.125">Local Variables 2..n</tspan>
</text>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="0" y1="40" x2="170" y2="40"/>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="0" y1="72" x2="120" y2="72"/>
<g>
<rect style="fill: #ff3333" x="0" y="70" width="170" height="120"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x="0" y="70" width="170" height="120"/>
</g>
<text font-size="9.03111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="40" y="179.125">
<tspan x="40" y="179.125">Red Zone</tspan>
</text>
<text font-size="7.9021" style="fill: #000000;text-anchor:end;font-family:sans-serif;font-style:normal;font-weight:normal" x="-20" y="132.75">
<tspan x="-20" y="132.75">128 bytes</tspan>
</text>
<g>
<rect style="fill: #ffc200" x="30" y="70" width="140" height="30"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x="30" y="70" width="140" height="30"/>
</g>
<text font-size="9.03097" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="100" y="88.125">
<tspan x="100" y="88.125">Exception Stack Frame</tspan>
</text>
<g>
<rect style="fill: #ffe000" x="30" y="100" width="140" height="30"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x="30" y="100" width="140" height="30"/>
</g>
<text font-size="9.03097" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="100" y="118.125">
<tspan x="100" y="118.125">Register Backup</tspan>
</text>
<g>
<rect style="fill: #c6db97" x="30" y="130" width="140" height="30"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x="30" y="130" width="140" height="30"/>
</g>
<text font-size="8.46654" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="100" y="147.925">
<tspan x="100" y="147.925">Handler Function Stack Frame</tspan>
</text>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke-dasharray: 4; stroke: #ff3333" x1="30" y1="70" x2="30" y2="160"/>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke-dasharray: 4; stroke: #ff3333" x1="30" y1="160" x2="170" y2="160"/>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="204" y1="160" x2="176.236" y2="160"/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" points="185.118,155 175.118,160 185.118,165 "/>
</g>
<text font-size="7.90222" style="fill: #000000;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="208" y="162.635">
<tspan x="208" y="162.635">New Stack Pointer</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd">
<svg width="14cm" height="9cm" viewBox="-60 -21 270 172" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<text font-size="9.03111" style="fill: #000000;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="90" y="65.9389">
<tspan x="90" y="65.9389"></tspan>
</text>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="154" y1="70" x2="126.236" y2="70"/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" points="135.118,65 125.118,70 135.118,75 "/>
</g>
<text font-size="7.90222" style="fill: #000000;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="158" y="72.75">
<tspan x="158" y="72.75">Stack Pointer</tspan>
</text>
<text font-size="7.90222" style="fill: #000000;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="75" y="230">
<tspan x="75" y="230"></tspan>
</text>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="-20" y1="70" x2="-4" y2="70"/>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="-20" y1="150" x2="-4" y2="150"/>
<g>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke-dasharray: 4; stroke: #000000" x1="-12" y1="72.2361" x2="-12" y2="147.764"/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" points="-7,81.118 -12,71.118 -17,81.118 "/>
<polyline style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" points="-17,138.882 -12,148.882 -7,138.882 "/>
</g>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke-dasharray: 4; stroke: #000000" x1="0" y1="-20" x2="0" y2="0"/>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke-dasharray: 4; stroke: #000000" x1="120" y1="-20" x2="120" y2="0"/>
<g>
<rect style="fill: #00ff00" x="0" y="0" width="120" height="20"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x="0" y="0" width="120" height="20"/>
</g>
<text font-size="9.03097" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="60" y="13.125">
<tspan x="60" y="13.125">Return Address</tspan>
</text>
<text font-size="9.03097" style="fill: #000000;text-anchor:start;font-family:sans-serif;font-style:normal;font-weight:normal" x="84" y="4">
<tspan x="84" y="4"></tspan>
</text>
<g>
<rect style="fill: #dddddd" x="0" y="20" width="120" height="20"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x="0" y="20" width="120" height="20"/>
</g>
<text font-size="9.03111" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="60" y="33.125">
<tspan x="60" y="33.125">Local Variable 1</tspan>
</text>
<g>
<rect style="fill: #cccccc" x="0" y="40" width="120" height="32"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke-dasharray: 4; stroke: #000000" x="0" y="40" width="120" height="32"/>
</g>
<text font-size="9.03097" style="fill: #000000;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="60" y="59.125">
<tspan x="60" y="59.125">Local Variables 2..n</tspan>
</text>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="0" y1="40" x2="120" y2="40"/>
<line style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x1="0" y1="72" x2="120" y2="72"/>
<g>
<rect style="fill: #ff3333" x="0" y="70" width="120" height="80"/>
<rect style="fill: none; fill-opacity:0; stroke-width: 1; stroke: #000000" x="0" y="70" width="120" height="80"/>
</g>
<text font-size="9.03111" style="fill: #ffffff;text-anchor:middle;font-family:sans-serif;font-style:normal;font-weight:normal" x="60" y="113.125">
<tspan x="60" y="113.125">Red Zone</tspan>
</text>
<text font-size="7.9021" style="fill: #000000;text-anchor:end;font-family:sans-serif;font-style:normal;font-weight:normal" x="-20" y="112.75">
<tspan x="-20" y="112.75">128 bytes</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,44 @@
+++
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
<!-- more -->
- [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 (5121600 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 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, 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"
```

View File

@@ -0,0 +1,503 @@
+++
title = "یک هسته مینیمال با Rust"
weight = 2
path = "fa/minimal-rust-kernel"
date = 2018-02-10
[extra]
chapter = "Bare Bones"
# 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
<!-- more -->
این بلاگ بصورت آزاد روی [گیت‌هاب] توسعه داده شده است. اگر شما مشکل یا سوالی دارید، لطفاً آن‌جا یک ایشو باز کنید. شما همچنین می‌توانید [در زیر] این پست کامنت بگذارید. منبع کد کامل این پست را می‌توانید در بِرَنچ [`post-02`][post branch] پیدا کنید.
[گیت‌هاب]: https://github.com/phil-opp/blog_os
[در زیر]: #comments
[post branch]: https://github.com/phil-opp/blog_os/tree/post-02
<!-- toc -->
## فرآیند بوت شدن
وقتی یک رایانه را روشن می‌کنید، شروع به اجرای کد فِرْم‌وِر (کلمه: 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
اگر علاقه‌مند به ساخت بوت‌لودر هستید: با ما همراه باشید‌، مجموعه‌ای از پست‌ها در این زمینه از قبل برنامه‌ریزی شده است! <!-- , check out our “_[Writing a Bootloader]_” posts, where we explain in detail how a bootloader is built. -->
#### استاندارد بوت چندگانه
برای جلوگیری از این که هر سیستم عاملی بوت‌لودر خود را پیاده‌سازی کند، که فقط با یک سیستم عامل سازگار است، [بنیاد نرم افزار آزاد] در سال 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
راست دارای سه کانال انتشار است: _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/unstable-book/library-features/asm.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-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
اکثر فیلدها برای 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-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true
}
```
توجه داشته باشید که ما 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
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}
```
### ساخت هسته
عملیات کامپایل کردن برای هدف جدید ما از قراردادهای لینوکس استفاده خواهد کرد (کاملاً مطمئن نیستم که چرا، تصور می‌کنم این فقط پیش فرض 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 {}
}
#[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[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]
build-std = ["core", "compiler_builtins"]
```
این به کارگو می‌گوید که باید `core` و کتابخانه‌ `compiler_builtins` را دوباره کامپایل کند. مورد دوم لازم است زیرا یک وابستگی از `core` است. به منظور کامپایل مجدد این کتابخانه‌ها، کارگو نیاز به دسترسی به کد منبع Rust دارد که می‌توانیم آن را با `rustup component add rust-src` نصب کنیم.
<div class="note">
**یادداشت:** کلید پیکربندی `unstable.build-std` به نسخه‌‌ای جدیدتر از نسخه 2020-07-15 شبانه Rust نیاز دارد.
</div>
پس از تنظیم کلید پیکربندی `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` و غیره و اعمال صفت `[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]
build-std-features = ["compiler-builtins-mem"]
```
پشتیبانی برای ویژگی `compiler-builtins-mem` [به تازگی اضافه شده](https://github.com/rust-lang/rust/pull/77284)، پس حداقل به نسخه‌ شبانه‌ `2020-09-30` نیاز دارید.
در پشت صحنه، این پرچم [ویژگی `mem`] از کریت `compiler_builtins` را فعال می‌کند. اثرش این است که صفت `[no_mangle]#` بر روی [پیاده‌سازی `memcpy` و بقیه موارد] از کریت اعمال می‌شود، که آن‌ها در دسترس لینکر قرار می‌دهد. شایان ذکر است که این توابع در حال حاضر [بهینه نشده‌اند]، بنابراین ممکن است عملکرد آ‌ن‌ها در بهترین حالت نباشد، اما حداقل صحیح هستند. برای `x86_64` ، یک pull request باز برای [بهینه سازی این توابع با استفاده از دستورالعمل‌های خاص اسمبلی][memcpy rep movsb] وجود دارد.
[ویژگی `mem`]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L51-L52
[پیاده‌سازی `memcpy` و بقیه موارد]: (https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69)
[بهینه نشده‌اند]: 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
![screen output for common ASCII characters](https://upload.wikimedia.org/wikipedia/commons/f/f8/Codepage-437.png)
ما در پست بعدی، جایی که اولین درایور کوچک را برای آن می‌نویسیم، در مورد قالب دقیق بافر متن VGA بحث خواهیم کرد. برای چاپ “!Hello World”، فقط باید بدانیم که بافر در آدرس `0xb8000` قرار دارد و هر سلول کاراکتر از یک بایت ASCII و یک بایت رنگ تشکیل شده است.
پیاده‌سازی مشابه این است:
```rust
static HELLO: &[u8] = b"Hello World!";
#[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/stable/book/ch19-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/ch19-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.8"
```
افزودن بوت‌لودر به عنوان وابستگی برای ایجاد یک دیسک ایمیج قابل بوت کافی نیست. مشکل این است که ما باید هسته خود را با بوت لودر پیوند دهیم، اما کارگو از [اسکریپت های بعد از بیلد] پشتیبانی نمی‌کند.
[اسکریپت های بعد از بیلد]: 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]
```
این یک پنجره جداگانه با این شکل باز می‌کند:
![QEMU showing "Hello World!"](qemu.png)
می‌بینیم که “!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` را نیز اضافه خواهیم کرد.

View File

@@ -0,0 +1,501 @@
+++
title = "Rustでつくる最小のカーネル"
weight = 2
path = "ja/minimal-rust-kernel"
date = 2018-02-10
[extra]
chapter = "Bare Bones"
# Please update this when updating the translation
translation_based_on_commit = "7212ffaa8383122b1eb07fe1854814f99d2e1af4"
# GitHub usernames of the people that translated this post
translators = ["woodyZootopia", "JohnTitor"]
+++
この記事では、Rustで最小限の64bitカーネルを作ります。前の記事で作った[フリースタンディングなRustバイナリ][freestanding Rust binary]を下敷きにして、何かを画面に出力する、ブータブルディスクイメージを作ります。
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.ja.md
<!-- more -->
このブログの内容は [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
<!-- toc -->
## <ruby>起動<rp> (</rp><rt>Boot</rt><rp>) </rp></ruby>のプロセス
コンピュータを起動すると、マザーボードの [ROM] に保存されたファームウェアのコードを実行し始めます。このコードは、[<ruby>起動時の自己テスト<rp> (</rp><rt>power-on self test</rt><rp>) </rp></ruby>][power-on self-test]を実行し、使用可能なRAMを検出し、CPUとハードウェアを<ruby>事前初期化<rp> (</rp><rt>pre-initialize</rt><rp>) </rp></ruby>します。その後、<ruby>ブータブル<rp> (</rp><rt>bootable</rt><rp>) </rp></ruby>ディスクを探し、オペレーティングシステムのカーネルを<ruby>起動<rp> (</rp><rt>boot</rt><rp>) </rp></ruby>します。
[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が[<ruby>リアルモード<rp> (</rp><rt>real mode</rt><rp>) </rp></ruby>][real mode]と呼ばれる16bit互換モードにされてしまうということを意味しているからです。
まあ順を追って見ていくこととしましょう。
コンピュータは起動時にマザーボードにある特殊なフラッシュメモリからBIOSを読み込みます。BIOSは自己テストとハードウェアの初期化ルーチンを実行し、ブータブルディスクを探します。ディスクが見つかると、 **<ruby>ブートローダー<rp> (</rp><rt>bootloader</rt><rp>) </rp></ruby>** と呼ばれる、その先頭512バイトに保存された実行可能コードへと操作権が移ります。多くのブートローダーのサイズは512バイトより大きいため、通常は512バイトに収まる小さな最初のステージと、その最初のステージによって読み込まれる第2ステージに分けられています。
ブートローダーはディスク内のカーネルイメージの場所を特定し、メモリに読み込まなければなりません。また、CPUを16bitの[リアルモード][real mode]から32bitの[<ruby>プロテクトモード<rp> (</rp><rt>protected mode</rt><rp>) </rp></ruby>][protected mode]へ、そして64bitの[<ruby>ロングモード<rp> (</rp><rt>long mode</rt><rp>) </rp></ruby>][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を見つけることができません。他にも、カーネルに渡される[<ruby>ブート情報<rp> (</rp><rt>boot information</rt><rp>) </rp></ruby>][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!"と出力するようなディスクイメージを作ることです。というわけで、前の記事の[<ruby>独立した<rp> (</rp><rt>freestanding</rt><rp>) </rp></ruby>Rustバイナリ][freestanding Rust binary]をもとにして作っていきます。
覚えていますか、この独立したバイナリは`cargo`を使ってビルドしましたが、オペレーティングシステムに依って異なるエントリポイント名とコンパイルフラグが必要なのでした。これは`cargo`は標準では **ホストシステム**あなたの使っているシステム向けにビルドするためです。例えばWindows上で走るカーネルというのはあまり意味がなく、私達の望む動作ではありません。代わりに、明確に定義された **ターゲットシステム** 向けにコンパイルできると理想的です。
### 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]を有効化することができます。ただし、これらの実験的機能は全くもって<ruby>不安定<rp> (</rp><rt>unstable</rt><rp>) </rp></ruby>であり、将来のRustバージョンにおいては事前の警告なく変更されたり取り除かれたりする可能性があることに注意してください。このため、絶対に必要なときにのみこれらを使うことにします。
[`asm!` macro]: https://doc.rust-lang.org/unstable-book/library-features/asm.html
### ターゲットの仕様
Cargoは`--target`パラメータを使ってさまざまなターゲットをサポートします。ターゲットはいわゆる[target <ruby>triple<rp> (</rp><rt>3つ組</rt><rp>) </rp></ruby>][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-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
ほとんどのフィールドはLLVMがそのプラットフォーム向けのコードを生成するために必要なものです。例えば、[`data-layout`]フィールドは種々の整数、浮動小数点数、ポインタ型の大きさを定義しています。次に、`target-pointer-width`のような、条件付きコンパイルに用いられるフィールドがあります。第3の種類のフィールドはクレートがどのようにビルドされるべきかを定義します。例えば、`pre-link-args`フィールドは[<ruby>リンカ<rp> (</rp><rt>linker</rt><rp>) </rp></ruby>][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-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true
}
```
<ruby>ベアメタル<rp> (</rp><rt>bare metal</rt><rp>) </rp></ruby>環境で実行するので、`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]をサポートしていないので、プログラムは代わりに直接<ruby>中断<rp> (</rp><rt>abort</rt><rp>) </rp></ruby>しなければならないということを指定しています。これは、Cargo.tomlに`panic = "abort"`という設定を書くのに等しいですから、後者の設定を消しても構いませんこのターゲット設定は、Cargo.tomlの設定と異なり、このあと行う`core`ライブラリの再コンパイルにも適用されます。ですので、Cargo.tomlに設定する方が好みだったとしても、この設定を追加するようにしてください
[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php
```json
"disable-redzone": true,
```
カーネルを書いている以上、ある時点で<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]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md
```json
"features": "-mmx,-sse,+soft-float",
```
`features`フィールドは、ターゲットの<ruby>機能<rp> (</rp><rt>features</rt><rp>) </rp></ruby>を有効化/無効化します。マイナスを前につけることで`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
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}
```
### カーネルをビルドする
私達の新しいターゲットのコンパイルには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 {}
}
#[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[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コンパイラと一緒に<ruby>コンパイル済み<rp> (</rp><rt>precompiled</rt><rp>) </rp></ruby>ライブラリとして配布されているということです。そのため、これは、私達独自のターゲットではなく、サポートされているhost triple例えば `x86_64-unknown-linux-gnu`)でのみ使えるのです。他のターゲットのためにコードをコンパイルしたいときには、`core`をそれらのターゲットに向けて再コンパイルする必要があります。
#### `build-std`オプション
ここでcargoの[`build-std`機能][`build-std` feature]の出番です。これを使うと`core`やその他の標準ライブラリクレートについて、Rustインストール時に一緒についてくるコンパイル済みバージョンを使う代わりに、必要に応じて再コンパイルすることができます。これはとても新しくまだ完成していないので、<ruby>不安定<rp> (</rp><rt>unstable</rt><rp>) </rp></ruby>機能とされており、[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]
build-std = ["core", "compiler_builtins"]
```
これはcargoに`core``compiler_builtins`ライブラリを再コンパイルするよう命令します。後者が必要なのは`core`がこれに依存しているためです。 これらのライブラリを再コンパイルするためには、cargoがRustのソースコードにアクセスできる必要があります。これは`rustup component add rust-src`でインストールできます。
<div class="note">
**注意:** `unstable.build-std`設定キーを使うには、少なくとも2020-07-15以降のRust nightlyが必要です。
</div>
`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`を私達のカスタムターゲット向けに再コンパイルしているということがわかります。
#### メモリ関係の<ruby>組み込み関数<rp> (</rp><rt>intrinsics</rt><rp>) </rp></ruby>
Rustコンパイラは、すべてのシステムにおいて、特定の組み込み関数が利用可能であるということを前提にしています。それらの関数の多くは、私達がちょうど再コンパイルした`compiler_builtins`クレートによって提供されています。しかしながら、通常システムのCライブラリによって提供されているので標準では有効化されていない、メモリ関係の関数がいくつかあります。それらの関数には、メモリブロック内のすべてのバイトを与えられた値にセットする`memset`、メモリーブロックを他のブロックへとコピーする`memcpy`、2つのメモリーブロックを比較する`memcmp`などがあります。これらの関数はどれも、現在の段階で我々のカーネルをコンパイルするのに必要というわけではありませんが、コードを追加していくとすぐに必要になるでしょう(たとえば、構造体をコピーする、など)。
オペレーティングシステムのCライブラリにリンクすることはできませんので、これらの関数をコンパイラに与えてやる別の方法が必要になります。このための方法として考えられるものの一つが、自前で`memset`を実装し、(コンパイル中の自動リネームを防ぐため)`#[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`]フラグを`["computer-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]
build-std-features = ["compiler-builtins-mem"]
```
`compiler-builtins-mem`機能のサポートが追加されたのは[つい最近](https://github.com/rust-lang/rust/pull/77284)なので、`2019-09-30`以降のRust nightlyが必要です。
このとき、裏で`compiler_builtins`クレートの[`mem`機能][`mem` feature]が有効化されています。これにより、このクレートの[`memcpy`などの実装][`memcpy` etc. implementations]に`#[no_mangle]`アトリビュートが適用され、リンカがこれらを利用できるようになっています。これらの関数は今のところ[最適化されておらず][not optimized]、性能は最高ではないかもしれないものの、少なくとも正しい実装ではあるということは知っておく価値があるでしょう。`x86_64`については、[これらの関数を特殊なアセンブリ命令を使って最適化する][memcpy rep movsb]プルリクエストが提出されています。
[`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)
[not optimized]: 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`の[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
![screen output for common ASCII characters](https://upload.wikimedia.org/wikipedia/commons/f/f8/Codepage-437.png)
次の記事では、VGAバッファの正確なレイアウトについて議論し、このためのちょっとしたドライバも書きます。"Hello World!"を出力するためには、バッファがアドレス`0xb8000`にあり、それぞれの文字セルはASCIIのバイトと色のバイトからなることを知っている必要があります。
実装はこんな感じになります:
```rust
static HELLO: &[u8] = b"Hello World!";
#[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]にキャストします。次に[<ruby>静的<rp> (</rp><rt>static</rt><rp>) </rp></ruby>][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
すべてのメモリへの書き込み処理のコードを、[<ruby>`unsafe`<rp> (</rp><rt>安全でない</rt><rp>) </rp></ruby>][`unsafe`]ブロックが囲んでいることに注意してください。この理由は、私達の作った生ポインタが正しいものであることをRustコンパイラが証明できないためです。生ポインタはどんな場所でも指しうるので、データの破損につながるかもしれません。これらの操作を`unsafe`ブロックに入れることで、私達はこれが正しいことを確信しているとコンパイラに伝えているのです。ただし、`unsafe`ブロックはRustの安全性チェックを消すわけではなく、[追加で5つのことができるようになる][five additional things]だけということに注意してください。
<div class="note">
**訳注:** 翻訳時点(2020-10-20)では、リンク先のThe Rust book日本語版には「追加でできるようになること」は4つしか書かれていません。
</div>
[`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では安全な<ruby>abstraction<rp> (</rp><rt>抽象化されたもの</rt><rp>) </rp></ruby>を作ることができます。たとえば、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.8"
```
bootloaderを依存として加えることだけでブータブルディスクイメージが実際に作れるわけではなく、私達のカーネルをコンパイル後にブートローダーにリンクする必要があります。問題は、cargoが[<ruby>ビルド後<rp> (</rp><rt>post-build</rt><rp>) </rp></ruby>にスクリプトを走らせる機能][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ファイルを読み、解釈します。次にプログラム部を<ruby>ページテーブル<rp> (</rp><rt>page table</rt><rp>) </rp></ruby>の<ruby>仮想アドレス<rp> (</rp><rt>virtual address</rt><rp>) </rp></ruby>にマップし、`.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]
```
これにより、以下のような見た目の別のウィンドウが開きます:
![QEMU showing "Hello World!"](qemu.png)
私達の書いた"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`マクロのサポートも行います。

View File

@@ -0,0 +1,493 @@
+++
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
<!-- more -->
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
<!-- toc -->
## 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 centuries. 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, the 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! <!-- , check out our “_[Writing a Bootloader]_” posts, where we explain in detail how a bootloader is built. -->
#### 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 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 in 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. For that we build upon the [freestanding Rust binary] from the previous post.
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/unstable-book/library-features/asm.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 a `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-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
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 fields define 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 a `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-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true
}
```
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 be sure to add this option, even if you prefer to keep the Cargo.toml 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 corruptions 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 (5121600 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).
#### Putting it Together
Our target specification file now looks like this:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}
```
### 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]: @/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 {}
}
#[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[E0463]: can't find crate for `core`
```
It fails! The error tells us that the Rust compiler no longer finds 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 create a [cargo configuration] file at `.cargo/config.toml` with the following content:
```toml
# in .cargo/config.toml
[unstable]
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`.
<div class="note">
**Note:** The `unstable.build-std` configuration key requires at least the Rust nightly from 2020-07-15.
</div>
After setting the `unstable.build-std` configuration key and installing the `rust-src` component, we can rerun the 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 `#[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, you might get an endless recursion when implementing `memcpy` using a `for` loop because `for` loops implicitly call the [`IntoIterator::into_iter`] trait method, which might 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 `-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]
build-std-features = ["compiler-builtins-mem"]
```
(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 `#[no_mangle]` attribute is applied to the [`memcpy` etc. implementations] of the crate, which makes them available to the linker. It's worth noting that these functions are [not optimized] right now, so their performance might not be the best, but at least they are correct. For `x86_64`, there is an open pull request to [optimize these functions using special assembly instructions][memcpy rep movsb].
[`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
[not optimized]: https://github.com/rust-lang/compiler-builtins/issues/339
[memcpy rep movsb]: https://github.com/rust-lang/compiler-builtins/pull/365
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
![screen output for common ASCII characters](https://upload.wikimedia.org/wikipedia/commons/f/f8/Codepage-437.png)
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!";
#[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/stable/book/ch19-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/ch19-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` 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.8"
```
Adding the bootloader as 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, 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, we can create a bootable disk image by 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 to 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 an USB drive to boot it on real hardware. (Note that this is not a CD image, which have 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
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
```
This opens a separate window with that looks like this:
![QEMU showing "Hello World!"](qemu.png)
We see that our "Hello World!" is visible on the screen.
### Real Machine
It is also possible to write it to an USB stick and boot it on a real machine:
```
> 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. **Be careful** to choose the correct device name, because everything on that device is overwritten.
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 that have set the `"os"` field of their target configuration file 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 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.

View File

@@ -0,0 +1,389 @@
+++
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 = "bd6fbcb1c36705b2c474d7fcee387bfea1210851"
# GitHub usernames of the people that translated this post
translators = ["luojia65", "Rustin-Liu"]
+++
在这篇文章中,我们将基于 **x86架构**the x86 architecture使用 Rust 语言,编写一个最小化的 64 位内核。我们将从上一章中构建的独立式可执行程序开始,构建自己的内核;它将向显示器打印字符串,并能被打包为一个能够引导启动的**磁盘映像**disk image
[freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.md
<!-- more -->
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
<!-- toc -->
## 引导启动
当我们启动电脑时,主板 [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 固件的引导启动方式。
### 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 引导你的系统内核是可能的。
## 最小化内核
现在我们已经明白电脑是如何启动的那也是时候编写我们自己的内核了。我们的小目标是创建一个内核的磁盘映像它能够在启动时向屏幕输出一行“Hello World!”;我们的工作将基于上一章构建的独立式可执行程序。
如果读者还有印象的话,在上一章,我们使用 `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/unstable-book/library-features/asm.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-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
```
一个配置清单中包含多个**配置项**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-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true,
}
```
需要注意的是,因为我们要在**裸机**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有关的栈指针优化因为此时这个优化可能会导致栈被破坏。我们撰写了一篇专门的短文来更详细地解释红区及与其相关的优化。
```json
"features": "-mmx,-sse,+soft-float",
```
`features` 配置项被用来启用或禁用某个目标 **CPU 特征**CPU feature。通过在它们前面添加`-`号,我们将 `mmx``sse` 特征禁用;添加前缀`+`号,我们启用了 `soft-float` 特征。
`mmx``sse` 特征决定了是否支持**单指令多数据流**[Single Instruction Multiple DataSIMD](https://en.wikipedia.org/wiki/SIMD))相关指令,这些指令常常能显著地提高程序层面的性能。然而,在内核中使用庞大的 SIMD 寄存器,可能会造成较大的性能影响:因为每次程序中断时,内核不得不储存整个庞大的 SIMD 寄存器以备恢复——这意味着,对每个硬件中断或系统调用,完整的 SIMD 状态必须存到主存中。由于 SIMD 状态可能相当大512~1600 个字节),而中断可能时常发生,这些额外的存储与恢复操作可能显著地影响效率。为解决这个问题,我们对内核禁用 SIMD但这不意味着禁用内核之上的应用程序的 SIMD 支持)。
禁用 SIMD 产生的一个问题是,`x86_64` 架构的浮点数指针运算默认依赖于 SIMD 寄存器。我们的解决方法是,启用 `soft-float` 特征,它将使用基于整数的软件功能,模拟浮点数指针运算。
为了让读者的印象更清晰,我们撰写了一篇关于禁用 SIMD 的短文。
现在,我们将各个配置项整合在一起。我们的目标配置清单应该长这样:
```json
{
"llvm-target": "x86_64-unknown-none",
"data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "none",
"executables": true,
"linker-flavor": "ld.lld",
"linker": "rust-lld",
"panic-strategy": "abort",
"disable-redzone": true,
"features": "-mmx,-sse,+soft-float"
}
```
### 编译内核
要编译我们的内核,我们将使用 Linux 系统的编写风格(这可能是 LLVM 的默认风格)。这意味着,我们需要把前一篇文章中编写的入口点重命名为 `_start`
```rust
// src/main.rs
#![no_std] // 不链接 Rust 标准库
#![no_main] // 禁用所有 Rust 层级的入口点
use core::panic::PanicInfo;
/// 这个函数将在 panic 时被调用
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
#[no_mangle] // 不重整函数名
pub extern "C" fn _start() -> ! {
// 因为编译器会寻找一个名为 `_start` 的函数,所以这个函数就是入口点
// 默认命名为 `_start`
loop {}
}
```
注意的是,无论你开发使用的是哪类操作系统,你都需要将入口点命名为 `_start`。前一篇文章中编写的 Windows 系统和 macOS 对应的入口点不应该被保留。
通过把 JSON 文件名传入 `--target` 选项,我们现在可以开始编译我们的内核。让我们试试看:
```
> cargo build --target x86_64-blog_os.json
error[E0463]: can't find crate for `core`
(或者是下面的错误)
error[E0463]: can't find crate for `compiler_builtins`
```
哇哦编译失败了输出的错误告诉我们Rust 编译器找不到 `core` 或者 `compiler_builtins` 包;而所有 `no_std` 上下文都隐式地链接到这两个包。[`core` 包](https://doc.rust-lang.org/nightly/core/index.html)包含基础的 Rust 类型,如` Result``Option` 和迭代器等;[`compiler_builtins` 包](https://github.com/rust-lang-nursery/compiler-builtins)提供 LLVM 需要的许多底层操作,比如 `memcpy`
通常状况下,`core` 库以**预编译库**precompiled library的形式与 Rust 编译器一同发布——这时,`core` 库只对支持的宿主系统有效,而我们自定义的目标系统无效。如果我们想为其它系统编译代码,我们需要为这些系统重新编译整个 `core` 库。
### Cargo xbuild
这就是为什么我们需要 [cargo xbuild 工具](https://github.com/rust-osdev/cargo-xbuild)。这个工具封装了 `cargo build`;但不同的是,它将自动交叉编译 `core` 库和一些**编译器内建库**compiler built-in libraries。我们可以用下面的命令安装它
```bash
cargo install cargo-xbuild
```
这个工具依赖于Rust的源代码我们可以使用 `rustup component add rust-src` 来安装源代码。
现在我们可以使用 `xbuild` 代替 `build` 重新编译:
```bash
> cargo xbuild --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling compiler_builtins v0.1.5
Compiling rustc-std-workspace-core v1.0.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling alloc v0.0.0 (/tmp/xargo.PB7fj9KZJhAI)
Finished release [optimized + debuginfo] target(s) in 45.18s
Compiling blog_os v0.1.0 (file:///…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
```
我们能看到,`cargo xbuild` 为我们自定义的目标交叉编译了 `core``compiler_builtin``alloc` 三个部件。这些部件使用了大量的**不稳定特性**unstable features所以只能在[nightly 版本的 Rust 编译器][installing rust nightly]中工作。这之后,`cargo xbuild` 成功地编译了我们的 `blog_os` 包。
[installing rust nightly]: #an-zhuang-nightly-rust
现在我们可以为裸机编译内核了;但是,我们提供给引导程序的入口点 `_start` 函数还是空的。我们可以添加一些东西进去,不过我们可以先做一些优化工作。
### 设置默认目标
为了避免每次使用`cargo xbuild`时传递`--target`参数,我们可以覆写默认的编译目标。我们创建一个名为`.cargo/config`的[cargo配置文件](https://doc.rust-lang.org/cargo/reference/config.html),添加下面的内容:
```toml
# in .cargo/config
[build]
target = "x86_64-blog_os.json"
```
这里的配置告诉 `cargo` 在没有显式声明目标的情况下,使用我们提供的 `x86_64-blog_os.json` 作为目标配置。这意味着保存后,我们可以直接使用:
```
cargo xbuild
```
来编译我们的内核。[官方提供的一份文档](https://doc.rust-lang.org/cargo/reference/config.html)中有对 cargo 配置文件更详细的说明。
### 向屏幕打印字符
要做到这一步,最简单的方式是写入 **VGA 字符缓冲区**[VGA text buffer](https://en.wikipedia.org/wiki/VGA-compatible_text_mode)):这是一段映射到 VGA 硬件的特殊内存片段,包含着显示在屏幕上的内容。通常情况下,它能够存储 25 行、80 列共 2000 个**字符单元**character cell每个字符单元能够显示一个 ASCII 字符,也能设置这个字符的**前景色**foreground color和**背景色**background color。输出到屏幕的字符大概长这样
![](https://upload.wikimedia.org/wikipedia/commons/6/6d/Codepage-737.png)
我们将在下篇文章中详细讨论 VGA 字符缓冲区的内存布局;目前我们只需要知道,这段缓冲区的地址是 `0xb8000`,且每个字符单元包含一个 ASCII 码字节和一个颜色字节。
我们的实现就像这样:
```rust
static HELLO: &[u8] = b"Hello World!";
#[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.3"
```
只添加引导程序为依赖项,并不足以创建一个可引导的磁盘映像;我们还需要内核编译完成之后,将内核和引导程序组合在一起。然而,截至目前,原生的 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 xbuild` 编译你的内核,所以它将增量编译我们修改后的源码。在这之后,它会编译内核的引导程序,这可能将花费一定的时间;但和所有其它依赖包相似的是,在首次编译后,产生的二进制文件将被缓存下来——这将显著地加速后续的编译过程。最终,`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=bootimage-blog_os.bin
```
![](https://os.phil-opp.com/minimal-rust-kernel/qemu.png)
我们可以看到,屏幕窗口已经显示出 “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
[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 xrun` 来编译内核并在 QEMU 中启动了。和 `xbuild` 类似,`xrun` 子命令将在调用 cargo 命令前编译内核所需的包。这个子命令也由 `cargo-xbuild` 工具提供,所以你不需要安装额外的工具。
## 下篇预告
在下篇文章中,我们将细致地探索 VGA 字符缓冲区,并包装它为一个安全的接口。我们还将基于它实现 `println!` 宏。

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB