ツールキットを使わずに UEFI アプリケーションの Hello World! を作る

UEFI アプリケーションを作成するためのツールキットには、EDK IIgnu-efi などがあります。しかし、EDK II は複雑すぎて中身の構造が分かりにくく、とっつきにくい印象があります。また、gnu-efi は wrapper 経由で関数を呼び出さなければならないなど、標準的な UEFI アプリケーションのソースコードと若干異なるものになってしまうのが気になります。

そこで、UEFI の勉強も兼ねて、これらのツールキットを使わずに、本当に最小限の Hello World を表示するだけのアプリケーションを作ってみました。

開発環境は Linux です。必要な物は PE32+ executable を作るためのクロスコンパイラです。私は Fedora を使っているので、下記のコマンドでインストールできました。他のディストリビューションを使っている場合は、適宜探してインストールしてみてください。

yum install mingw64-gcc

実際に作成してみたソースコード(main.c)は以下のとおりです。定義はコンソール出力に必要な物だけに絞っており、かなり端折ってあります。その代わり、見える部分の定義はなるべく UEFI の仕様書のとおりになるようにしてみました。

#define IN
#define EFIAPI
#define EFI_SUCCESS 0

typedef unsigned short CHAR16;
typedef unsigned long long EFI_STATUS;
typedef void *EFI_HANDLE;

struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;
typedef
EFI_STATUS
(EFIAPI *EFI_TEXT_STRING) (
    IN struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *This,
    IN CHAR16                                   *String
    );

typedef struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
    void             *a;
    EFI_TEXT_STRING  OutputString;
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

typedef struct {
    char                             a[52];
    EFI_HANDLE                       ConsoleOutHandle;
    EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *ConOut;
} EFI_SYSTEM_TABLE;

EFI_STATUS
EFIAPI
EfiMain (
    IN EFI_HANDLE ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
    )
{
    SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello World!\n");
    while(1);
    return EFI_SUCCESS;
}

EfiMain がエントリーポイントです。この関数に渡される EFI_SYSTEM_TABLE 型の SystemTable が各種システム・サービスへのポインタとなっています。その中に EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL 型の ConOut というデータがあり、オブジェクト指向のような関数テーブルとなっています。この中の OutputString という関数を呼び出すと、コンソールに文字列を出力することが出来ます。出力する文字列は Unicode なので、文字列の定義に L を付けます。

上記の main.c をコンパイルするための Makefile は以下のとおりです。

CC = x86_64-w64-mingw32-gcc
CFLAGS = -shared -nostdlib -mno-red-zone -fno-stack-protector -Wall \
         -e EfiMain

all: main.efi

%.efi: %.dll
	objcopy --target=efi-app-x86_64 $< $@

%.dll: %.c
	$(CC) $(CFLAGS) $< -o $@

qemu: main.efi OVMF.fd image/EFI/BOOT/BOOTX64.EFI
	qemu-system-x86_64 -nographic -bios OVMF.fd -hda fat:image

image/EFI/BOOT/BOOTX64.EFI:
	mkdir -p image/EFI/BOOT
	ln -sf ../../../main.efi image/EFI/BOOT/BOOTX64.EFI

OVMF.fd:
	wget http://downloads.sourceforge.net/project/edk2/OVMF/OVMF-X64-r15214.zip
	unzip OVMF-X64-r15214.zip OVMF.fd

clean:
	rm -f main.efi

ロスコンパイラとして x86_64-w64-mingw32-gcc を指定しています。他のディストリビューションでは名称が異なるようですので、適宜修正してください。

このクロスコンパイラを使って、まず普通の PE 形式のファイル main.dll を作ります。余計なものをリンクしないように、-nostdlib を付けています。また、エントリポイントとして EfiMain を指定しています。

生成された main.dll は Windows 等で使われる DLL などと同じ形式になっているので、objcopy を使って subsystem が 10 になるように変換して、UEFI アプリケーションとして認識されるようにします。


生成された main.efiqemu を使って実行してみます。

まず、qemu 用の UEFIファームウェアである OVMF.fd を入手します。http://tianocore.sourceforge.net/wiki/OVMF から最新のファームウェアをダウンロードできます。執筆時点では、r15214 でした。先ほどの Makefile を使うと自動的に入手できます。

make OVMF.fd

次に先ほどの Makefile を使って qemu を起動します。

make qemu

-nographic を指定することで、グラフィックスモードは使わずに、コンソールから起動するようにしています。-biosUEFIファームウェアのファイルを指定しています。-hda fat:image とすることで、image ディレクトリ以下を FAT フォーマットの仮想ディスクとして見えるようにしています。image 以下には EFI/BOOT/BOOTX64.EFI というファイル名で生成された EFI ファイルを置いておくことで、仮想マシンの起動時に自動的に EFI アプリケーションが実行されるようにしています。

うまくいけば、Hello World! と表示されて停止するはずです。Ctrl-a x で qemu を抜けましょう。

Boot Failed. EFI Floppy
Boot Failed. EFI Floppy 1
Boot Failed. EFI DVD/CDROM
Hello World!
QEMU: Terminated

GitHub にコードを置きました.https://github.com/utshina/uefi-simple