重力に縋るな

千種夜羽です

RustでUEFIアプリケーションを書く 2020 Edition

RustでUEFIアプリケーションを書く 2020 Edition

みなさんこんにちは.sksatです. 最近はVRで生活したり,オタクで を取ってゲラゲラしたりしています.

ちなみに映画化しました*1.よろしくお願いします. youtu.be

さて,これは自作OS Advent Calendar 2020の21日目の記事なわけですが, Advent Calendarに登録していたことを完全に忘れていて,Twitterで教えてもらいました.オイオイ. 今日中に公開したら許してほしい*2.今は13:50(JST)です.

adventar.org

ということでなんも準備してないわけですが(は?), 実は今年のセキュリティ・キャンプ全国大会のYトラック自作OSゼミでチューターをやったりしていました. まあ今回はオンライン開催だったということもあり,受講生の人と一緒にワイワイデバッグしながらガッと開発,みたいなことはできなかったんですが*3,チューターをやっている間に(ようやく重い腰を上げて)Rustに入門したり,RustでUEFIアプリケーションを書いたりしていたのです.なのでこの話をします.

Rust

言わずと知れた最近ブイブイ言わせている*4プログラミング言語です. なんか最も愛されていたりするらしい.ℒ𝒪𝒱ℰ...

簡単に紹介しておくと,コンパイラに天才を投入し書き手に制約を課しコンパイル時間を天に捧げる*5ことで, 最高の開発体験と高速に動作するバイナリを得ることができるプログラミング言語です.

Rust,ずっと気になってはいたんですよね. どれくらい気になっていたかというとRustはいいぞというツイートを見て「ホォ〜ン良さそうじゃん.僕はC++17書くけど...」とか言ったり, AmazonのwishlistにRustの本入れたり, wishlistに入れてたRustの本がRust好きなフォロワーに買われて届いたけど積んだり, 積んだRustの本をチラチラ見て「いいじゃ〜んそういうのだよそういうの」とか呟いたり, Rustで書かれた組み込みOSを眺めてみたりThe Rust Programing Language 日本語版をチラチラ見たり, プログラミング言語の入門にはLISP書いてみるといいですよとTwitterで言われて一理あるなと思って所有権とかなんもわからんままLISPインタプリタ実装しかけてAST作ったところで詰んだりしていました.

そんなこんなでRustやるんだかやらないんだか微妙なところで踏みとどまっていたんですが,まあ例によってVRChatのオタクに布教されたので教えてもらいながら始めました. VRChatは最高.

ここまででVRChatとRustが最高なことしか情報がないですね.そういうこともある.

自作OS的に分かりやすいRust最高ポイントはpanicをちゃんと実装してあげれば突然の死の時にもある程度情報が得られるところとか,ターゲットアーキテクチャバチバチ切り替えられるところとか,ユニットテストフレームワークが言語標準で付いてるところとか,OS(std)に依存しないライブラリ(crate)がたくさんある上にそれらを標準のビルドツール(cargo)で引っ張ってくることができるところとかですかね.

UEFIアプリケーションを書きたい

というわけでRustってやつでUEFIアプリケーションを書いてみましょう. LLVMだしclang+lldの時みたいにやればできるでしょ.

Rust UEFI [検索]

\ババーン/ github.com

なんとライブラリがあります.UEFIアプリケーションを書くための.何?(こういうところもRust最高ポイントですね)

これを使ってHello World!する話は例によってオタクが書いてますね. neriring.hatenablog.jp

しかしせっかくなら全部自分で書いてみたいです.車輪の再発明も車輪をバラすのも大好きなので. セキュリティ・キャンプでも受講生の一人が最初uefi.hをUEFI Specificationを見ながら手打ちしていて,「わかる〜」となりました*6

んでもってそういう記事も既にあるわけです.やった〜 garasubo.github.io

でもこれは今は動かなくなっています.まあcargo-xbuildのバージョンをこの当時のものにすれば大丈夫な気はしますが.

とはいえRustは進化の速い言語です.最新のRustでもuefi-rsを使わずにUEFIアプリケーションを書きたい.僕は書きたかったです. ということで書きました.

github.com

apt update && apt install -y curl git gcc
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env
git clone https://github.com/sk2sat/uefi-hello-rs
cd uefi-hello-rs
rustup component add rust-src
cargo build --release

