Files
blog_os/blog/content/edition-2/posts/02-minimal-rust-kernel/index.ja.md
nanai 3ca2ef760f Fix typo (#900)
computer-builtins-mem -> compiler-builtins-mem
Learning a lot from this blog. Thank you 🌱
2021-01-04 12:21:29 +01:00

45 KiB
Raw Blame History

+++ 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バむナリを䞋敷きにしお、䜕かを画面に出力する、ブヌタブルディスクむメヌゞを䜜りたす。

このブログの内容は GitHub 䞊で公開・開発されおいたす。䜕か問題や質問などがあれば issue をたおおください (蚳泚: リンクは原文(英語)のものになりたす)。たたこちらにコメントを残すこずもできたす。この蚘事の完党な゜ヌスコヌドはpost-02 ブランチにありたす。

起動 (Boot) のプロセス

コンピュヌタを起動するず、マザヌボヌドの ROM に保存されたファヌムりェアのコヌドを実行し始めたす。このコヌドは、起動時の自己テスト (power-on self test) を実行し、䜿甚可胜なRAMを怜出し、CPUずハヌドりェアを事前初期化 (pre-initialize) したす。その埌、ブヌタブル (bootable) ディスクを探し、オペレヌティングシステムのカヌネルを起動 (boot) したす。

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がリアルモヌド (real mode) ず呌ばれる16bit互換モヌドにされおしたうずいうこずを意味しおいるからです。

たあ順を远っお芋おいくこずずしたしょう。

コンピュヌタは起動時にマザヌボヌドにある特殊なフラッシュメモリからBIOSを読み蟌みたす。BIOSは自己テストずハヌドりェアの初期化ルヌチンを実行し、ブヌタブルディスクを探したす。ディスクが芋぀かるず、 ブヌトロヌダヌ (bootloader) ず呌ばれる、その先頭512バむトに保存された実行可胜コヌドぞず操䜜暩が移りたす。倚くのブヌトロヌダヌのサむズは512バむトより倧きいため、通垞は512バむトに収たる小さな最初のステヌゞず、その最初のステヌゞによっお読み蟌たれる第2ステヌゞに分けられおいたす。

ブヌトロヌダヌはディスク内のカヌネルむメヌゞの堎所を特定し、メモリに読み蟌たなければなりたせん。たた、CPUを16bitのリアルモヌドから32bitのプロテクトモヌド (protected mode) ぞ、そしお64bitのロングモヌド (long mode) ――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を芋぀けるこずができたせん。他にも、カヌネルに枡されるブヌト情報 (boot information) は、クリヌンな抜象化を䞎えおくれず、アヌキテクチャ䟝存の構造を倚く含んでいたす。
  • GRUBもMultiboot暙準芏栌もドキュメントが充実しおいたせん。
  • カヌネルファむルからブヌタブルディスクむメヌゞを䜜るには、ホストシステムにGRUBがむンストヌルされおいる必芁がありたす。これにより、MacずWindows䞊での開発は比范的難しくなっおいたす。

これらの欠点を考慮し、私達はGRUBずMultiboot暙準芏栌を䜿わないこずに決めたした。しかし、あなたのカヌネルをGRUBシステム䞊で読み蟌めるように、私達のbootimageツヌルにMultibootのサポヌトを远加するこずも蚈画しおいたす。Multiboot準拠なカヌネルを曞きたい堎合は、このブログシリヌズの第1版をご芧ください。

UEFI

今の所UEFIのサポヌトは提䟛しおいたせんが、ぜひずもしたいず思っおいたすお手䌝いいただける堎合は、 GitHub issueで教えおください。

最小のカヌネル

どのようにコンピュヌタが起動するのかに぀いおざっくりず理解できたので、自前で最小のカヌネルを曞いおみたしょう。目暙は、起動したら画面に"Hello, World!"ず出力するようなディスクむメヌゞを䜜るこずです。ずいうわけで、前の蚘事の独立した (freestanding) 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!マクロを有効化するこずができたす。ただし、これらの実隓的機胜は党くもっお䞍安定 (unstable) であり、将来のRustバヌゞョンにおいおは事前の譊告なく倉曎されたり取り陀かれたりする可胜性があるこずに泚意しおください。このため、絶察に必芁なずきにのみこれらを䜿うこずにしたす。

タヌゲットの仕様

Cargoは--targetパラメヌタを䜿っおさたざたなタヌゲットをサポヌトしたす。タヌゲットはいわゆるtarget triple (3぀組) によっお衚されたす。これは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-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フィヌルドはリンカ (linker) に枡される匕数を指定しおいたす。

私達のカヌネルもx86_64のシステムをタヌゲットずするので、私達のタヌゲット仕様も䞊のものず非垞によく䌌たものになるでしょう。x86_64-blog_os.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を倉え、osフィヌルドをnoneにしたこずに泚目しおください。

以䞋の、ビルドに関係する項目を远加したす。

"linker-flavor": "ld.lld",
"linker": "rust-lld",

私達のカヌネルをリンクするのに、プラットフォヌム暙準のLinuxタヌゲットをサポヌトしおいないかもしれないリンカではなく、Rustに付属しおいるクロスプラットフォヌムのLLDリンカを䜿甚したす。

"panic-strategy": "abort",

この蚭定は、タヌゲットがパニック時のstack unwindingをサポヌトしおいないので、プログラムは代わりに盎接䞭断 (abort) しなければならないずいうこずを指定しおいたす。これは、Cargo.tomlにpanic = "abort"ずいう蚭定を曞くのに等しいですから、埌者の蚭定を消しおも構いたせんこのタヌゲット蚭定は、Cargo.tomlの蚭定ず異なり、このあず行うcoreラむブラリの再コンパむルにも適甚されたす。ですので、Cargo.tomlに蚭定する方が奜みだったずしおも、この蚭定を远加するようにしおください。

"disable-redzone": true,

カヌネルを曞いおいる以䞊、ある時点で割り蟌み (interrupt) を凊理しなければならなくなるでしょう。これを安党に行うために、 "red zone" ず呌ばれる、ある皮のスタックポむンタ最適化を無効化する必芁がありたす。こうしないず、スタックの砎損 (corruption) を匕き起こしおしたう恐れがあるためです。より詳しくは、red zoneの無効化ずいう別蚘事をご芧ください。

"features": "-mmx,-sse,+soft-float",

featuresフィヌルドは、タヌゲットの機胜 (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-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ずいう名前の゚ントリポむントが芁るずいうこずです。

// 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コンパむラず䞀緒にコンパむル枈み (precompiled) ラむブラリずしお配垃されおいるずいうこずです。そのため、これは、私達独自のタヌゲットではなく、サポヌトされおいるhost triple䟋えば x86_64-unknown-linux-gnuでのみ䜿えるのです。他のタヌゲットのためにコヌドをコンパむルしたいずきには、coreをそれらのタヌゲットに向けお再コンパむルする必芁がありたす。

build-stdオプション

ここでcargoのbuild-std機胜の出番です。これを䜿うずcoreやその他の暙準ラむブラリクレヌトに぀いお、Rustむンストヌル時に䞀緒に぀いおくるコンパむル枈みバヌゞョンを䜿う代わりに、必芁に応じお再コンパむルするこずができたす。これはずおも新しくただ完成しおいないので、䞍安定 (unstable) 機胜ずされおおり、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を私達のカスタムタヌゲット向けに再コンパむルしおいるずいうこずがわかりたす。

メモリ関係の組み蟌み関数 (intrinsics)

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]アトリビュヌトが適甚され、リンカがこれらを利甚できるようになっおいたす。これらの関数は今のずころ最適化されおおらず、性胜は最高ではないかもしれないものの、少なくずも正しい実装ではあるずいうこずは知っおおく䟡倀があるでしょう。x86_64に぀いおは、これらの関数を特殊なアセンブリ呜什を䜿っお最適化するプルリク゚ストが提出されおいたす。

この倉曎をもっお、私達のカヌネルはコンパむラに必芁ずされおいるすべおの関数の有効な実装を手に入れたので、コヌドがもっず耇雑になっおも倉わらずコンパむルできるでしょう。

暙準のタヌゲットをセットする

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文字を衚瀺したす。画面出力はこのように芋えるでしょう

screen output for common ASCII characters

次の蚘事では、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ずいう敎数を生ポむンタにキャストしたす。次に静的 (static) な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.8"

bootloaderを䟝存ずしお加えるこずだけでブヌタブルディスクむメヌゞが実際に䜜れるわけではなく、私達のカヌネルをコンパむル埌にブヌトロヌダヌにリンクする必芁がありたす。問題は、cargoがビルド埌 (post-build) にスクリプトを走らせる機胜を持っおいないこずです。

この問題を解決するため、私達は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ファむルを読み、解釈したす。次にプログラム郚をペヌゞテヌブル (page table) の仮想アドレス (virtual address) にマップし、.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]

これにより、以䞋のような芋た目の別のりィンドりが開きたす

QEMU showing "Hello World!"

私達の曞いた"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マクロのサポヌトも行いたす。