Edited at

gnu-efiでUEFI遊びをはじめよう

More than 1 year has passed since last update.

こちらはNTTコミュニケーションズアドベントカレンダー9日目の投稿です(1日遅れてすみません)。

新入社員です。面白そうなので参加させていただきました。

趣味は組み込み系の開発です。学生の時はGSoC2016でGregさんに教わりながらマイコンでLinux動くようにしたり、U-Bootにパッチ投げたりしていました。

最近、趣味でUEFIアプリを作っていたので、忘れないうちにUEFIアプリの基本的なところについて書こうと思います。

なお、OSはArch Linuxを使っているものとします。


UEFIとは

UEFIとは、一言で言うと「最近のPCやサーバーに入っている、新しくて高機能なファームウェア(及びそのインターフェースの仕様)」です。

体感では、2世代目Core iシリーズのCPU以降が入っているマシンには、このUEFIが従来のBIOS(以降、BIOSと表記)の代わりに、または選択可能な状態で入っているように感じています。


UEFIの特徴

UEFIはBIOSに比べ様々な利点がありますが、個人的には以下の3つを大きな特徴と感じています。


  1. 仕様がオープン

  2. プログラムがC言語で記述可能

  3. マルチプラットフォーム


1. 仕様がオープン

UEFIはBIOSに比べ多くの機能をもち、その機能と利用方法等の多くはオープンな仕様として決まっています。

これはユーザーが自由に利用参照及び利用が可能で、仕様の詳細は以下のサイトから読むことができます。


2. プログラムがC言語で記述可能

BIOSでは一部で必ずアセンブリ言語を利用してプログラムを書く必要がありましたが、UEFIでは基本的にC言語のみでプログラムを書くことができます。


3. マルチプラットフォーム

UEFIはx86のマシンだけではなく、最近話題のARM64サーバー機などでも使われています。

UEFIの仕様はハードウェアに依存しないため、コードは異なるCPUアーキテクチャでも動作させることが可能です。もちろん、ハード固有のコードがなければという条件はつきますし、コードはCPUで直接実行されるので再コンパイル等は必要です。

UEFIは非常に多くの機能を持つため、OSの起動だけではなく、いろいろな遊びが可能です。

例えば、DOOMを移植している方や、poiOSというOSっぽいものを作っている方々がいらっしゃいます。

前置きが長くなりましたが、今回の内容はこのUEFIを使ったアプリの作り方の基本的なところを学ぼうという内容になります。


UEFI開発環境構築

UEFIアプリ開発は最低限仕様書とコンパイラがアレばできてしまうのでやっている方もいらっしゃるのですが、大変なので一般的にはツールキットを使って作ります。

ツールキットはEDK2とgnu-efiという2つが有名です。

EDK2は多機能でLibcなども使えるというメリットがありますが、その分規模が大きく、ビルドの方法なども少し複雑でわかりづらいです。

gnu-efiはEDK2に比べると規模が小さく、Makefileを使ったビルドができるためわかりやすいです。一方、機能はEDK2に比べると少ないので、努力でカバーが必要です。

今回はgnu-efiを使って解説を行いたいと思います。


gnu-efiのインストール

以下のサイトより最新のgnu-efiを取得してください。

本記事執筆時点のバージョンはv3.0.6でした。

次に、以下のコマンドでビルドを行います。

tar jxvf gnu-efi-3.0.6.tar.bz2

cd gnu-efi-3.0.6
make apps

これでライブラリとサンプルプログラムのビルドは完了です。

この後 sudo make install とするとライブラリがシステムにインストールされますが、今回はサンプルをいじっていくだけの予定なので、行わなくてOKです。


qemu用UEFIイメージの取得

UEFIアプリのテストを実機で行うと大変なので、qemuでテストを行います。

(※私のメインPC(thinkpad x201)はまだUEFI非対応なので、そもそもqemuじゃないと試せない...)

qemuのbiosとして、UEFIのファームウェア(OVMF)イメージを与えることで、UEFIアプリのテストが可能になります。

ところでこのOVMFですが、x86向けに関して以前は最新のビルド済みバイナリが提供されていたはずですが、現在は提供がなくなっているようです。

仕方がないので、Arch Linuxのパッケージでインストールします。

sudo pacman -S ovmf


UEFI Hello world!!

gnu-efi-3.0.6/x86_64/apps 以下にあるサンプルを動かしてみましょう。

gnu-efi-3.0.6/x86_64/apps に移動した後、まず以下のコマンドでカレントディレクトリをHDDとしてマウントしつつ、qemuを起動します。

qemu-system-x86_64 -cpu qemu64 \

-drive if=pflash,format=raw,unit=0,file=/usr/share/ovmf/ovmf_code_x64.bin,readonly=on \
-drive if=pflash,format=raw,unit=1,file=/usr/share/ovmf/ovmf_vars_x64.bin \
-hda fat:rw:.

しばらく待つとUEFI Shellが立ち上がってくるので、以下のコマンドでサンプルアプリを起動します。

Shell> fs0:

FS0: \> t2.efi
Hello Wolrd!


サンプルコードを読む

先程動かしたサンプルt2.efiのコードは gnu-efi-3.0.6/apps/t2.c にあります。実際に中身を見てみましょう。

なお、このコードのライセンスはBSDライセンスです。

#include <efi.h>

#include <efilib.h>