こんなかんじでビルドできるはずです.Dockerでもいける.

rustupはRustツールチェーンのバージョンやらターゲットやらをいいかんじに管理できるやつです. Rustはディストリのパッケージマネージャから入れるよりも https://rustup.rsに書いてあるワンライナーで入れた方がいいまである. 実際これで一回詰まった((実はrustupでRustを入れると使えるrustcとかcargoとかはそれそのものではなくてrustupのシンボリックリンクになってるんですよね. ls -l which cargoとかしてみると分かる.Buildrootかな?まあなるほど感はあるけど微妙に挙動が違って非常に時間を溶かしたので,なにも考えずにpacman -S rustとかやらない方がいいです.communityにrustupあるから使おうね.)). 実態は$HOME/.rustupとかにいるので環境をブチ壊さなくて安心.エコってやつです.

cargoはビルドシステム+パッケージマネージャみたいなやつです.これの存在もRust最高ポイントの一つですね. なんとライブラリを追加するのはCargo.tomlのdependenciesでcrate名とバージョンを指定するだけ*7. ほとんどのcrateはhttps://crates.io に登録されているのでこれだけで大丈夫な上,gitで取ってくるとかもできます. あとはcargo buildとか叩くとガーッと依存物がダウンロードされビルドが走ります.

cargo buildで自作OSをビルドしたい

Rustで開発する時はほぼcargoしか叩かない(rustcを直接呼んだりmakeを使ったりしない)んですが, 諸事情により自作OSではcargoではなくxargoやそのforkのcargo-xbuildが使われていました. xargomakeで呼んでるやつとかも見ますね.

でも,せっかくRustを使っているわけですし,cargo buildでおもむろに自作OSがビルドできるようになってほしいですよね. 僕はなってほしいです. そこで,今cargo-xbuildのリポジトリを見にいくと,

github.com

Cargo now has its own feature for cross compiling the sysroot: build-std. You can use it by passing -Z build-std=core,alloc to cargo build. Alternatively, you can specify the following in a .cargo/config.toml file:

[unstable]
build-std = ["core", "compiler_builtins", "alloc"]

The above requires at least Rust nightly 2020–07–15. With the above config in place, the normal cargo build command will now automatically cross-compile the specified sysroot crates.

とあります.おや?なんか最近のやつならいけそうじゃあないですか.これはやるしかない.

ということでなんとcargoだけでビルドできるようになりました.やった〜.

まずはなにもしないやつ

cargo buildでビルドできるようになったところでHello, Worldやっていきましょう.まずはなにもしないものを作ります.

#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[no_mangle]
pub extern "C" fn efi_main() {
    loop {}
}

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}

最初の#![~~~]はcrate-level attributeというもので,ソースコード中で色々な指定ができます. ここではno_stdno_mainですね. no_std-nostdlibみたいなやつで, no_mainmainなんていう関数は無いけど怒らないでねってやつです. いつものやつですね.

次のusePanicInfoという型を後で使うのでそれを引っ張ってきています. C++usingみたいなやつですね. ここで急に出てきたcoreは何かというと,なんと標準ライブラリみたいなやつです. 普段なら自作OSで標準ライブラリが使えるわけないだろ!下がってろ!となるところですが, なんとcoreはOSの機能に依存しておらず自作OSでも使えるのです.やった〜((coreがOSの機能に依存していない,というよりは標準ライブラリを作るにあたってOSの機能に依存しないが必要なものをcoreとして明確に切り分けて実装した,というのが適切なんじゃないかなと思います.たぶん.))

次はefi_main関数ですね.エントリポイントです.この関数にはno_mangleというattributeが付いているのでマングリングが行われません.お手軽ですね.

最後はpanic handlerです. panicした時にダイイングメッセージを書く奴ですね. no_std環境では標準出力なんてものがあるわけがないので自分で実装する必要があります. この引数にさっきのPanicInfoが来るわけですね.

Hello, World!

さてHello, World!していきましょう. UEFIアプリケーションなので,とりあえず必要な分だけシステムテーブルの構造体を作ってSimpleTextOutputProtocolを取ってきてOutputString関数を叩くだけです. かんたんですね(そうかな?)

ということでsrc/uefi.rsがこんなかんじになりました.

use core::ffi::c_void;

#[derive(Clone, Copy)]
#[repr(C)]
pub struct Handle(*mut c_void);

