UEFIベアメタルプログラミング - Hello UEFI!(ベアメタルプログラミングの流れについて)

わんくま同盟 札幌勉強会 #1OSC 2017 Tokyo/SpringのLTでは発表していましたが、
最近、自作OSをUEFI+x86_64でやるために、まずはUEFIの勉強をしています。


UEFIの勉強として、UEFIの機能を呼び出すプログラム(UEFIアプリケーション)を、
EDK2やgnu-efiといった開発環境やツールキットを使わずにフルスクラッチで作ってみています。


■ Bare Metal Samples (UEFI application) - GitHub


今回は最初の記事として、Hello worldプログラムを作りながら、ベアメタルプログラミングの流れを説明します。

0. UEFIとは

UEFIは、マザーボードに元から書き込まれているソフトウェア(ファームウェア)と、そこから起動されるOSあるいはブートローダーの間のインタフェースです。


ただし、ファームウェアは、OSやブートローダーを起動するだけでなく、UEFIの仕様に従って機能を呼び出すことで、画面に文字や画像を表示するなど、色々なことができます。


ちなみに、C言語を使用します。

1. ソースコードを書く

以降で説明しますが、わんくま同盟 札幌勉強会 #1のスライドにもまとめていますので、興味があれば見てみてください。


また、今回説明する内容のソースコードは以下の場所にあります。

1.1. プロトコル

UEFIでは各機能を「プロトコル」と呼んでいます。各プロトコルの実体は構造体で、関数ポインタをメンバに持っています。この関数を呼び出すことで、UEFIの各機能を呼び出せます。


画面に文字を出力するプロトコルとしては「EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL」があります。このプロトコルは、「OutputString」という関数をメンバに持っており、この関数を使用して画面に文字を出力します。

1.2. プロトコルの取得方法

それでは、「プロトコルの構造体を取得するにはどうすれば良いのか」というと、エントリ関数の引数を使用します。(「エントリ関数」は、C言語の一般的なアプリケーションプログラムでの「main関数」に相当するものです。)


UEFIではエントリ関数の引数が仕様で決められており、その内容は以下のとおりです。

  • 第1引数: EFI_HANDLE(void *) ImageHandle
  • 第2引数: EFI_SYSTEM_TABLE *SystemTable


EFI_SYSTEM_TABLE型」は構造体で、「EFI_SIMPLE_TEXT_INPUT_PROTOCOL」構造体をメンバに持っています。
そのため、OutputString関数は以下のように呼び出せます。

SystemTable->ConOut->OutputString()


■ なお、たいていのプロトコルは、、、
なお、EFI_SYSTEM_TABLEから辿れるプロトコルは一部のみです。
その他のプロトコルには「GUID」というIDが存在し、このIDを使用してプロトコルの構造体の先頭アドレスを取得します。


GUIDからプロトコル構造体の先頭アドレスを取得するには「LocateProtocol」関数を使用します。LocateProtocol関数はEFI_SYSTEM_TABLEから以下のように呼び出せます。

SystemTable->BootServices->LocateProtocol()


今回は使いませんが、LocateProtocol関数の引数は以下のとおりです。

  • 第1引数: GUIDを指定
  • 第3引数: プロトコルの構造体の先頭アドレスを格納するポインタ変数を指定

とすることで、第3引数のポインタ変数へプロトコルの構造体の先頭アドレスを格納してくれます。
(第2引数はオプショナル)

1.2. ソースコード完成版

以上の説明から、「Hello UEFI!」と画面へ出力するソースコードは以下のとおりです。


■ hello.c

struct EFI_SYSTEM_TABLE {
	char _buf[60];
	struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
		void *_buf;
		unsigned long long (*OutputString)(struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, unsigned short *);
	} *ConOut;
};

void efi_main(void *ImageHandle __attribute__ ((unused)), struct EFI_SYSTEM_TABLE *SystemTable)
{
	SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello UEFI!\n");
	while (1);
}


ソースコードについて、
EFI_SYSTEM_TABLE構造体やEFI_SIMPLE_TEXT_OUTPUT_STRING構造体の定義では、使用するメンバのみ定義して、使用しないメンバについては「_buf」変数でアドレスの調整のみ行っています。
構造体の全貌については、UEFIの仕様書を見てみてください。
(わんくま札幌でもお話しましたが、構造体などはC言語ソースコードの形で書かれており、結構読みやすいです。)


また、戻る先が無いので、OutputString関数で画面へ文字を出力した後は、whileの無限ループで止めています。

2. コンパイルする

UEFIアプリケーションの実行ファイル形式は「PE32+」です。
Windowsで一般的な実行ファイル形式らしいのですが、私の環境はDebianなので、PE32+を生成するクロスコンパイラ(x86_64-w64-mingw32-gcc)をインストールします。


x86_64-w64-mingw32-gccは、「gcc-mingw-w64-x86-64」パッケージに入っていますので、aptでインストールします。

$ sudo apt install gcc-mingw-w64-x86-64


インストールできたら、コンパイルします。

$ x86_64-w64-mingw32-gcc -Wall -Wextra -e efi_main -nostdinc -nostdlib -fno-builtin -Wl,--subsystem,10 -o hello.efi hello.c

完璧には把握できていないのですが、「--subsystem,10」の箇所がUEFIアプリケーションを生成する指定みたいです。

3. 実行する

UEFIはFATのファイルシステムを認識します。
FAT32あたりでフォーマットしたUSBフラッシュメモリに、

という階層でディレクトリを作成し、

というファイル名で実行ファイルを配置すると、UEFIが実行してくれます。
(起動ディスクの優先順位でUSBフラッシュメモリを再優先にしておいてください。)

おわりに

UEFIでマルチコアを制御できたので、そのことを記事にしようと思ったのですが、
そもそもUEFIのプログラミングについて記事を書いていなかったので、導入を書いていたら結構な量になってしまい、記事を分けました。


マルチコア制御方法は、次回辺りに記事を書きます。
なお、ソースコードは、本記事冒頭でも説明した以下の場所にありますので、興味があれば見てみてください。

07x番台がマルチコアのサンプルです。
# ただし、まだREADMEも無く、ソースコードもちょっと読みづらいですね。
# ソースコード上部は定義ばかりなので、一番下のefi_main()から読み始めると良いです。
# (おいおい、ソースコードを分けます。。)