Open11

Writing an OS in Rust - A Freestanding Rust Binary を読む

TakatomTakatom

まずはRustで標準ライブラリに依存しないコードを書く方法から。
やることは以下の通り

  1. #![no_std] アトリビュートの追加
  2. #[panic_handler] function の実装
  3. Unwinding の無効化( abort on panic の有効化)
  4. エントリーポイントの設定
    1. #![no_main] アトリビュートの追加
    2. #[no_mangle] かつ extern C な関数の追加
    3. linux環境ではエントリーポイントは _start であることが多い(がリンカの設定で変えられる)
  5. ターゲットトリプルの変更(OSが存在するtripleだとリンクエラーが発生する)

リンクエラーを解消するためにここではターゲットの変更を行なっているけど、他の方法で解消できないかな?
もっとシンプルな方法を模索したい。(ターゲットトリプルの変更は大袈裟というか、他の要素も変更されてしまっている気がする)

TakatomTakatom

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 というもので、↓に一覧が載ってる。
https://doc.rust-lang.org/rustc/codegen-options/index.html

TakatomTakatom

↑で設定されているオプションを参考に、自分で 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"

TakatomTakatom

rustc にバイナリではなくアセンブリやオブジェクトファイルを出力させてみる。

rustc src/main.rs -C panic=abort --emit=asm
rustc src/main.rs -C panic=abort --emit=obj
TakatomTakatom

↑の結果、シンプルなアセンブリファイルが生成された。

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
TakatomTakatom

てか↑のリンクの章で、リンカにオプションを渡すことでリンクエラーを解消する方法が詳しく書いてある。

その章によると、macOSの場合のリンクエラーは2つあって、

  1. エントリーポイントが見つからない問題
    1. -e オプションをリンカに渡せば解決
  2. macOS が statically linked binary を公式にはサポートしていない(マジ??)
    1. https://developer.apple.com/library/archive/qa/qa1118/_index.html
    2. カーネルのシステムコールのバイナリ互換性を保証しないかららしい。システムフレームワークでそれを保証するからシステムフレームワークに動的リンクしてね。とのこと。
    3. システムフレームワークにリンクしないようにするには、 -static オプションを渡す。
    4. また、このままでは crt0 へのリンクが残っているので、 -nostartfiles オプションを渡してそれもなくす。
    5. ちなみに、リンカとして 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 なバイナリを生成できる。

TakatomTakatom

ついでに 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"
TakatomTakatom

また、rustc を直接利用するのではなく、cargo を経由して rustc にオプションを渡すためには、 cargo rustc コマンドを利用すれば良い。この方法だと、cargo がデフォルトで設定してくれるオプションに 追加して rustc にオプションを渡すことができる。

例えば、 macOS で静的リンクされたバイナリを生成するコマンドは以下のようになる。

rustcに渡すオプションは -- の後につける。

cargo rustc -- -C link-args="-e __start -static -nostartfiles"
TakatomTakatom

ちなみに手元の環境だと、 cargo rustc -- -C linker=lld で rustc に linker オプションを渡しても、cargo が内部的に rustc に渡す linker オプションによって上書きされてしまい、linker を設定することができない。
これが意図した挙動なのかは不明。