+++ title = "Rustでつくる最小のカーネル" weight = 2 path = "ja/minimal-rust-kernel" date = 2018-02-10 [extra] # Please update this when updating the translation translation_based_on_commit = "7212ffaa8383122b1eb07fe1854814f99d2e1af4" # GitHub usernames of the people that translated this post translators = ["swnakamura", "JohnTitor"] +++ この記事では、Rustで最小限の64bitカーネルを作ります。前の記事で作った[フリースタンディングなRustバイナリ][freestanding Rust binary]を下敷きにして、何かを画面に出力する、ブータブルディスクイメージを作ります。 [freestanding Rust binary]: @/edition-2/posts/01-freestanding-rust-binary/index.ja.md このブログの内容は [GitHub] 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。また[こちら][at the bottom]にコメントを残すこともできます。この記事の完全なソースコードは[`post-02` ブランチ][post branch]にあります。 [GitHub]: https://github.com/phil-opp/blog_os [at the bottom]: #comments [post branch]: https://github.com/phil-opp/blog_os/tree/post-02 ## 起動のプロセス {#the-boot-process} コンピュータを起動すると、マザーボードの [ROM] に保存されたファームウェアのコードを実行し始めます。このコードは、[起動時の自己テスト][power-on self-test]を実行し、使用可能なRAMを検出し、CPUとハードウェアを事前初期化します。その後、ブータブルディスクを探し、オペレーティングシステムのカーネルを起動します。 [ROM]: https://ja.wikipedia.org/wiki/Read_only_memory [power-on self-test]: https://ja.wikipedia.org/wiki/Power_On_Self_Test x86には2つのファームウェアの標準規格があります:"Basic Input/Output System" (**[BIOS]**) と、より新しい "Unified Extensible Firmware Interface" (**[UEFI]**) です。BIOS規格は古く時代遅れですが、シンプルでありすべてのx86のマシンで1980年代からよくサポートされています。対して、UEFIはより現代的でずっと多くの機能を持っていますが、セットアップが複雑です(少なくとも私はそう思います)。 [BIOS]: https://ja.wikipedia.org/wiki/Basic_Input/Output_System [UEFI]: https://ja.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface 今の所、このブログではBIOSしかサポートしていませんが、UEFIのサポートも計画中です。お手伝いいただける場合は、[GitHubのissue](https://github.com/phil-opp/blog_os/issues/349)をご覧ください。 ### BIOSの起動 ほぼすべてのx86システムがBIOSによる起動をサポートしています。これは近年のUEFIベースのマシンも例外ではなく、それらはエミュレートされたBIOSを使います。前世紀のすべてのマシンにも同じブートロジックが使えるなんて素晴らしいですね。しかし、この広い互換性は、BIOSによる起動の最大の欠点でもあるのです。というのもこれは、1980年代の化石のようなブートローダーを動かすために、CPUが[リアルモード][real mode]と呼ばれる16bit互換モードにされてしまうということを意味しているからです。 まあ順を追って見ていくこととしましょう。 コンピュータは起動時にマザーボードにある特殊なフラッシュメモリからBIOSを読み込みます。BIOSは自己テストとハードウェアの初期化ルーチンを実行し、ブータブルディスクを探します。ディスクが見つかると、 **ブートローダー** と呼ばれる、その先頭512バイトに保存された実行可能コードへと操作権が移ります。多くのブートローダーのサイズは512バイトより大きいため、通常は512バイトに収まる小さな最初のステージと、その最初のステージによって読み込まれる第2ステージに分けられています。 ブートローダーはディスク内のカーネルイメージの場所を特定し、メモリに読み込まなければなりません。また、CPUを16bitの[リアルモード][real mode]から32bitの[プロテクトモード][protected mode]へ、そして64bitの[ロングモード][long mode]――64bitレジスタとすべてのメインメモリが利用可能になります――へと変更しなければなりません。3つ目の仕事は、特定の情報(例えばメモリーマップなどです)をBIOSから聞き出し、OSのカーネルに渡すことです。 [real mode]: https://ja.wikipedia.org/wiki/リアルモード [protected mode]: https://ja.wikipedia.org/wiki/プロテクトモード [long mode]: https://en.wikipedia.org/wiki/Long_mode [memory segmentation]: https://en.wikipedia.org/wiki/X86_memory_segmentation ブートローダーを書くのにはアセンブリ言語を必要とするうえ、「何も考えずにプロセッサーのこのレジスタにこの値を書き込んでください」のような勉強の役に立たない作業がたくさんあるので、ちょっと面倒くさいです。ですのでこの記事ではブートローダーの制作については飛ばして、代わりに[bootimage]という、自動でカーネルの前にブートローダを置いてくれるツールを使いましょう。 [bootimage]: https://github.com/rust-osdev/bootimage 自前のブートローダーを作ることに興味がある人もご期待下さい、これに関する記事も計画中です! #### Multiboot標準規格 すべてのオペレーティングシステムが、自身にのみ対応しているブートローダーを実装するということを避けるために、1995年に[フリーソフトウェア財団][Free Software Foundation]が[Multiboot]というブートローダーの公開標準規格を策定しています。この標準規格では、ブートローダーとオペレーティングシステムのインターフェースが定義されており、Multibootに準拠したブートローダーであれば、同じくそれに準拠したすべてのオペレーティングシステムが読み込めるようになっています。そのリファレンス実装として、Linuxシステムで一番人気のブートローダーである[GNU GRUB]があります。 [Free Software Foundation]: https://ja.wikipedia.org/wiki/フリーソフトウェア財団 [Multiboot]: https://wiki.osdev.org/Multiboot [GNU GRUB]: https://ja.wikipedia.org/wiki/GNU_GRUB カーネルをMultibootに準拠させるには、カーネルファイルの先頭にいわゆる[Multiboot header]を挿入するだけで済みます。このおかげで、OSをGRUBで起動するのはとても簡単です。しかし、GRUBとMultiboot標準規格にはいくつか問題もあります: [Multiboot header]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format - これらは32bitプロテクトモードしかサポートしていません。そのため、64bitロングモードに変更するためのCPUの設定は依然行う必要があります。 - これらは、カーネルではなくブートローダーがシンプルになるように設計されています。例えば、カーネルは[通常とは異なるデフォルトページサイズ][adjusted default page size]でリンクされる必要があり、そうしないとGRUBはMultiboot headerを見つけることができません。他にも、カーネルに渡される[ブート情報][boot information]は、クリーンな抽象化を与えてくれず、アーキテクチャ依存の構造を多く含んでいます。 - GRUBもMultiboot標準規格もドキュメントが充実していません。 - カーネルファイルからブータブルディスクイメージを作るには、ホストシステムにGRUBがインストールされている必要があります。これにより、MacとWindows上での開発は比較的難しくなっています。 [adjusted default page size]: https://wiki.osdev.org/Multiboot#Multiboot_2 [boot information]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format これらの欠点を考慮し、私達はGRUBとMultiboot標準規格を使わないことに決めました。しかし、あなたのカーネルをGRUBシステム上で読み込めるように、私達の[bootimage]ツールにMultibootのサポートを追加することも計画しています。Multiboot準拠なカーネルを書きたい場合は、このブログシリーズの[第1版][first edition]をご覧ください。 [first edition]: @/edition-1/_index.md ### UEFI (今の所UEFIのサポートは提供していませんが、ぜひともしたいと思っています!お手伝いいただける場合は、 [GitHub issue](https://github.com/phil-opp/blog_os/issues/349)で教えてください。) ## 最小のカーネル どのようにコンピュータが起動するのかについてざっくりと理解できたので、自前で最小のカーネルを書いてみましょう。目標は、起動したら画面に"Hello, World!"と出力するようなディスクイメージを作ることです。というわけで、前の記事の[独立したRustバイナリ][freestanding Rust binary]をもとにして作っていきます。 覚えていますか、この独立したバイナリは`cargo`を使ってビルドしましたが、オペレーティングシステムに依って異なるエントリポイント名とコンパイルフラグが必要なのでした。これは`cargo`は標準では **ホストシステム**(あなたの使っているシステム)向けにビルドするためです。例えばWindows上で走るカーネルというのはあまり意味がなく、私達の望む動作ではありません。代わりに、明確に定義された **ターゲットシステム** 向けにコンパイルできると理想的です。 ### RustのNightly版をインストールする {#installing-rust-nightly} Rustには**stable**、**beta**、**nightly**の3つのリリースチャンネルがあります。Rust Bookはこれらの3つのチャンネルの違いをとても良く説明しているので、一度[確認してみてください](https://doc.rust-jp.rs/book-ja/appendix-07-nightly-rust.html)。オペレーティングシステムをビルドするには、nightlyチャンネルでしか利用できないいくつかの実験的機能を使う必要があるので、Rustのnightly版をインストールすることになります。 Rustの実行環境を管理するのには、[rustup]を強くおすすめします。nightly、beta、stable版のコンパイラをそれぞれインストールすることができますし、アップデートするのも簡単です。現在のディレクトリにnightlyコンパイラを使うようにするには、`rustup override set nightly`と実行してください。もしくは、`rust-toolchain`というファイルに`nightly`と記入してプロジェクトのルートディレクトリに置くことでも指定できます。Nightly版を使っていることは、`rustc --version`と実行することで確かめられます。表示されるバージョン名の末尾に`-nightly`とあるはずです。 [rustup]: https://www.rustup.rs/ nightlyコンパイラでは、いわゆる**feature flag**をファイルの先頭につけることで、いろいろな実験的機能を使うことを選択できます。例えば、`#![feature(asm)]`を`main.rs`の先頭につけることで、インラインアセンブリのための実験的な[`asm!`マクロ][`asm!` macro]を有効化することができます。ただし、これらの実験的機能は全くもって不安定であり、将来のRustバージョンにおいては事前の警告なく変更されたり取り除かれたりする可能性があることに注意してください。このため、絶対に必要なときにのみこれらを使うことにします。 [`asm!` macro]: https://doc.rust-lang.org/stable/reference/inline-assembly.html ### ターゲットの仕様 Cargoは`--target`パラメータを使ってさまざまなターゲットをサポートします。ターゲットはいわゆる[target triple][target triple]によって表されます。これはCPUアーキテクチャ、製造元、オペレーティングシステム、そして[ABI]を表します。例えば、`x86_64-unknown-linux-gnu`というtarget tripleは、`x86_64`のCPU、製造元不明、GNU ABIのLinuxオペレーティングシステム向けのシステムを表します。Rustは[多くのtarget triple][platform-support]をサポートしており、その中にはAndroidのための`arm-linux-androideabi`や[WebAssemblyのための`wasm32-unknown-unknown`](https://www.hellorust.com/setup/wasm-target/)などがあります。 [target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple [ABI]: https://stackoverflow.com/a/2456882 [platform-support]: https://forge.rust-lang.org/release/platform-support.html [custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html しかしながら、私達のターゲットシステムには、いくつか特殊な設定パラメータが必要になります(例えば、その下ではOSが走っていない、など)。なので、[既存のtarget triple][platform-support]はどれも当てはまりません。ありがたいことに、RustではJSONファイルを使って[独自のターゲット][custom-targets]を定義できます。例えば、`x86_64-unknown-linux-gnu`というターゲットを表すJSONファイルはこんな感じです。 ```json { "llvm-target": "x86_64-unknown-linux-gnu", "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", "arch": "x86_64", "target-endian": "little", "target-pointer-width": 64, "target-c-int-width": 32, "os": "linux", "executables": true, "linker-flavor": "gcc", "pre-link-args": ["-m64"], "morestack": false } ``` ほとんどのフィールドはLLVMがそのプラットフォーム向けのコードを生成するために必要なものです。例えば、[`data-layout`]フィールドは種々の整数、浮動小数点数、ポインタ型の大きさを定義しています。次に、`target-pointer-width`のような、条件付きコンパイルに用いられるフィールドがあります。第3の種類のフィールドはクレートがどのようにビルドされるべきかを定義します。例えば、`pre-link-args`フィールドは[リンカ][linker]に渡される引数を指定しています。 [`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout [linker]: https://ja.wikipedia.org/wiki/リンケージエディタ 私達のカーネルも`x86_64`のシステムをターゲットとするので、私達のターゲット仕様も上のものと非常によく似たものになるでしょう。`x86_64-blog_os.json`というファイル(お好きな名前を選んでください)を作り、共通する要素を埋めるところから始めましょう。 ```json { "llvm-target": "x86_64-unknown-none", "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", "arch": "x86_64", "target-endian": "little", "target-pointer-width": 64, "target-c-int-width": 32, "os": "none", "executables": true } ``` ベアメタル環境で実行するので、`llvm-target`のOSを変え、`os`フィールドを`none`にしたことに注目してください。 以下の、ビルドに関係する項目を追加します。 ```json "linker-flavor": "ld.lld", "linker": "rust-lld", ``` 私達のカーネルをリンクするのに、プラットフォーム標準の(Linuxターゲットをサポートしていないかもしれない)リンカではなく、Rustに付属しているクロスプラットフォームの[LLD]リンカを使用します。 [LLD]: https://lld.llvm.org/ ```json "panic-strategy": "abort", ``` この設定は、ターゲットがパニック時の[stack unwinding]をサポートしていないので、プログラムは代わりに直接中断しなければならないということを指定しています。これは、Cargo.tomlに`panic = "abort"`という設定を書くのに等しいですから、後者の設定を消しても構いません(このターゲット設定は、Cargo.tomlの設定と異なり、このあと行う`core`ライブラリの再コンパイルにも適用されます。ですので、Cargo.tomlに設定する方が好みだったとしても、この設定を追加するようにしてください)。 [stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php ```json "disable-redzone": true, ``` カーネルを書いている以上、ある時点で割り込みを処理しなければならなくなるでしょう。これを安全に行うために、 **"red zone"** と呼ばれる、ある種のスタックポインタ最適化を無効化する必要があります。こうしないと、スタックの破損を引き起こしてしまう恐れがあるためです。より詳しくは、[red zoneの無効化][disabling the red zone]という別記事をご覧ください。 [disabling the red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md ```json "features": "-mmx,-sse,+soft-float", ``` `features`フィールドは、ターゲットの機能を有効化/無効化します。マイナスを前につけることで`mmx`と`sse`という機能を無効化し、プラスを前につけることで`soft-float`という機能を有効化しています。それぞれのフラグの間にスペースは入れてはならず、もしそうするとLLVMが機能文字列の解釈に失敗してしまうことに注意してください。 `mmx`と`sse`という機能は、[Single Instruction Multiple Data (SIMD)]命令をサポートするかを決定します。この命令は、しばしばプログラムを著しく速くしてくれます。しかし、大きなSIMDレジスタをOSカーネルで使うことは性能上の問題に繋がります。 その理由は、カーネルは、割り込まれたプログラムを再開する前に、すべてのレジスタを元に戻さないといけないためです。これは、カーネルがSIMDの状態のすべてを、システムコールやハードウェア割り込みがあるたびにメインメモリに保存しないといけないということを意味します。SIMDの状態情報はとても巨大(512〜1600 bytes)で、割り込みは非常に頻繁に起こるかもしれないので、保存・復元の操作がこのように追加されるのは性能にかなりの悪影響を及ぼします。これを避けるために、(カーネルの上で走っているアプリケーションではなく!)カーネル上でSIMDを無効化するのです。 [Single Instruction Multiple Data (SIMD)]: https://ja.wikipedia.org/wiki/SIMD SIMDを無効化することによる問題に、`x86_64`における浮動小数点演算は標準ではSIMDレジスタを必要とするということがあります。この問題を解決するため、`soft-float`機能を追加します。これは、すべての浮動小数点演算を通常の整数に基づいたソフトウェア上の関数を使ってエミュレートするというものです。 より詳しくは、[SIMDを無効化する](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md)ことに関する私達の記事を読んでください。 ```json "rustc-abi": "x86-softfloat" ``` As we want to use the `soft-float` feature, we also need to tell the Rust compiler `rustc` that we want to use the corresponding ABI. We can do that by setting the `rustc-abi` field to `x86-softfloat`. #### まとめると 私達のターゲット仕様ファイルは今このようになっているはずです。 ```json { "llvm-target": "x86_64-unknown-none", "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", "arch": "x86_64", "target-endian": "little", "target-pointer-width": 64, "target-c-int-width": 32, "os": "none", "executables": true, "linker-flavor": "ld.lld", "linker": "rust-lld", "panic-strategy": "abort", "disable-redzone": true, "features": "-mmx,-sse,+soft-float", "rustc-abi": "x86-softfloat" } ``` ### カーネルをビルドする 私達の新しいターゲットのコンパイルにはLinuxの慣習に倣います(理由は知りません、LLVMのデフォルトであるというだけではないでしょうか)。つまり、[前の記事][previous post]で説明したように`_start`という名前のエントリポイントが要るということです。 [previous post]: @/edition-2/posts/01-freestanding-rust-binary/index.ja.md ```rust // src/main.rs #![no_std] // don't link the Rust standard library #![no_main] // disable all Rust-level entry points use core::panic::PanicInfo; /// This function is called on panic. #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } #[unsafe(no_mangle)] // don't mangle the name of this function pub extern "C" fn _start() -> ! { // this function is the entry point, since the linker looks for a function // named `_start` by default loop {} } ``` ホストOSが何であるかにかかわらず、エントリポイントは`_start`という名前でなければならないことに注意してください。 これで、私達の新しいターゲットのためのカーネルを、JSONファイル名を`--target`として渡すことでビルドできるようになりました。 ``` > cargo build --target x86_64-blog_os.json error[E0463]: can't find crate for `core` ``` 失敗しましたね!エラーはRustコンパイラが[`core`ライブラリ][`core` library]を見つけられなくなったと言っています。このライブラリは、`Result` や `Option`、イテレータのような基本的なRustの型を持っており、暗黙のうちにすべての`no_std`なクレートにリンクされています。 [`core` library]: https://doc.rust-lang.org/nightly/core/index.html 問題は、coreライブラリはRustコンパイラと一緒にコンパイル済みライブラリとして配布されているということです。そのため、これは、私達独自のターゲットではなく、サポートされているhost triple(例えば `x86_64-unknown-linux-gnu`)でのみ使えるのです。他のターゲットのためにコードをコンパイルしたいときには、`core`をそれらのターゲットに向けて再コンパイルする必要があります。 #### `build-std`オプション ここでcargoの[`build-std`機能][`build-std` feature]の出番です。これを使うと`core`やその他の標準ライブラリクレートについて、Rustインストール時に一緒についてくるコンパイル済みバージョンを使う代わりに、必要に応じて再コンパイルすることができます。これはとても新しくまだ完成していないので、不安定機能とされており、[nightly Rustコンパイラ][nightly Rust compilers]でのみ利用可能です。 [`build-std` feature]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std [nightly Rust compilers]: #installing-rust-nightly この機能を使うためには、[cargoの設定][cargo configuration]ファイルを`.cargo/config.toml`に作り、次の内容を書きましょう。 ```toml # in .cargo/config.toml [unstable] build-std = ["core", "compiler_builtins"] ``` これはcargoに`core`と`compiler_builtins`ライブラリを再コンパイルするよう命令します。後者が必要なのは`core`がこれに依存しているためです。 これらのライブラリを再コンパイルするためには、cargoがRustのソースコードにアクセスできる必要があります。これは`rustup component add rust-src`でインストールできます。