UEFIアプリケーションをRustで書く(外部クレートなし)

このあいだのRustのアップデートで、x86_64-unknown-uefiなるターゲットが追加された、と聞いてRustでUEFIプログラミングに挑戦しました。
なお、世の中にはすでにuefi-rsというものが用意されているので、もっと簡単にUEFIアプリケーションを書くことができます。
ただ、このクレートの構造の説明みたいなのがドキュメントとして見当たらず、今までUEFIを触ったことがなかったので、せっかくなのでこのクレートも含め外部クレートなしでのプログラミングをしました。

UEFIとは

BIOSに代わるファームウェアに対するソフトウェアインターフェースのこと。詳しくはWikipediaないし、C言語を使っての開発についてはもっと多くの先例があるのでそちらを参考にすればいいと思われる。

最新の仕様は直接仕様書を見て確認しましょう。今回はversion 2.7に準拠。
Unified Extensible Firmware Interface Forum

準備

今回はx86_64のUEFIアプリケーションをつくります。
まず、x86_64ターゲットが追加されているRustを使う必要があります。また、ターゲットは追加されたものの、標準ライブラリ(stdではなくcoreとか)がまだ整えられていないのでstableではなくnightlyを使う必要があります。
更に、標準ライブラリを組み込むためcargo-xbuildも使う必要があります

1
2
$ rustup default nightly-2019-03-23
$ cargo install cargo-xbuild

また、実行環境としてqemu-system-x86_64とqemu用のUEFIファームウェアであるOVMFが必要です。
自前でビルドする方法はやってみたのですが、途中で詰まってしまったし、めちゃくちゃ時間もかかるのでビルド済みのものをとってきたほうが早いでしょう
https://www.kraxel.org/repos/からjenkins/edk2以下にあるx64用のrpmをとってきてその中にあるOVMF_VARS-pure-efi.fdとOVMF_CODE-pure-efi.fdをとってきました。

できたもの

Githubに置いておきました。
garasubo/uefi-practice
cargo xbuild --target x86_64-unknown-uefiとしてビルドし、qemu-run.shでqemu上で走らせます。ただし、OVMF_VARS.fdとOVMF_CODE.fdをプロジェクトのルートディレクトリに置いておく必要があります。

qemu-run.shを実行するとuefiシェルが立ち上がるので

1
2
Shell > fs0:
FS0: \> uefi-practice.efi

とすると、画面がクリアされた後、Hello Worldします

解説

efi_main関数がエントリポイントになっています。仕様書で言うとEFI_IMAGE_ENTRY_POINTに相当します。
仕様書にはC言語での型宣言が書かれているのですが、これをRustで書いていくことになります。
普通にstructやenumを宣言してしまうとRust独自のABIでコンパイルされてしまうので、reprをつけて互換性を保ちます。詳しくはThe RustonomiconのData Layoutの章が参考になります。
今回の目標はhello worldすることなのですが、UEFIのテキストを出力するためのインターフェースを利用します。
仕様書で言うとEFI_SIMPLE_TEXT_OUTPUT_PROTOCOLです。
僕のコードではreset関数とoutput_string関数が定義されていますが、本当はもっとあります。
他のstructについても今回は使う部分だけ型宣言をして残りはサボりました。使わない関数はusizeでごまかしました。全部実装するのはしんどい。

output_stringはPROTOCOL自体へのポインタと文字列への先頭ポインタを渡す必要があります。この文字列が少々やっかいでUCS-2でエンコーディングされていないといけません。
RustはUTF-8で文字列を扱っているため、1文字が16ビットのUCS-2に変換するのは標準ライブラリだけで簡単にやってくれそうではなかったので、
適当なバッファを用意して、実行時に無理やり16ビット配列に変換しました(uefi-rsも内部ではそうやっていた)。

あとはno_stdではpanic_handlerは自前で用意しなければならないのでそれも忘れずに。

疑問点

uefi-rsを参考にしながらつくったのだが、extern "C"extren "win64"の両方が出てくることがあり、これの違いについてはよくわからなかった。
とりあえずuefi-rsに従いこれらの宣言をつけておいたが、本当はextern "C"としておいても問題ないかも?

まとめ

UEFIの仕様は真面目に実装するのは大変なので、おとなしくuefi-rsを使いましょう