Writing an OS in Rust - A Freestanding Rust Binary を読む
まずはRustで標準ライブラリに依存しないコードを書く方法から。
やることは以下の通り
-
#![no_std]
アトリビュートの追加 -
#[panic_handler]
function の実装 - Unwinding の無効化( abort on panic の有効化)
- エントリーポイントの設定
-
#![no_main]
アトリビュートの追加 -
#[no_mangle]
かつextern C
な関数の追加 - linux環境ではエントリーポイントは
_start
であることが多い(がリンカの設定で変えられる)
-
- ターゲットトリプルの変更(OSが存在するtripleだとリンクエラーが発生する)
リンクエラーを解消するためにここではターゲットの変更を行なっているけど、他の方法で解消できないかな?
もっとシンプルな方法を模索したい。(ターゲットトリプルの変更は大袈裟というか、他の要素も変更されてしまっている気がする)
cargo build -v
して、内部でどんなコマンドが発行されているのか見てみた。
↓はリンクエラーを治す直前まで。
rustc --crate-name baremetal --edition=2021 src/main.rs --error-format=json --json=diagnostic-rendered-ansi --crate-type bin --emit=dep-info,link -C panic=abort -C embed-bitcode=no -C split-debuginfo=unpacked -C debuginfo=2 -C metadata=2322d926703684a2 -C extra-filename=-2322d926703684a2 --out-dir /Users/takahashiatsuki/Develop/tmp/rust/baremetal/target/debug/deps -C incremental=/Users/takahashiatsuki/Develop/tmp/rust/baremetal/target/debug/incremental -L dependency=/Users/takahashiatsuki/Develop/tmp/rust/baremetal/target/debug/deps -C linker=clang
設定されているオプションを抜き出してみる。また、今回重要そうなものを 太字 にしてみる。
--crate-name baremetal
--edition=2021
--error-format=json
--json=diagnostic-rendered-ansi
--crate-type bin
--emit=dep-info,link
-C panic=abort
-C embed-bitcode=no
-C split-debuginfo=unpacked
-C debuginfo=2
-C metadata=2322d926703684a2
-C extra-filename=-2322d926703684a2
--out-dir /Users/..../deps
-C incremental=/Users/..../incremental
-L dependency=/Users/.../deps
-C linker=clang
-C
は codegen option というもので、↓に一覧が載ってる。
↑で設定されているオプションを参考に、自分で rustc を直接呼び出してみる。
rustc src/main.rs -C panic=abort -C linker=clang
するとやはり、リンクエラーが出る。
エラー内容
note: "clang" "-m64" "-arch" "x86_64" "main.main.cbc7ddee-cgu.0.rcgu.o" "-L" "/Users/takahashiatsuki/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib" "/Users/takahashiatsuki/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/librustc_std_workspace_core-1108e622f5a15c3d.rlib" "/Users/takahashiatsuki/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/libcore-43af7053e70b1eed.rlib" "/Users/takahashiatsuki/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib/libcompiler_builtins-3a81ebf6a3abbdee.rlib" "-L" "/Users/takahashiatsuki/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/x86_64-apple-darwin/lib" "-o" "main" "-Wl,-dead_strip" "-nodefaultlibs"
rustc にバイナリではなくアセンブリやオブジェクトファイルを出力させてみる。
rustc src/main.rs -C panic=abort --emit=asm
rustc src/main.rs -C panic=abort --emit=obj
↑の結果、シンプルなアセンブリファイルが生成された。
main.s
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 7
.globl __start
.p2align 4, 0x90
__start:
pushq %rbp
movq %rsp, %rbp
jmp LBB0_1
LBB0_1:
jmp LBB0_1
.private_extern _rust_begin_unwind
.globl _rust_begin_unwind
.p2align 4, 0x90
_rust_begin_unwind:
pushq %rbp
movq %rsp, %rbp
jmp LBB1_1
LBB1_1:
jmp LBB1_1
.subsections_via_symbols
__start
と _rust_begin_unwind
という2つのラベルがあり、それぞれ無限ループするように実装されている。
ただ、一つだけハマったのは、 _start
ではなく __start
( _
2つ)で出力されていたこと。
これのせいで ld でリンクしようとした時に、エントリーポイントがないというエラーになってしまった。
mac os の場合、デフォルトのエントリーポイントは、OS のバージョンによって違う。
10.7 以前だと start
, それ以降だと _main
がエントリーポイントとなる。
今回生成されたアセンブリでは、 macosx_version_min 10, 7
が設定されているので、リンカはエントリーポイント start
を探す。しかし rustc による出力では、なぜか追加のアンダースコアが1つ余計についてしまうので、エントリーポイントを明示的に指定することで解決する。
ld -e __start main.o
元記事にも書いてあった。
for some reason all functions are prefixed with a
_
on macOS
てか↑のリンクの章で、リンカにオプションを渡すことでリンクエラーを解消する方法が詳しく書いてある。
その章によると、macOSの場合のリンクエラーは2つあって、
- エントリーポイントが見つからない問題
-
-e
オプションをリンカに渡せば解決
-
- macOS が statically linked binary を公式にはサポートしていない(マジ??)
- https://developer.apple.com/library/archive/qa/qa1118/_index.html
- カーネルのシステムコールのバイナリ互換性を保証しないかららしい。システムフレームワークでそれを保証するからシステムフレームワークに動的リンクしてね。とのこと。
- システムフレームワークにリンクしないようにするには、
-static
オプションを渡す。 - また、このままでは
crt0
へのリンクが残っているので、-nostartfiles
オプションを渡してそれもなくす。 - ちなみに、リンカとして
cc
ではなくld
を使った場合、このエラーはでない。
まとめると、
rustc src/main.rs -C panic=abort -C link-args="-e __start -static -nostartfiles"
または、
rustc src/main.rs -C panic=abort -C link-args="-e __start" -C linker=ld
で、freestanding なバイナリを生成できる。
ついでに linux 環境でのリンクエラーについてもメモしておく。
linux 環境でも、リンカはデフォルトで C runtime の startup routine をリンクしようとする。(記事中では Scrt1
という C runtime )
しかし、そのランタイムは libc
に依存しており、今回のような no_std 環境では libc
がリンクされないため、シンボルが解決できずリンクエラーが発生する。
これを解消するためには、そのような startup routine をリンクしなければ良いので、 -nostartfiles
オプションをリンカに渡して解決する。
rustc src/main.rs -C panic=abort -C link-arg="-nostartfiles"
また、rustc を直接利用するのではなく、cargo を経由して rustc にオプションを渡すためには、 cargo rustc
コマンドを利用すれば良い。この方法だと、cargo がデフォルトで設定してくれるオプションに 追加して rustc にオプションを渡すことができる。
例えば、 macOS で静的リンクされたバイナリを生成するコマンドは以下のようになる。
rustcに渡すオプションは --
の後につける。
cargo rustc -- -C link-args="-e __start -static -nostartfiles"
ちなみに手元の環境だと、 cargo rustc -- -C linker=lld
で rustc に linker オプションを渡しても、cargo が内部的に rustc に渡す linker オプションによって上書きされてしまい、linker を設定することができない。
これが意図した挙動なのかは不明。