#[repr(usize)]
pub enum Status {
    Success = 0,
}

#[repr(C)]
pub struct TableHeader {
    signature: u64,
    revision: u32,
    header_size: u32,
    crc32: u32,
    _reserved: u32,
}

#[repr(C)]
//#[repr(packed)] // packedを付けると32bit縮められて死ぬ
pub struct SystemTable {
    pub hdr: TableHeader,
    pub firmware_vendor: *const u16,
    pub firmware_revision: u32,
    pub console_handle: Handle,
    pub _con_in: usize,
    pub console_out_handle: Handle,
    pub con_out: *mut SimpleTextOutputProtocol,
}

#[repr(C)]
pub struct SimpleTextOutputProtocol {
    reset: unsafe extern "efiapi" fn(this: &SimpleTextOutputProtocol, extended: bool) -> Status,
    output_string:
        unsafe extern "efiapi" fn(this: &SimpleTextOutputProtocol, string: *const u16) -> Status,
    _resb2: u128,
}

impl SystemTable {
    pub fn stdout(&self) -> &mut SimpleTextOutputProtocol {
        unsafe { &mut *self.con_out }
    }
}

impl SimpleTextOutputProtocol {
    pub fn reset(&self, extended: bool) -> Status {
        unsafe { (self.reset)(self, extended) }
    }

    pub fn output_string(&self, string: *const u16) -> Status {
        unsafe { (self.output_string)(self, string) }
    }
}

最初は自分でゴチャゴチャやってたんですけど結局uefi-rsを参考にしました.uefi-rs使った方がいいです. あと,C/C++のノリで「あ〜こういうのはpacked付けとけばいいでしょ」と思って付けまくってたら動きませんでしたね. なんかメンバのアドレスが32bitズレてたりしてました. ちゃんと挙動を調べたかったのでメンバのオフセットが合ってるかみたいなユニットテストを書きたかったんですが,これもイマイチやり方がわからず時間を溶かしました. ウーム.今度ちゃんと調べます(ホンマか?)

あともちろん,いきなり構造体のメンバのアドレスを関数だと思って呼ぶなどというCでは一般的な野蛮な活動はRustでは許されないので,unsafeを付けています.

UTF-16

これでHello, World!する準備が整ったので,あとはefi_main関数でoutput_string()とかを呼んであげればいいわけですが, ここで一つ問題があります. それは文字列です.

Rustでは文字列はUTF-8です. これもRust最高ポイントですね.

どこぞのC++とかいうプログラミング言語は最近ようやく文字とは何かを理解したわけですが...(なお...) qiita.com

さてここでUEFI Specificationを見てみましょう

\ババーン/

FI_SIMPLE_TEXT_OUTPUT_PROTOCOL.OutputString
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.OutputString()

_人人人人人_
> CHAR16 <
 ̄Y^Y^Y^Y^Y^ ̄

┏-┷-┓
┃ U ┃
┃ n ┃
┃ i ┃
┃ c ┃
┃ o ┃
┃ d ┃
┃ e ┃
┗━━┛

はい.まあUEFIMicrosoftが策定にガッチリ絡んでますからね. MicrosoftUnicodeと言ったらUTF-16ですし,型はCHARとかWCHARですよ.まあCHAR16なだけだいぶマシですかね. UEFIは滲み出るwindows.h感でとても懐かしい気持ちになる物体としておすすめです.

こういうこと言うとWindows 10のMay 2019 Updateでnotepad.exeのデフォルトがUTF-8になったじゃないかとか, WSLは最高の開発体験だとか言われることがありそうな気がしますが, そういう人はWin10のシステムロケールUTF-8にしてみるといいんじゃないでしょうか.あまりに色んなものがブチ壊れて楽しいですよ.

まあ僕も最近VR関連の諸々のためにWin10使うことがだいぶ増えてはきましたけどね*8. それでもArch Linux使う時間の方がずっと長いですが.

話が逸れました.さてUTF-16です. まあUEFIアプリケーションのサンプルでたまにありがちな,文字列を一旦バッファに入れてunsigned 16bitの型の配列の下位8bitに雑に詰めるやつをやってもいいんですが, あれはなにかに敗北した気持ちになるのでやりたくないです.まあどうせ表示するのはASCIIのHello, World!なんですけど.