EFI_STATUS
efi_main (EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
{
SIMPLE_TEXT_OUTPUT_INTERFACE *conout;

InitializeLib(image, systab);
conout = systab->ConOut;
uefi_call_wrapper(conout->OutputString, 2, conout, L"Hello World!\n\r");

return EFI_SUCCESS;
}

まずmain関数です。

EFI_STATUS

efi_main (EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)

UEFIアプリではefi_main関数がmain関数になります。

引数はEFIハンドルのimageと、EFI_SYSTEM_TABLEのポインタsystabです。

戻り値はEFI_STATUSにEFI_SUCCESSなど、処理の結果を返します。

InitializeLib(image, systab);

次におまじないです。gnu-efiではefi_mainの最初に必ずやる必要があります。

SIMPLE_TEXT_OUTPUT_INTERFACE *conout;

conout = systab->ConOut;

次にシステムテーブルからEFI_SIMPLE_TEXT_OUTPUT_PROTOCOLを取り出します。

(SIMPLE_TEXT_OUTPUT_INTERFACEとなっていますが、中身はEFI_SIMPLE_TEXT_OUTPUT_PROTOCOLです)

uefi_call_wrapper(conout->OutputString, 2, conout, L"Hello World!\n\r");

最後に、EFI_SIMPLE_TEXT_OUTPUT_PROTOCOLのOutPutStringにUTF-16の出力文字列を与えれば標準出力に文字列が出力され、「Hello world」の出力が得られます。

以上でサンプルの解説は終了ですが、「ハンドラ」や「プロトコル」など、意味のわからないことが多いと思います。次の節ではこれらについて解説を行います。


UEFIのハンドラ、プロトコルとは...?


プロトコルとは?

そもそもUEFIのアプリケーションは、メモリの何処かに置かれているUEFIの用意した関数を呼び出して処理を行っていっていくものです。

これらの関数はプロトコルという単位で区切られ、これら関数への関数ポインタやGUIDなどが詰まった構造体として、メモリの何処かに置かれています。

SIMPLE_TEXT_OUTPUT_INTERFACE *conout;

conout = systab->ConOut;

この構造体のアドレス取得を行っているところが、サンプルのこの部分になります。

プロトコルの構造体に登録された関数の詳細は仕様書に書かれているので、仕様書を読みながら適切に関数を呼び出せば、その関数を使うことができます。

例として、サンプルで使ったEFI_SIMPLE_TEXT_OUTPUT_PROTOCOLの定義を見てみましょう。

http://wiki.phoenix.com/wiki/index.php/EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL

このプロトコルにあるOutputString関数に、第1引数としてプロトコルのアドレス、第2引数にUTF-16の出力文字列を与えると文字列が出力されます。これを行っているのが以下の部分になります。

uefi_call_wrapper(conout->OutputString, 2, conout, L"Hello World!\n\r");

ここで uefi_call_wrapper が何者かについて気になると思います。

この詳細を説明するにはABIの話をしなければならないのですが、今回は簡単のために「gnu-efiではuefi_call_wrapperマクロを使って、プロトコルに登録された関数呼び出しを行う必要がある」程度に考えていただければと思います。

uefi_call_wrapper マクロの書式は以下のようになっています。

uefi_call_wrapper( 

呼び出すプロトコル関数ポインタ,
関数に渡す引数の数,
関数の引数1,
関数の引数2,
...,
関数の引数N
);

例えば、OutputStringの引数は2つなので、uefi_call_wrapper の第2引数には 2 をいれて、その後に関数の引数を2つ入れています。


ハンドラとは?

サンプルのように、一部プロトコルは既に開かれた状態で登録されているため、何処かからそのプロトコルのアドレスをもらってくるだけでつかえます。

一方、その他多くのプロトコルはプロトコル自体を開くところから始める必要があります。そこで必要になるのがハンドラです。

任意のプロトコルを呼び出すためには、まずそのプロトコルの呼び出しが可能なハンドラを取得する必要があります。これを行うのが、BOOT SERVICEに登録されているLocateHandle関数です。

http://wiki.phoenix.com/wiki/index.php/EFI_BOOT_SERVICES#LocateHandle.28.29

この関数に使いたいプロトコルのGUIDを入れて呼び出すと、ハンドラを得ることができます。

次のハンドラからプロトコルの取得は、同じくBOOT SERVICEのHandleProtocol関数で行えます。

http://wiki.phoenix.com/wiki/index.php/EFI_BOOT_SERVICES#HandleProtocol.28.29

呼び出し方の詳細は以前自分のブログにサンプルを書いたので、こちらを参照してください。

http://tnishinaga.hatenablog.com/entry/2017/10/29/034343

以上で任意のプロトコルの取得が終わり、プロトコルに登録された関数が利用可能となります。


今後UEFIで遊ぶときのアドバイス

最後に、私がUEFIアプリを作って遊ぶときにやってる事や、見てる情報を色々書いておこうと思います。


プロトコルを探す

UEFIアプリづくりの最初は、使えそうなプロトコルを探すところから始めています。

実現したい機能の大抵は、直接ハードウェアのペリフェラルを叩くコードを作る必要なく、UEFIのプロトコルをつかえば実現できますので、以下のサイトや仕様書などを眺めて使えるプロトコルが無いかを調べることを最初にやるのがおすすめです。

http://wiki.phoenix.com/wiki/index.php/Category:UEFI_2.0


資料の場所

仕様は公式の仕様書を読むか、ちょっと古いですがPhoenixのサイトを読むのが良いです。

実際の使い方については、大神さんの書かれている同人誌がまとまっていておすすめです。

その他では、以下のあたりを参照していくのがおすすめです。


おしまい

最後まで読んでいただきありがとうございました。

内容に関し大きな誤り等がアレばコメント欄でご指摘のほどよろしくお願いします。

本記事が少しでもみなさまのUEFI遊びのお役にたてれば幸いです。


追記

修正依頼をいただいて、この機能を初めて知りました。

依頼していただいた方ありがとうございました!