2020-12-23: cargo xbuild が不要になったので一部修正しました。
さいきん Rust のべんきょうはじまりました!
Rust とは
安全に低レベルプログラミングができるナウい言語っぽいです。 メモリ管理が厳しいので初学者はコンパイルを成功させるだけでも一苦労です。
UEFI とは
2000年ごろに BIOS を代替する目的で開発された PC 向けの新しいファームウェアです。 当初はマイナーな存在でしたが Window 8 の登場とともに市販の PC に広く利用されるようになり、今後は PC の BIOS は廃止されて UEFI が搭載されることになっています。
UEFI で Hello World
さて、 Rust で UEFI のプログラミングするにはどうすればいいでしょうか。
まずは公式から Rust をインストールしましょう。
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
インストールが終わったらシェルを再起動しましょう。
次に、 UEFI でプログラミングするには cargo-xbuild というツールを使ってクロスビルドが必要ですのでこれもインストールします。
$ cargo install cargo-xbuild
現在は不要になりました。
また、ベアメタルプログラミング全般で nightly のコンパイラが必要になることが多いらしいのでこれも使えるようにしておきます。
$ rustup install nightly
これでツールの用意ができました。
つぎにプロジェクトを作成します。
$ cargo new uefi-rs-hello
この時点で以下のようなツリー構造になっているはずなので uefi-rs-hello フォルダに移動します。
. `-- uefi-rs-hello |-- Cargo.toml `-- src `-- main.rs
Rust で UEFI のプログラミングをするには uefi-rs というライブラリを使うのが簡単です。
また、ベアメタルでは compiler_builtins
という最低限のライブラリがいい感じに組み込まれるはずですが、 uefi-rs を使った場合何かが足りないようでうまくビルドできなかったので rlibc
というライブラリを組み込みます。 rlibc 自体は非推奨になっているようなのでこの部分はいずれ修正したいです。
Cargo.toml の [dependencies]
に以下のように記述します。
[dependencies] uefi = { git = "https://github.com/rust-osdev/uefi-rs.git" } rlibc = "1.0.0"
いよいよ src/main.rs を書いていきます。
#![feature(abi_efiapi)] #![no_std] #![no_main] use uefi::prelude::*; use core::panic::PanicInfo; use core::fmt::Write; #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } #[entry] fn efi_main(_handle: Handle, st: SystemTable<Boot>) -> Status { writeln!(st.stdout(), "Hello, world!").unwrap(); loop {} // Status::SUCCESS }
最初の3行は Rust で UEFI のプログラミングに必要なおまじないです。
#![feature(abi_efiapi)]
で EFI の API を使うことを指定してるみたいです。
#![no_std]
と #![no_main]
は通常のライブラリ(std)を使わないベアメタル環境という指定と通常のmain関数から始まらないことを指定してます。
use uefi::prelude::*;
は uefi-rs の基本的な定義を取り込むのに必要です。
use core::panic::PanicInfo;
と panic
の定義まわりは Rust のエラー処理に必要です。
通常 panic は std の中に定義されていますが、今回は no_std を指定しているので自前実装が必要です。
Hello World 程度のプログラムでは panic は不要なのでただの無限ループにしてます。
use core::fmt::Write;
は writeln!
マクロを呼び出すためのおまじないです。
#[entry]
は efi_main
関数がエントリポイントであることを指定します。
fn efi_main(_handle: Handle, st: SystemTable<Boot>) -> Status
がメイン関数に相当する定義になります。通常の UEFI と基本的には同じ内容です。
_handle: Handle
はイメージハンドルです。今回は使わないので名前の先頭に _ を付けてます。
st: SystemTable<Boot>
は EFI_SYSTEM_TABLE
を受け取る指定です。
EFI_SYSTEM_TABLE
は UEFI のプログラミングにおいて最も重要な構造体で、 uefi-rs ではシステムテーブルが SystemTable<Boot>
と SystemTable<Runtime>
の2種類あります。これはそもそも UEFI が Boot Services 環境と Runtime Services 環境の2種類あるためで、 efi_main
の呼び出し時は SystemTable<Boot>
、 st.exit_boot_services
呼び出し後は SystemTable<Runtime>
が使用できます。 Rust の所有権の仕組みを利用して st.exit_boot_services
後は元の SystemTable<Boot>
に誤ってアクセスできないようになっています。*1
writeln!(st.stdout(), "Hello, world!").unwrap();
がハローワールドの本体です。ここまで来るまで長かった・・・
writeln!
は Rust 標準のマクロで、あとに続く文字列をフォーマットして出力します。 Rust では名前の最後に !
がつく関数っぽいものは全てマクロです。
st.stdout()
は EFI_SYSTEM_TABLE
の中にある EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
を取得する処理です。 EFI_SYSTEM_TABLE
の構造が若干 stdin/stdout を意識したような構造になっているのでこのようなメソッド呼び出しになっているのだと思います。
最後に付いてる unwrap()
は writeln!
の出力が Result
になっているためアンラップしてます。この処理がなくても警告が出るだけで一応動作します。
最後の行の loop {}
は無限ループしています。コメント化している Status::SUCCESS
の方に書き換えるとシェルに戻る御行儀の良い UEFI アプリケーションになりますが、ハローワールドでは必要ないでしょう。
main.rs ができたら以下のようなコマンドでビルドできます。
$ cargo +nightly build -Zbuild-std=core --target x86_64-unknown-uefi --release
初回はちょっと時間がかかり、無事ビルドが成功すると /target/x86_64-unknown-uefi/release/uefi-rs-hello.efi として出力されます。
これを qemu からファイルアクセスできるようにして実行します。
いかがでしたか。
多分これが一番簡単だと思います。