なのでUTF-16な文字列リテラルとかがあるとうれしいんですが,Rustにはそういうのは無いらし...おや?

github.com

こんなcrateがありました.求めていたものっぽい.

これはRustのマクロを使ってUTF-16な文字列リテラルの真似事をするというものです. 具体的にはutf16_literal::u16というマクロに普通に文字列を突っ込むとu16の配列になって出てくるようになっています. UTF-8の文字列をUTF-16の文字列に変えるなんて処理は動的メモリ確保が無い状態だとちょっとつらいですし, なにより今回は表示するものが決まっているので,UEFIアプリケーション上で処理する必要すらないわけです.

ということでこんなかんじになりました.やったぜ.

extern crate utf16_literal;
use utf16_literal::utf16;

mod uefi;
use uefi::Status;

#[no_mangle]
pub extern "efiapi" fn efi_main(_image: uefi::Handle, stable: uefi::SystemTable) -> Status {
    let stdout = stable.stdout();
    stdout.reset(false);
    stdout.output_string(utf16!("Hello, World!").as_ptr());

    loop {}
}

cargo runでQEMUが起動してほしい...起動してほしくない?

Rustで普通のHello, World!したことある人なら分かると思うんですが(分からない場合はcargo new hoge && cd hoge && cargo run), cargo runってやると書いたアプリケーションが起動するじゃないですか. あれが最高なので,というか普段Makefile書いてmake runしてる人間なのでcargo runしたらQEMUなりBochsなりが起動してほしい.起動してほしいです.頼む.

で,まさにそういうことができるものがあります.custom runnerってやつですね.

これは.cargo/configにこんなかんじに追記してやればいいです.

[target.x86_64-unknown-uefi]
runner = "qemu-system-x86_64"

これで行けるならいいんですけどね.残念ながらUEFIアプリケーションなのでmnt/EFI/BOOT/BOOTx64.EFIとかにバイナリを置いてマウントする必要があります((でも,カーネルだけならqemu-system-x86_64 -kernelで行けるし,なにかトチ狂ってLinuxでWin32AP直叩きアプリケーションを書くとき(たまにある)にwineを指定できたり,夢が広がりますね(?).)). なので最初はじゃあディレクトリ切ってコピーするワンライナーを前に押し込んでやればいいなとか思ったんですが, これはsystem()が呼ばれるとかではない(シェルが呼ばれるわけではない)のでそういうことはできませんでした.

あと,cargo runMakefileでよくあるようにcargo build->cargo runと実行されるわけではなく, なにかそれで時間をめちゃくちゃ溶かした気がするのですが覚えてないです. 完全に先入観のせいなんですが「え?これどうなってるんだ」とcargoのソースコードを読みに行ったのはよかったような気がします.

結局これは以下のようなrun-qemuというシェルスクリプトを作ってgot kotonakiしました.

#!/bin/sh
echo $1
mkdir -p mnt/EFI/BOOT
cp $1 mnt/EFI/BOOT/BOOTx64.EFI
qemu-system-x86_64 --bios bios/RELEASEX64_OVMF.fd -drive format=raw,file=fat:rw:mnt

これでcargo runすると... f:id:sksat:20201221203836p:plain

やった〜

cargo runQEMUが起動するようになって満足し,その後進捗がありませんでした. いかがでしたか?

*1:Q. これはなに A. クソドメイン部 Q. クソドメイン部ってなんですか A. 僕も知りたい

*2:履修も受講も課題提出も全部失敗してるオタクがアドベントカレンダー書けるわけないじゃん.じゃあ登録するな.https://soude.su

