借り初めのひみつきち

仮ブログです。

Rust で UEFI のハローワールド

2020-12-23: cargo xbuild が不要になったので一部修正しました。

さいきん Rust のべんきょうはじまりました!

github.com

Rust とは

安全に低レベルプログラミングができるナウい言語っぽいです。 メモリ管理が厳しいので初学者はコンパイルを成功させるだけでも一苦労です。

UEFI とは

2000年ごろに BIOS を代替する目的で開発された PC 向けの新しいファームウェアです。 当初はマイナーな存在でしたが Window 8 の登場とともに市販の PC に広く利用されるようになり、今後は PC の BIOS は廃止されて UEFI が搭載されることになっています。

UEFIHello 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)]EFIAPI を使うことを指定してるみたいです。 #![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_TABLEUEFI のプログラミングにおいて最も重要な構造体で、 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 からファイルアクセスできるようにして実行します。

f:id:neriring16:20200517001646p:plain

いかがでしたか。

多分これが一番簡単だと思います。

*1:しかしこの仕組みだと exit_boot_services の失敗時のリカバリができないのでは...?