フリースタンディングな Rust バイナリ
この記事は翻訳されたものです: この記事はA Freestanding Rust Binaryをコミュニティの手により翻訳したものです。そのため、翻訳が完全・最新でなかったり、原文にない誤りを含んでいる可能性があります。問題があればこのissue上で報告してください!
翻訳者: @JohnTitor.
私達自身のオペレーティングシステム(以下、OS)カーネルを作っていく最初のステップは標準ライブラリとリンクしない Rust の実行可能ファイルをつくることです。これにより、基盤となる OS がないベアメタル上で Rust のコードを実行することができるようになります。
このブログの内容は GitHub 上で公開・開発されています。何か問題や質問などがあれば issue をたててください (訳注: リンクは原文(英語)のものになります)。またこちらにコメントを残すこともできます。この記事の完全なソースコードはpost-01
ブランチにあります。
目次
🔗導入
OS カーネルを書くためには、いかなる OS の機能にも依存しないコードが必要となります。つまり、スレッドやヒープメモリ、ネットワーク、乱数、標準出力、その他 OS による抽象化や特定のハードウェアを必要とする機能は使えません。私達は自分自身で OS とそのドライバを書こうとしているので、これは理にかなっています。
これは Rust の標準ライブラリをほとんど使えないということを意味しますが、それでも私達が使うことのできる Rust の機能はたくさんあります。例えば、イテレータやクロージャ、パターンマッチング、 Option
や Result
型に文字列フォーマット、そしてもちろん所有権システムを使うことができます。これらの機能により、未定義動作やメモリ安全性を気にせずに、高い水準で表現力豊かにカーネルを書くことができます。
Rust で OS カーネルを書くには、基盤となる OS なしで動く実行環境をつくる必要があります。そのような実行環境はフリースタンディング環境やベアメタルのように呼ばれます。
この記事では、フリースタンディングな Rust のバイナリをつくるために必要なステップを紹介し、なぜそれが必要なのかを説明します。もし最小限の説明だけを読みたいのであれば 概要 まで飛ばしてください。
🔗標準ライブラリの無効化
デフォルトでは、全ての Rust クレートは標準ライブラリにリンクされています。標準ライブラリはスレッドやファイル、ネットワークのような OS の機能に依存しています。また OS と密接な関係にある C の標準ライブラリ(libc
)にも依存しています。私達の目的は OS を書くことなので、 OS 依存のライブラリを使うことはできません。そのため、 no_std
attribute を使って標準ライブラリが自動的にリンクされるのを無効にします。
新しい Cargo プロジェクトをつくるところから始めましょう。もっとも簡単なやり方はコマンドラインで以下を実行することです。
cargo new blog_os --bin --edition 2018
プロジェクト名を blog_os
としましたが、もちろんお好きな名前をつけていただいても大丈夫です。--bin
フラグは実行可能バイナリを作成することを、 --edition 2018
は2018エディションを使用することを明示的に指定します。コマンドを実行すると、 Cargoは以下のようなディレクトリ構造を作成します:
blog_os
├── Cargo.toml
└── src
└── main.rs
Cargo.toml
にはクレートの名前や作者名、セマンティックバージョニングに基づくバージョンナンバーや依存関係などが書かれています。src/main.rs
には私達のクレートのルートモジュールと main
関数が含まれています。cargo build
コマンドでこのクレートをコンパイルして、 target/debug
ディレクトリの中にあるコンパイルされた blog_os
バイナリを実行することができます。
🔗no_std
Attribute
今のところ私達のクレートは暗黙のうちに標準ライブラリをリンクしています。no_std
attributeを追加してこれを無効にしてみましょう:
// main.rs
#![no_std]
fn main() {
println!("Hello, world!");
}
(cargo build
を実行して)ビルドしようとすると、次のようなエラーが発生します:
error: cannot find macro `println!` in this scope
--> src/main.rs:4:5
|
4 | println!("Hello, world!");
| ^^^^^^^
これは println
マクロが標準ライブラリに含まれているためです。no_std
で標準ライブラリを無効にしたので、何かをプリントすることはできなくなりました。println
は標準出力に書き込むのでこれは理にかなっています。標準出力は OS によって提供される特別なファイル記述子であるためです。
では、 println
を削除し main
関数を空にしてもう一度ビルドしてみましょう:
// main.rs
#![no_std]
fn main() {}
> cargo build
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
この状態では #[panic_handler]
関数と language item
がないというエラーが発生します。
🔗Panic の実装
panic_handler
attribute はパニックが発生したときにコンパイラが呼び出す関数を定義します。標準ライブラリには独自のパニックハンドラー関数がありますが、 no_std
環境では私達の手でそれを実装する必要があります:
// in main.rs
use core::panic::PanicInfo;
/// この関数はパニック時に呼ばれる
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
PanicInfo
パラメータには、パニックが発生したファイルと行、およびオプションでパニックメッセージが含まれます。この関数は戻り値を取るべきではないので、“never” 型(!
)を返すことで発散する関数となります。今のところこの関数でできることは多くないので、無限にループするだけです。
🔗eh_personality
Language Item
language item はコンパイラによって内部的に必要とされる特別な関数や型です。例えば、Copy
トレイトはどの型がコピーセマンティクスを持っているかをコンパイラに伝える language item です。実装を見てみると、 language item として定義されている特別な #[lang = "copy"]
attribute を持っていることが分かります。
独自に language item を実装することもできますが、これは最終手段として行われるべきでしょう。というのも、language item は非常に不安定な実装であり型検査も行われないからです(なので、コンパイラは関数が正しい引数の型を取っているかさえ検査しません)。幸い、上記の language item のエラーを修正するためのより安定した方法があります。
eh_personality
language item はスタックアンワインド を実装するための関数を定義します。デフォルトでは、パニックが起きた場合には Rust はアンワインドを使用してすべてのスタックにある変数のデストラクタを実行します。これにより、使用されている全てのメモリが確実に解放され、親スレッドはパニックを検知して実行を継続できます。しかしアンワインドは複雑であり、いくつかの OS 特有のライブラリ(例えば、Linux では libunwind 、Windows では構造化例外)を必要とするので、私達の OS には使いたくありません。
🔗アンワインドの無効化
他にもアンワインドが望ましくないユースケースがあります。そのため、Rust には代わりにパニックで中止するオプションがあります。これにより、アンワインドのシンボル情報の生成が無効になり、バイナリサイズが大幅に削減されます。アンワインドを無効にする方法は複数あります。もっとも簡単な方法は、Cargo.toml
に次の行を追加することです:
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
これは dev プロファイル(cargo build
に使用される)と release プロファイル(cargo build --release
に使用される)の両方でパニックで中止するようにするための設定です。これで eh_personality
language item が不要になりました。
これで上の2つのエラーを修正しました。しかし、コンパイルしようとすると別のエラーが発生します:
> cargo build
error: requires `start` lang_item
私達のプログラムにはエントリポイントを定義する start
language item がありません。
🔗start
attribute
main
関数はプログラムを実行したときに最初に呼び出される関数であると思うかもしれません。しかし、ほとんどの言語にはランタイムシステムがあり、これはガベージコレクション(Java など)やソフトウェアスレッド(Go のゴルーチン)などを処理します。ランタイムは自身を初期化する必要があるため、main
関数の前に呼び出す必要があります。これにはスタック領域の作成と正しいレジスタへの引数の配置が含まれます。
標準ライブラリをリンクする一般的な Rust バイナリでは、crt0
(“C runtime zero”)と呼ばれる C のランタイムライブラリで実行が開始され、C アプリケーションの環境が設定されます。その後 C ランタイムは、start
language item で定義されている Rust ランタイムのエントリポイントを呼び出します。Rust にはごくわずかなランタイムしかありません。これは、スタックオーバーフローを防ぐ設定やパニック時のバックトレースの表示など、いくつかの小さな処理を行います。最後に、ランタイムは main
関数を呼び出します。
私達のフリースタンディングな実行可能ファイルは今のところ Rust ランタイムと crt0
へアクセスできません。なので、私達は自身でエントリポイントを定義する必要があります。start
language item を実装することは crt0
を必要とするのでここではできません。代わりに crt0
エントリポイントを直接上書きしなければなりません。
🔗エントリポイントの上書き
Rust コンパイラに通常のエントリポイントを使いたくないことを伝えるために、#![no_main]
attribute を追加します。
#![no_std]
#![no_main]
use core::panic::PanicInfo;
/// This function is called on panic.
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
main
関数を削除したことに気付いたかもしれません。main
関数を呼び出す基盤となるランタイムなしには置いていても意味がないからです。代わりに、OS のエントリポイントを独自の _start
関数で上書きしていきます:
#[no_mangle]
pub extern "C" fn _start() -> ! {
loop {}
}
Rust コンパイラが _start
という名前の関数を実際に出力するように、#[no_mangle]
attributeを用いて名前修飾を無効にします。この attribute がないと、コンパイラはすべての関数にユニークな名前をつけるために、 _ZN3blog_os4_start7hb173fedf945531caE
のようなシンボルを生成します。次のステップでエントリポイントとなる関数の名前をリンカに伝えるため、この属性が必要となります。
また、(指定されていない Rust の呼び出し規約の代わりに)この関数に C の呼び出し規約を使用するようコンパイラに伝えるために、関数を extern "C"
として定義する必要があります。_start
という名前をつける理由は、これがほとんどのシステムのデフォルトのエントリポイント名だからです。
戻り値の型である !
は関数が発散している、つまり値を返すことができないことを意味しています。エントリポイントはどの関数からも呼び出されず、OS またはブートローダから直接呼び出されるので、これは必須です。なので、値を返す代わりに、エントリポイントは例えば OS の exit
システムコールを呼び出します。今回はフリースタンディングなバイナリが返されたときマシンをシャットダウンするようにすると良いでしょう。今のところ、私達は無限ループを起こすことで要件を満たします。
cargo build
を実行すると、見づらいリンカエラーが発生します。
🔗リンカエラー
リンカは、生成されたコードを実行可能ファイルに紐付けるプログラムです。実行可能ファイルの形式は Linux や Windows、macOS でそれぞれ異なるため、各システムにはそれぞれ異なるエラーを発生させる独自のリンカがあります。エラーの根本的な原因は同じです。リンカのデフォルト設定では、プログラムが C ランタイムに依存していると仮定していますが、実際にはしていません。
エラーを回避するためにはリンカに C ランタイムに依存しないことを伝える必要があります。これはリンカに一連の引数を渡すか、ベアメタルターゲット用にビルドすることで可能となります。
🔗ベアメタルターゲット用にビルドする
デフォルトでは、Rust は現在のシステム環境に合った実行可能ファイルをビルドしようとします。例えば、x86_64
で Windows を使用している場合、Rust は x86_64
用の .exe
Windows 実行可能ファイルをビルドしようとします。このような環境は「ホスト」システムと呼ばれます。
様々な環境を表現するために、Rust は target triple という文字列を使います。rustc --version --verbose
を実行すると、ホストシステムの target triple を確認できます:
rustc 1.35.0-nightly (474e7a648 2019-04-07)
binary: rustc
commit-hash: 474e7a6486758ea6fc761893b1a49cd9076fb0ab
commit-date: 2019-04-07
host: x86_64-unknown-linux-gnu
release: 1.35.0-nightly
LLVM version: 8.0
上記の出力は x86_64
の Linux によるものです。host
は x86_64-unknown-linux-gnu
です。これには CPU アーキテクチャ(x86_64
)、ベンダー(unknown
)、OS(Linux
)、そして ABI (gnu
)が含まれています。
ホストの triple 用にコンパイルすることで、Rust コンパイラとリンカは、デフォルトで C ランタイムを使用する Linux や Windows のような基盤となる OS があると想定し、それによってリンカエラーが発生します。なのでリンカエラーを回避するために、基盤となる OS を使用せずに異なる環境用にコンパイルします。
このようなベアメタル環境の例としては、thumbv7em-none-eabihf
target triple があります。これは、組込みシステムを表しています。詳細は省きますが、重要なのは none
という文字列からわかるように、 この target triple に基盤となる OS がないことです。このターゲット用にコンパイルできるようにするには、 rustup にこれを追加する必要があります:
rustup target add thumbv7em-none-eabihf
これにより、この target triple 用の標準(およびコア)ライブラリのコピーがダウンロードされます。これで、このターゲット用にフリースタンディングな実行可能ファイルをビルドできます:
cargo build --target thumbv7em-none-eabihf
--target
引数を渡すことで、ベアメタルターゲット用に実行可能ファイルをクロスコンパイルします。このターゲットシステムには OS がないため、リンカは C ランタイムをリンクしようとせず、ビルドはリンカエラーなしで成功します。
これが私達の OS カーネルを書くために使うアプローチです。thumbv7em-none-eabihf
の代わりに、x86_64
のベアメタル環境を表すカスタムターゲットを使用することもできます。詳細は次のセクションで説明します。
🔗リンカへの引数
ベアメタルターゲット用にコンパイルする代わりに、特定の引数のセットをリンカにわたすことでリンカエラーを回避することもできます。これは私達がカーネルに使用するアプローチではありません。したがって、このセクションはオプションであり、選択肢を増やすために書かれています。表示するには以下の「リンカへの引数」をクリックしてください。
リンカへの引数
このセクションでは、Linux、Windows、および macOS で発生するリンカエラーについてと、リンカに追加の引数を渡すことによってそれらを解決する方法を説明します。実行可能ファイルの形式とリンカは OS によって異なるため、システムごとに異なる引数のセットが必要です。
🔗Linux
Linux では以下のようなエラーが発生します(抜粋):
error: linking with `cc` failed: exit code: 1
|
= note: "cc" […]
= note: /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
(.text+0x12): undefined reference to `__libc_csu_fini'
/usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
(.text+0x19): undefined reference to `__libc_csu_init'
/usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start':
(.text+0x25): undefined reference to `__libc_start_main'
collect2: error: ld returned 1 exit status
問題は、デフォルトで C ランタイムの起動ルーチンがリンカに含まれていることです。これは _start
とも呼ばれます。no_std
attribute により、C 標準ライブラリ libc
のいくつかのシンボルが必要となります。なので、リンカはこれらの参照を解決できません。これを解決するために、リンカに -nostartfiles
フラグを渡して、C の起動ルーチンをリンクしないようにします。
Cargo を通してリンカの attribute を渡す方法の一つに、cargo rustc
コマンドがあります。このコマンドは cargo build
と全く同じように動作しますが、基本となる Rust コンパイラである rustc
にオプションを渡すことができます。rustc
にはリンカに引数を渡す -C link-arg
フラグがあります。新しいビルドコマンドは次のようになります:
cargo rustc -- -C link-arg=-nostartfiles
これで crate を Linux 上で独立した実行ファイルとしてビルドできます!
リンカはデフォルトで _start
という名前の関数を探すので、エントリポイントとなる関数の名前を明示的に指定する必要はありません。
🔗Windows
Windows では別のリンカエラーが発生します(抜粋):
error: linking with `link.exe` failed: exit code: 1561
|
= note: "C:\\Program Files (x86)\\…\\link.exe" […]
= note: LINK : fatal error LNK1561: entry point must be defined
“entry point must be defined” というエラーは、リンカがエントリポイントを見つけられていないことを意味します。Windows では、デフォルトのエントリポイント名は使用するサブシステムによって異なります。CONSOLE
サブシステムの場合、リンカは mainCRTStartup
という名前の関数を探し、WINDOWS
サブシステムの場合は、WinMainCRTStartup
という名前の関数を探します。デフォルトの動作を無効にし、代わりに _start
関数を探すようにリンカに指示するには、/ENTRY
引数をリンカに渡します:
cargo rustc -- -C link-arg=/ENTRY:_start
引数の形式が異なることから、Windows のリンカは Linux のリンカとは全く異なるプログラムであることが分かります。
これにより、別のリンカエラーが発生します:
error: linking with `link.exe` failed: exit code: 1221
|
= note: "C:\\Program Files (x86)\\…\\link.exe" […]
= note: LINK : fatal error LNK1221: a subsystem can't be inferred and must be
defined
このエラーは Windows での実行可能ファイルが異なる subsystems を使用することができるために発生します。通常のプログラムでは、エントリポイント名に基づいて推定されます。エントリポイントが main
という名前の場合は CONSOLE
サブシステムが使用され、エントリポイント名が WinMain
である場合には WINDOWS
サブシステムが使用されます。_start
関数は別の名前を持っているので、サブシステムを明示的に指定する必要があります:
This error occurs because Windows executables can use different subsystems. For normal programs they are inferred depending on the entry point name: If the entry point is named main
, the CONSOLE
subsystem is used, and if the entry point is named WinMain
, the WINDOWS
subsystem is used. Since our _start
function has a different name, we need to specify the subsystem explicitly:
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
ここでは CONSOLE
サブシステムを使用しますが、WINDOWS
サブシステムを使うこともできます。-C link-arg
を複数渡す代わりに、スペースで区切られたリストを引数として取る -C link-args
を渡します。
このコマンドで、実行可能ファイルが Windows 上で正しくビルドされます。
🔗macOS
macOS では次のようなリンカエラーが発生します(抜粋):
error: linking with `cc` failed: exit code: 1
|
= note: "cc" […]
= note: ld: entry point (_main) undefined. for architecture x86_64
clang: error: linker command failed with exit code 1 […]
このエラーメッセージは、リンカがデフォルト名が main
(いくつかの理由で、macOS 上ではすべての関数の前には _
が付きます) であるエントリポイントとなる関数を見つけられないことを示しています。_start
関数をエントリポイントとして設定するには、-e
というリンカ引数を渡します:
cargo rustc -- -C link-args="-e __start"
-e
というフラグでエントリポイントとなる関数の名前を指定できます。macOS 上では全ての関数には _
というプレフィックスが追加されるので、_start
ではなく __start
にエントリポイントを設定する必要があります。
これにより、次のようなリンカエラーが発生します:
error: linking with `cc` failed: exit code: 1
|
= note: "cc" […]
= note: ld: dynamic main executables must link with libSystem.dylib
for architecture x86_64
clang: error: linker command failed with exit code 1 […]
macOS は正式には静的にリンクされたバイナリをサポートしておらず、プログラムはデフォルトで libSystem
ライブラリにリンクされる必要があります。これを無効にして静的バイナリをリンクするには、-static
フラグをリンカに渡します:
cargo rustc -- -C link-args="-e __start -static"
これでもまだ十分ではありません、3つ目のリンカエラーが発生します:
error: linking with `cc` failed: exit code: 1
|
= note: "cc" […]
= note: ld: library not found for -lcrt0.o
clang: error: linker command failed with exit code 1 […]
このエラーは、macOS 上のプログラムがデフォルトで crt0
(“C runtime zero”) にリンクされるために発生します。これは Linux 上で起きたエラーと似ており、-nostartfiles
というリンカ引数を追加することで解決できます:
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
これで 私達のプログラムを macOS 上で正しくビルドできます。
🔗ビルドコマンドの統一
現時点では、ホストプラットフォームによって異なるビルドコマンドを使っていますが、これは理想的ではありません。これを回避するために、プラットフォーム固有の引数を含む .cargo/config.toml
というファイルを作成します:
# in .cargo/config.toml
[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-arg=-nostartfiles"]
[target.'cfg(target_os = "windows")']
rustflags = ["-C", "link-args=/ENTRY:_start /SUBSYSTEM:console"]
[target.'cfg(target_os = "macos")']
rustflags = ["-C", "link-args=-e __start -static -nostartfiles"]
rustflags
には rustc
を呼び出すたびに自動的に追加される引数が含まれています。.cargo/config.toml
についての詳細は公式のドキュメントを確認してください。
これで私達のプログラムは3つすべてのプラットフォーム上で、シンプルに cargo build
のみでビルドすることができるようになります。
🔗私達はこれをすべきですか?
これらの手順で Linux、Windows および macOS 用の独立した実行可能ファイルをビルドすることはできますが、おそらく良い方法ではありません。その理由は、例えば _start
関数が呼ばれたときにスタックが初期化されるなど、まだ色々なことを前提としているからです。C ランタイムがなければ、これらの要件のうちいくつかが満たされない可能性があり、セグメンテーション違反(segfault)などによってプログラムが失敗する可能性があります。
もし既存の OS 上で動作する最小限のバイナリを作成したいなら、libc
を使って #[start]
attribute をここで説明するとおりに設定するのが良いでしょう。
🔗概要
最小限の独立した Rust バイナリは次のようになります:
src/main.rs
:
#![no_std] // Rust の標準ライブラリにリンクしない
#![no_main] // 全ての Rust レベルのエントリポイントを無効にする
use core::panic::PanicInfo;
#[no_mangle] // この関数の名前修飾をしない
pub extern "C" fn _start() -> ! {
// リンカはデフォルトで `_start` という名前の関数を探すので、
// この関数がエントリポイントとなる
loop {}
}
/// この関数はパニック時に呼ばれる
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
Cargo.toml
:
[package]
name = "crate_name"
version = "0.1.0"
authors = ["Author Name <author@example.com>"]
# the profile used for `cargo build`
[profile.dev]
panic = "abort" # disable stack unwinding on panic
# the profile used for `cargo build --release`
[profile.release]
panic = "abort" # disable stack unwinding on panic
このバイナリをビルドするために、thumbv7em-none-eabihf
のようなベアメタルターゲット用にコンパイルする必要があります:
cargo build --target thumbv7em-none-eabihf
あるいは、追加のリンカ引数を渡してホストシステム用にコンパイルすることもできます:
# Linux
cargo rustc -- -C link-arg=-nostartfiles
# Windows
cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console"
# macOS
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
これは独立した Rust バイナリの最小の例にすぎないことに注意してください。このバイナリは _start
関数が呼び出されたときにスタックが初期化されるなど、さまざまなことを前提としています。そのため、このようなバイナリを実際に使用するには、より多くの手順が必要となります。
🔗次は?
次の記事では、この独立したバイナリを最小限の OS カーネルにするために必要なステップを説明しています。カスタムターゲットの作成、実行可能ファイルとブートローダの組み合わせ、画面に何か文字を表示する方法について説明しています。
コメント
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.
可能な限りコメントは英語で残すようにしてください。