*3:footnoteなのであんまり関係ない話していいですか?いいよあり. 今日もCOVID☆19さんが全世界で猛威をふるっていらっしゃるわけですが,なんというかこの伝染病は良くも悪くもFateシリーズの聖杯ってかんじですよね. 伝染病が大流行しているような事態はこれっぽちもよくはないわけですが, 色々なところで言われているように(要出典),これには巨大なデメリットと同時にメリットも振り撒いていますよね. 未だかつてこんなにも世界中の「IT化」が進んだ瞬間があったでしょうか.まあパワーのある政治をやっている国ならあったかもしれませんが. そんなメリットの一つが外に出なくてい(ry,じゃなかった,今回のセキュリティキャンプだったり大学の講義だったりのオンライン化だと個人的には思っています. もちろんこれちゃんとやるのは非常に難しいんですけどね. まあ難しいだろうなと思って大学の講義を見ていたわけですが,セキュキャンで実際やる側に少し回ってみてやはりこれは大変だなと思いましたね. メリットの話をしましょうか.まあ大きいのは参加のハードルの低下,録画の公開(の一般化)ですかね. でもこれデメリットも同じくらいあるんですよね(オイ).通話に参加すること事態のハードルは低くても, (見ず知らずの人間と)ミュート解除して話すコストはめちゃくちゃ高いし.とくにGoogle MeetなりZoomなりだと初期設定の虚無アイコンがババーンと表示されてるだけですしね. これはかなり前からだいぶ気にしていて,5月くらいからFaceRigを試したりしていました.実はVRにどっぷりハマったのもそういう経緯があったり無かったりする(どうかな). 研究室でも最初に美少女になったのは僕でしたね.その後数人が3teneとかでやってましたね. まあここ最近精神と生活リズムが完全にブチ壊れていてろくにミーティング出られてないんですけど.すみません. 僕は最近はVRChatでストリームカメラ or VRM化したアバターをVMagicMirrorに突っ込んだものOBSでキャプチャしてやっています. 毎回Index被るのはさすがに面倒だったり不都合があったりするのでVMagicMirrorは重宝してますね. 普通に便利だしあそこらへんのツールで唯一(?)wineでまともに動くし.Linuxデスクトップ太郎としてはな. 仮想カメラで美少女が喋ってると個人的にはだいぶ話すハードル下がると思うんですよね.他の人がどうかはよく分からないけれど. 今年のセキュキャンはせっかくオンライン開催だったのでほぼ全編美少女で参加しました. 受講生の人の心理的なハードルを少しでも下げられていたらよかったなと思います. あとLTでVRへの勧誘したり時間外にVRChatへの勧誘したり元々VRChatにいた参加者・チューター・講師とワチャワチャしたりしました(あれ?) 話を戻しますか.まあこれ全部脱線だけど. あー録画の話ですね.これは非常にありがたいとともに個人的な恨みつらみがたくさんあります. 講義が録画されるようになることとそれが受講者に対して公開されるムーブ自体は最高なんですが,問題はその共有方法です. まあ権利的に難しかったりするわけで同情できないわけではないんですが,あの不自由まみれの状態は一体なんなんですかね. 他の大学だとだいぶマシだったりするみたいですが,クソみたいなDRMのかかったクソみたいに使いにくい動画ビューアに始まり, それをWindowsでしか動かないカスゴミアプリケーション上でのみ再生できるようにするだのリクエストがあった時のみ公開するだの1週間経ったら公開を停止するだの. まあこんなことに一丁前にキレてたら色んなものが破滅したんですけど. 最後のやつは毎週ちゃんと見ろよと言われたらまあそうですねとなるし. それにしたってこういう状況なんだからせっかくのメリットを生かしてほしい気持ちが非常に強いですけどね. 毎週講義準備するのが恐ろしく大変だということは少しは分かるつもりではいるので,そんなに強いこと言う気にはならないんですが. さてこの欄は本編書くのに飽きたら書いてたんですが本編書き終わったので終了です.ひでえfootnoteだ.

*4:V言語でゎなぃ

*5:これはリンク以外はCPUが十分強ければまあまあ速くなります.僕の(ろくに活用されていなかった)Ryzen 3700Xがフル活用されて最高.とはいえ自作OSならクソデカライブラリ使わないのでリリースビルドでもだいぶ速く済みますけどね.

*6:Q. 別に仕様書見てるならただやるだけのものを手打ちする必要は無いのでは? A. ち,ちがうんですよ!ぜんぶじぶんでかいてみたいじゃん!

*7:ライブラリの追加もcargo-editを入れておくとcargo addだけでできて最高です.

*8:でもこれProtonが進化してVRChat上で動画が見られるようになったり,xrdesktopとかでいいかんじにXS Overlayが代替できればWin10使う理由かなり減るんですよね.Valve Softwareの異常者がんばってほしい.なんである程度は動いてるのかわかるけど意味分からんけども.あとはWebEx Trainingとかいうカスですかね.まあこっちはVMでいい気もする.そしてそもそも不自由な形式での講義動画の配布をやめればいい話.