Rustでつくる最小のカーネル
この記事は翻訳されたものです: この記事はA Minimal Rust Kernelをコミュニティの手により翻訳したものです。そのため、翻訳が完全・最新でなかったり、原文にない誤りを含んでいる可能性があります。問題があればこのissue上で報告してください!
翻訳者: @swnakamura 及び @JohnTitor.
この記事では、Rustで最小限の64bitカーネルを作ります。前の記事で作ったフリースタンディングなRustバイナリを下敷きにして、何かを画面に出力する、ブータブルディスクイメージを作ります。
このブログの内容は GitHub 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。またこちらにコメントを残すこともできます。この記事の完全なソースコードはpost-02
ブランチにあります。
目次
🔗起動のプロセス
コンピュータを起動すると、マザーボードの ROM に保存されたファームウェアのコードを実行し始めます。このコードは、起動時の自己テストを実行し、使用可能なRAMを検出し、CPUとハードウェアを事前初期化します。その後、ブータブルディスクを探し、オペレーティングシステムのカーネルを起動します。
x86には2つのファームウェアの標準規格があります:“Basic Input/Output System” (BIOS) と、より新しい “Unified Extensible Firmware Interface” (UEFI) です。BIOS規格は古く時代遅れですが、シンプルでありすべてのx86のマシンで1980年代からよくサポートされています。対して、UEFIはより現代的でずっと多くの機能を持っていますが、セットアップが複雑です(少なくとも私はそう思います)。
今の所、このブログではBIOSしかサポートしていませんが、UEFIのサポートも計画中です。お手伝いいただける場合は、GitHubのissueをご覧ください。
🔗BIOSの起動
ほぼすべてのx86システムがBIOSによる起動をサポートしています。これは近年のUEFIベースのマシンも例外ではなく、それらはエミュレートされたBIOSを使います。前世紀のすべてのマシンにも同じブートロジックが使えるなんて素晴らしいですね。しかし、この広い互換性は、BIOSによる起動の最大の欠点でもあるのです。というのもこれは、1980年代の化石のようなブートローダーを動かすために、CPUがリアルモードと呼ばれる16bit互換モードにされてしまうということを意味しているからです。
まあ順を追って見ていくこととしましょう。
コンピュータは起動時にマザーボードにある特殊なフラッシュメモリからBIOSを読み込みます。BIOSは自己テストとハードウェアの初期化ルーチンを実行し、ブータブルディスクを探します。ディスクが見つかると、 ブートローダー と呼ばれる、その先頭512バイトに保存された実行可能コードへと操作権が移ります。多くのブートローダーのサイズは512バイトより大きいため、通常は512バイトに収まる小さな最初のステージと、その最初のステージによって読み込まれる第2ステージに分けられています。
ブートローダーはディスク内のカーネルイメージの場所を特定し、メモリに読み込まなければなりません。また、CPUを16bitのリアルモードから32bitのプロテクトモードへ、そして64bitのロングモード――64bitレジスタとすべてのメインメモリが利用可能になります――へと変更しなければなりません。3つ目の仕事は、特定の情報(例えばメモリーマップなどです)をBIOSから聞き出し、OSのカーネルに渡すことです。
ブートローダーを書くのにはアセンブリ言語を必要とするうえ、「何も考えずにプロセッサーのこのレジスタにこの値を書き込んでください」のような勉強の役に立たない作業がたくさんあるので、ちょっと面倒くさいです。ですのでこの記事ではブートローダーの制作については飛ばして、代わりにbootimageという、自動でカーネルの前にブートローダを置いてくれるツールを使いましょう。
自前のブートローダーを作ることに興味がある人もご期待下さい、これに関する記事も計画中です!
🔗Multiboot標準規格
すべてのオペレーティングシステムが、自身にのみ対応しているブートローダーを実装するということを避けるために、1995年にフリーソフトウェア財団がMultibootというブートローダーの公開標準規格を策定しています。この標準規格では、ブートローダーとオペレーティングシステムのインターフェースが定義されており、Multibootに準拠したブートローダーであれば、同じくそれに準拠したすべてのオペレーティングシステムが読み込めるようになっています。そのリファレンス実装として、Linuxシステムで一番人気のブートローダーであるGNU GRUBがあります。
カーネルをMultibootに準拠させるには、カーネルファイルの先頭にいわゆるMultiboot headerを挿入するだけで済みます。このおかげで、OSをGRUBで起動するのはとても簡単です。しかし、GRUBとMultiboot標準規格にはいくつか問題もあります:
- これらは32bitプロテクトモードしかサポートしていません。そのため、64bitロングモードに変更するためのCPUの設定は依然行う必要があります。
- これらは、カーネルではなくブートローダーがシンプルになるように設計されています。例えば、カーネルは通常とは異なるデフォルトページサイズでリンクされる必要があり、そうしないとGRUBはMultiboot headerを見つけることができません。他にも、カーネルに渡されるブート情報は、クリーンな抽象化を与えてくれず、アーキテクチャ依存の構造を多く含んでいます。
- GRUBもMultiboot標準規格もドキュメントが充実していません。
- カーネルファイルからブータブルディスクイメージを作るには、ホストシステムにGRUBがインストールされている必要があります。これにより、MacとWindows上での開発は比較的難しくなっています。
これらの欠点を考慮し、私達はGRUBとMultiboot標準規格を使わないことに決めました。しかし、あなたのカーネルをGRUBシステム上で読み込めるように、私達のbootimageツールにMultibootのサポートを追加することも計画しています。Multiboot準拠なカーネルを書きたい場合は、このブログシリーズの第1版をご覧ください。
🔗UEFI
(今の所UEFIのサポートは提供していませんが、ぜひともしたいと思っています!お手伝いいただける場合は、 GitHub issueで教えてください。)
🔗最小のカーネル
どのようにコンピュータが起動するのかについてざっくりと理解できたので、自前で最小のカーネルを書いてみましょう。目標は、起動したら画面に“Hello, World!“と出力するようなディスクイメージを作ることです。というわけで、前の記事の独立したRustバイナリをもとにして作っていきます。
覚えていますか、この独立したバイナリはcargo
を使ってビルドしましたが、オペレーティングシステムに依って異なるエントリポイント名とコンパイルフラグが必要なのでした。これはcargo
は標準では ホストシステム(あなたの使っているシステム)向けにビルドするためです。例えばWindows上で走るカーネルというのはあまり意味がなく、私達の望む動作ではありません。代わりに、明確に定義された ターゲットシステム 向けにコンパイルできると理想的です。
🔗RustのNightly版をインストールする
Rustにはstable、beta、nightlyの3つのリリースチャンネルがあります。Rust Bookはこれらの3つのチャンネルの違いをとても良く説明しているので、一度確認してみてください。オペレーティングシステムをビルドするには、nightlyチャンネルでしか利用できないいくつかの実験的機能を使う必要があるので、Rustのnightly版をインストールすることになります。
Rustの実行環境を管理するのには、rustupを強くおすすめします。nightly、beta、stable版のコンパイラをそれぞれインストールすることができますし、アップデートするのも簡単です。現在のディレクトリにnightlyコンパイラを使うようにするには、rustup override set nightly
と実行してください。もしくは、rust-toolchain
というファイルにnightly
と記入してプロジェクトのルートディレクトリに置くことでも指定できます。Nightly版を使っていることは、rustc --version
と実行することで確かめられます。表示されるバージョン名の末尾に-nightly
とあるはずです。
nightlyコンパイラでは、いわゆるfeature flagをファイルの先頭につけることで、いろいろな実験的機能を使うことを選択できます。例えば、#![feature(asm)]
をmain.rs
の先頭につけることで、インラインアセンブリのための実験的なasm!
マクロを有効化することができます。ただし、これらの実験的機能は全くもって不安定であり、将来のRustバージョンにおいては事前の警告なく変更されたり取り除かれたりする可能性があることに注意してください。このため、絶対に必要なときにのみこれらを使うことにします。
🔗ターゲットの仕様
Cargoは--target
パラメータを使ってさまざまなターゲットをサポートします。ターゲットはいわゆるtarget tripleによって表されます。これはCPUアーキテクチャ、製造元、オペレーティングシステム、そしてABIを表します。例えば、x86_64-unknown-linux-gnu
というtarget tripleは、x86_64
のCPU、製造元不明、GNU ABIのLinuxオペレーティングシステム向けのシステムを表します。Rustは多くのtarget tripleをサポートしており、その中にはAndroidのためのarm-linux-androideabi
やWebAssemblyのためのwasm32-unknown-unknown
などがあります。
しかしながら、私達のターゲットシステムには、いくつか特殊な設定パラメータが必要になります(例えば、その下ではOSが走っていない、など)。なので、既存のtarget tripleはどれも当てはまりません。ありがたいことに、RustではJSONファイルを使って独自のターゲットを定義できます。例えば、x86_64-unknown-linux-gnu
というターゲットを表すJSONファイルはこんな感じです。
{
"llvm-target": "x86_64-unknown-linux-gnu",
"data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128",
"arch": "x86_64",
"target-endian": "little",
"target-pointer-width": "64",
"target-c-int-width": "32",
"os": "linux",
"executables": true,
"linker-flavor": "gcc",
"pre-link-args": ["-m64"],
"morestack": false
}
ほとんどのフィールドはLLVMがそのプラットフォーム向けのコードを生成するために必要なものです。例えば、data-layout
フィールドは種々の整数、浮動小数点数、ポインタ型の大きさを定義しています。次に、target-pointer-width
のような、条件付きコンパイルに用いられるフィールドがあります。第3の種類のフィールドはクレートがどのようにビルドされるべきかを定義します。例えば、pre-link-args
フィールドはリンカに渡される引数を指定しています。
私達のカーネルもx86_64
のシステムをターゲットとするので、私達のターゲット仕様も上のものと非常によく似たものになるでしょう。x86_64-blog_os.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
にしたことに注目してください。
以下の、ビルドに関係する項目を追加します。
"linker-flavor": "ld.lld",
"linker": "rust-lld",
私達のカーネルをリンクするのに、プラットフォーム標準の(Linuxターゲットをサポートしていないかもしれない)リンカではなく、Rustに付属しているクロスプラットフォームのLLDリンカを使用します。
"panic-strategy": "abort",
この設定は、ターゲットがパニック時のstack unwindingをサポートしていないので、プログラムは代わりに直接中断しなければならないということを指定しています。これは、Cargo.tomlにpanic = "abort"
という設定を書くのに等しいですから、後者の設定を消しても構いません(このターゲット設定は、Cargo.tomlの設定と異なり、このあと行うcore
ライブラリの再コンパイルにも適用されます。ですので、Cargo.tomlに設定する方が好みだったとしても、この設定を追加するようにしてください)。
"disable-redzone": true,
カーネルを書いている以上、ある時点で割り込みを処理しなければならなくなるでしょう。これを安全に行うために、 “red zone” と呼ばれる、ある種のスタックポインタ最適化を無効化する必要があります。こうしないと、スタックの破損を引き起こしてしまう恐れがあるためです。より詳しくは、red zoneの無効化という別記事をご覧ください。
"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を無効化するのです。
SIMDを無効化することによる問題に、x86_64
における浮動小数点演算は標準ではSIMDレジスタを必要とするということがあります。この問題を解決するため、soft-float
機能を追加します。これは、すべての浮動小数点演算を通常の整数に基づいたソフトウェア上の関数を使ってエミュレートするというものです。
より詳しくは、SIMDを無効化することに関する私達の記事を読んでください。
🔗まとめると
私達のターゲット仕様ファイルは今このようになっているはずです。
{
"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"
}
🔗カーネルをビルドする
私達の新しいターゲットのコンパイルにはLinuxの慣習に倣います(理由は知りません、LLVMのデフォルトであるというだけではないでしょうか)。つまり、前の記事で説明したように_start
という名前のエントリポイントが要るということです。
// 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
ライブラリを見つけられなくなったと言っています。このライブラリは、Result
や Option
、イテレータのような基本的なRustの型を持っており、暗黙のうちにすべてのno_std
なクレートにリンクされています。
問題は、coreライブラリはRustコンパイラと一緒にコンパイル済みライブラリとして配布されているということです。そのため、これは、私達独自のターゲットではなく、サポートされているhost triple(例えば x86_64-unknown-linux-gnu
)でのみ使えるのです。他のターゲットのためにコードをコンパイルしたいときには、core
をそれらのターゲットに向けて再コンパイルする必要があります。
🔗build-std
オプション
ここでcargoのbuild-std
機能の出番です。これを使うとcore
やその他の標準ライブラリクレートについて、Rustインストール時に一緒についてくるコンパイル済みバージョンを使う代わりに、必要に応じて再コンパイルすることができます。これはとても新しくまだ完成していないので、不安定機能とされており、nightly Rustコンパイラでのみ利用可能です。
この機能を使うためには、cargoの設定ファイルを.cargo/config.toml
に作り、次の内容を書きましょう。
# in .cargo/config.toml
[unstable]
build-std = ["core", "compiler_builtins"]
これはcargoにcore
とcompiler_builtins
ライブラリを再コンパイルするよう命令します。後者が必要なのはcore
がこれに依存しているためです。 これらのライブラリを再コンパイルするためには、cargoがRustのソースコードにアクセスできる必要があります。これはrustup component add rust-src
でインストールできます。
注意: unstable.build-std
設定キーを使うには、少なくとも2020-07-15以降のRust nightlyが必要です。
unstable.build-std
設定キーをセットし、rust-src
コンポーネントをインストールしたら、ビルドコマンドをもう一度実行しましょう。
> cargo build --target x86_64-blog_os.json
Compiling core v0.0.0 (/…/rust/src/libcore)
Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
Compiling compiler_builtins v0.1.32
Compiling blog_os v0.1.0 (/…/blog_os)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
今回は、cargo build
がcore
、rustc-std-workspace-core
(compiler_builtins
の依存です)、そして compiler_builtins
を私達のカスタムターゲット向けに再コンパイルしているということがわかります。
🔗メモリ関係の組み込み関数
Rustコンパイラは、すべてのシステムにおいて、特定の組み込み関数が利用可能であるということを前提にしています。それらの関数の多くは、私達がちょうど再コンパイルしたcompiler_builtins
クレートによって提供されています。しかしながら、通常システムのCライブラリによって提供されているので標準では有効化されていない、メモリ関係の関数がいくつかあります。それらの関数には、メモリブロック内のすべてのバイトを与えられた値にセットするmemset
、メモリーブロックを他のブロックへとコピーするmemcpy
、2つのメモリーブロックを比較するmemcmp
などがあります。これらの関数はどれも、現在の段階で我々のカーネルをコンパイルするのに必要というわけではありませんが、コードを追加していくとすぐに必要になるでしょう(たとえば、構造体をコピーする、など)。
オペレーティングシステムのCライブラリにリンクすることはできませんので、これらの関数をコンパイラに与えてやる別の方法が必要になります。このための方法として考えられるものの一つが、自前でmemset
を実装し、(コンパイル中の自動リネームを防ぐため)#[no_mangle]
アトリビュートをこれらに適用することでしょう。しかし、こうすると、これらの関数の実装のちょっとしたミスが未定義動作に繋がりうるため危険です。たとえば、for
ループを使ってmemcpy
を実装すると無限再帰を起こしてしまうかもしれません。なぜなら、for
ループは暗黙のうちにIntoIterator::into_iter
トレイトメソッドを呼び出しており、これがmemcpy
を再び呼び出しているかもしれないためです。なので、代わりに既存のよくテストされた実装を再利用するのが良いでしょう。
ありがたいことに、compiler_builtins
クレートにはこれらの必要な関数すべての実装が含まれており、標準ではCライブラリの実装と競合しないように無効化されているだけなのです。これはcargoのbuild-std-features
フラグを["compiler-builtins-mem"]
に設定することで有効化できます。build-std
フラグと同じように、このフラグはコマンドラインで-Z
フラグとして渡すこともできれば、.cargo/config.toml
ファイルのunstable
テーブルで設定することもできます。ビルド時は常にこのフラグをセットしたいので、設定ファイルを使う方が良いでしょう:
# in .cargo/config.toml
[unstable]
build-std-features = ["compiler-builtins-mem"]
(compiler-builtins-mem
機能のサポートが追加されたのはつい最近なので、2019-09-30
以降のRust nightlyが必要です。)
このとき、裏でcompiler_builtins
クレートのmem
機能が有効化されています。これにより、このクレートのmemcpy
などの実装に#[no_mangle]
アトリビュートが適用され、リンカがこれらを利用できるようになっています。
この変更をもって、私達のカーネルはコンパイラに必要とされているすべての関数の有効な実装を手に入れたので、コードがもっと複雑になっても変わらずコンパイルできるでしょう。
🔗標準のターゲットをセットする
cargo build
を呼び出すたびに--target
パラメータを渡すのを避けるために、デフォルトのターゲットを書き換えることができます。これをするには、以下を.cargo/config.toml
のcargo設定ファイルに付け加えます:
# in .cargo/config.toml
[build]
target = "x86_64-blog_os.json"
これは、明示的に--target
引数が渡されていないときは、x86_64-blog_os.json
ターゲットを使うようにcargo
に命令します。つまり、私達はカーネルをシンプルなcargo build
コマンドでビルドできるということです。cargoの設定のオプションについてより詳しく知るには、公式のドキュメントを読んでください。
これにより、シンプルなcargo build
コマンドで、ベアメタルのターゲットに私達のカーネルをビルドできるようになりました。しかし、ブートローダーによって呼び出される私達の_start
エントリポイントはまだ空っぽです。そろそろここから何かを画面に出力してみましょう。
🔗画面に出力する
現在の段階で画面に文字を出力する最も簡単な方法はVGAテキストバッファです。これは画面に出力されている内容を保持しているVGAハードウェアにマップされた特殊なメモリです。通常、これは25行からなり、それぞれの行は80文字セルからなります。それぞれの文字セルは、背景色と前景色付きのASCII文字を表示します。画面出力はこのように見えるでしょう:
次の記事では、VGAバッファの正確なレイアウトについて議論し、このためのちょっとしたドライバも書きます。“Hello World!“を出力するためには、バッファがアドレス0xb8000
にあり、それぞれの文字セルはASCIIのバイトと色のバイトからなることを知っている必要があります。
実装はこんな感じになります:
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
という整数を生ポインタにキャストします。次に静的なHELLO
というバイト列変数の要素に対しイテレートします。enumerate
メソッドを使うことで、for
ループの実行回数を表す変数 i
も取得します。ループの内部では、offset
メソッドを使って文字列のバイトと対応する色のバイト(0xb
は明るいシアン色)を書き込んでいます。
すべてのメモリへの書き込み処理のコードを、unsafe
ブロックが囲んでいることに注意してください。この理由は、私達の作った生ポインタが正しいものであることをRustコンパイラが証明できないためです。生ポインタはどんな場所でも指しうるので、データの破損につながるかもしれません。これらの操作をunsafe
ブロックに入れることで、私達はこれが正しいことを確信しているとコンパイラに伝えているのです。ただし、unsafe
ブロックはRustの安全性チェックを消すわけではなく、追加で5つのことができるようになるだけということに注意してください。
訳注: 翻訳時点(2020-10-20)では、リンク先のThe Rust book日本語版には「追加でできるようになること」は4つしか書かれていません。
強調しておきたいのですが、 このような機能はRustでプログラミングするときに使いたいものではありません! unsafeブロック内で生ポインタを扱うと非常にしくじりやすいです。たとえば、注意不足でバッファの終端のさらに奥に書き込みを行ってしまったりするかもしれません。
ですので、unsafe
の使用は最小限にしたいです。これをするために、Rustでは安全なabstractionを作ることができます。たとえば、VGAバッファ型を作り、この中にすべてのunsafeな操作をカプセル化し、外側からの誤った操作が不可能であることを保証できるでしょう。こうすれば、unsafe
の量を最小限にでき、メモリ安全性を侵していないことを確かにできます。そのような安全なVGAバッファの abstraction を次の記事で作ります。
🔗カーネルを実行する
では、目で見て分かる処理を行う実行可能ファイルを手に入れたので、実行してみましょう。まず、コンパイルした私達のカーネルを、ブートローダーとリンクすることによってブータブルディスクイメージにする必要があります。そして、そのディスクイメージを、QEMUバーチャルマシン内や、USBメモリを使って実際のハードウェア上で実行できます。
🔗ブートイメージを作る
コンパイルされた私達のカーネルをブータブルディスクイメージに変えるには、ブートローダーとリンクする必要があります。起動のプロセスのセクションで学んだように、ブートローダーはCPUを初期化しカーネルをロードする役割があります。
自前のブートローダーを書くと、それだけで1つのプロジェクトになってしまうので、代わりにbootloader
クレートを使いましょう。このクレートは、Cに依存せず、Rustとインラインアセンブリだけで基本的なBIOSブートローダーを実装しています。私達のカーネルを起動するためにこれを依存関係に追加する必要があります:
# in Cargo.toml
[dependencies]
bootloader = "0.9"
bootloaderを依存として加えることだけでブータブルディスクイメージが実際に作れるわけではなく、私達のカーネルをコンパイル後にブートローダーにリンクする必要があります。問題は、cargoがビルド後にスクリプトを走らせる機能を持っていないことです。
この問題を解決するため、私達は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ファイルを読み、解釈します。次にプログラム部をページテーブルの仮想アドレスにマップし、.bss
部をゼロにし、スタックをセットアップします。最後に、エントリポイントのアドレス(私達の_start
関数)を読み、そこにジャンプします。
🔗QEMUで起動する
これで、ディスクイメージを仮想マシンで起動できます。QEMUを使ってこれを起動するには、以下のコマンドを実行してください:
> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
これにより、以下のような見た目の別のウィンドウが開きます:
私達の書いた“Hello World!“が画面に見えますね。
🔗実際のマシン
USBメモリにこれを書き込んで実際のマシン上で起動することも可能です:
> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync
sdX
はあなたのUSBメモリのデバイス名です。そのデバイス上のすべてのデータが上書きされてしまうので、 正しいデバイス名を選んでいるのかよく確認してください 。
イメージをUSBメモリに書き込んだあとは、そこから起動することによって実際のハードウェア上で走らせることができます。特殊なブートメニューを使ったり、BIOS設定で起動時の優先順位を変え、USBメモリから起動することを選択する必要があるでしょう。ただし、bootloader
クレートはUEFIをサポートしていないので、UEFIマシン上ではうまく動作しないということに注意してください。
🔗cargo run
を使う
QEMU上でより簡単に私達のカーネルを走らせるために、cargoのrunner
設定が使えます。
# 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のドキュメントを読んでください。
bootimage runner
コマンドは、runner
キーとして実行するために設計されています。このコマンドは、与えられた実行ファイルをプロジェクトの依存するbootloaderとリンクして、QEMUを立ち上げます。より詳しく知りたいときや、設定オプションについてはbootimage
のReadmeを読んでください。
これで、cargo run
を使ってカーネルをコンパイルしQEMU内で起動することができます。
🔗次は?
次の記事では、VGAテキストバッファをより詳しく学び、そのための安全なインターフェースを書きます。また、println
マクロのサポートも行います。
コメント
Do you have a problem, want to share feedback, or discuss further ideas? Feel free to leave a comment here! Please stick to English and follow Rust's code of conduct. This comment thread directly maps to a discussion on GitHub, so you can also comment there if you prefer.
Instead of authenticating the giscus application, you can also comment directly on GitHub.
可能な限りコメントは英語で残すようにしてください。