くろねこ日記

ソフトウェアに関する技術メモが多いです.

macos上でUEFI(64bit)の開発環境を整えてみた

はじめに

ご無沙汰してます. UEFIをつかった開発環境をmacで整えてみましたのでそのメモです.

UEFIはPE32+の形式になっており単にgccをたたくだけでは難しいです. コンパイルの方法を調べるとlinux環境上でmingwコンパイラを導入してそこからビルドしているような例を見かけました.そこで,dockerでビルドしてqemuを起動できればmacの上でも一応開発が進められるのでは?と思い調べた結果とりあえずhelloworld出力まではいったのでメモしておきます.

簡単に作業ステップをまとめておきます.

1. dockcrossのwindows-x64の実行ファイルを導入する
2. windows-x64をつかってefiファイルを生成
3. OVMF.fdとqemuの導入
4. 実行

なお,開発環境は以下のようになっております.

OS: macOS High Sierra(64bitのOSです)
マシン: Mac mini (Late 2012)
プロセッサ: 2.3 GHz Intel Core i7
メモリ: 16 GB 1600 MHz DDR3

1. dockcrossのwindows-x64の実行ファイルを導入する

mingwコンパイラをdockerで動かすときにdockcrossというリポジトリが非常に便利でしたので今回はそれをつかいました.

GitHub - dockcross/dockcross: Cross compiling toolchains in Docker images

dockerを導入していない方はdocker for macを導入することをおすすめします.

Docker For Mac | Docker

dockcrossのリポジトリにあるREADMEに基本的にしたがって導入

$ git clone https://github.com/dockcross/dockcross.git
$ cd dockcross
$ docker run --rm dockcross/windows-x64 > /path/to/dockcross-windows-x64

これで/path/to以下にdockcross-windows-x64が追加されればOKです.

2. windows-x64をつかってefiファイルを生成

efiファイルはCでコードを書く必要があるので元となるソースを用意します.ここではmain.cというファイルを/path/to/に配置するものとしておきます.無事動けば「Hello World!!!」が画面に表示されるはずです.

typedef unsigned short uint16;
typedef unsigned long long uint64;

typedef struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
    char reset[8];
    uint64 (*OutputString)(struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This, uint16 *String);
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;

typedef struct {
    char    efi_table_header[24];
    char    firmware_vendor[8];
    char    firmware_revision[4];
    char    console_in_handle[8];
    char    con_in[8];
    char    console_out_handle[8];
    EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *ConOut;
} EFI_SYSTEM_TABLE;

void efi_main(void *ImageHandle, EFI_SYSTEM_TABLE  *SystemTable)
{
    EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *con_out = SystemTable->ConOut;
    con_out->OutputString(con_out, L"Hello World!!!\n");
    while(1);
}

ざっとコードを解説します.まず見るべきはefi_mainという関数でしょう.この関数が本プログラムのエントリポイントとなります.通常,Cではmain関数を指定しますが,uefiの場合は任意のエントリポイントを指定できますので今回はefi_mainとしました.プログラム起動の際にはefi_mainの関数の2つの引数にUEFIから値がわたってきます.第2引数には様々な機能をもった関数があり,画面に文字列を描画する関数もあります.その関数をつかって"Hello World!!!"を表示しています.

EFI_SYSTEM_TABLEの構造体のうち画面に文字列を描画するのに必要な関数のみ使えば良いことからConOutを除く値については合計60バイトを確保するだけにしました.まとめて60バイト分だけ確保しても良かったですが各サイズを確認しながらコーディングしたかったので個別に定義しました. EFI_SIMPLE_TEXT_OUTPUT_TABLEにおいても同様にOutputStringさえあれば良いので最初の8バイト目だけを確保して実際には使っておりません.

3. OVMF.fdを導入してqemuで起動

OVMFとは「Open Virtual Machine Firmware」の略で仮想マシンの上でUEFIを実行するためのものらしいです. OVMFは以下のリンクからダウンロードできます.

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

伸長したらmain.cと同じように/path/toの直下にOVMF.fdを配置してください.

qemuはhomebrewをつかって導入ができますので以下のコマンドで導入してください

$ brew install qemu

4.実行

下記のコマンドを実行して/path/to/EFI/BOOT/BOOTX64.EFIというファイルを生成しましょう.BOOTX64.EFIというファイル名はUEFIが起動するときに読みにいくファイル名となっており,システムが起動したあと勝手に読んでくれるようになります.

$ mkdir -p /path/to/EFI/BOOT/
$ /path/to/x86_64-w64-mingw32-gcc bash -c '$CC -e efi_main -nostdlib -Wl,--subsystem,10 -o /path/to/EFI/BOOT/BOOTX64.EFI /path/to/main.c'

なお,gccの引数については以下の通りです.

-e efi_main・・・通常Cのコードではmain関数がエントリーポイントになりますが,efiについては自らエントリーポイントを指定する必要があります.サンプルではefi_mainという関数がをエントリーポイントとして作成したのでコンパイルするときに指定しています.
-nostdlib・・・標準ライブラリを呼ばない引数です.UEFIの場合は標準ライブラリを使用しないのでこのような引数をわたす必要があります.
-Wl・・・リンカに渡すオプションとして--subsystem,10を渡しています.「--subsystem,10」自体よくしらないのですが,どうやらsubsystem,10とするとUEFI向けの実行ファイルを生成してくれるようです.
-o ・・・ファイル出力名を指定してます.この場合は/path/to/EFI/BOOT/BOOTX64.EFIを指定してます.

次にqemuで実行しましょう.

$ qemu-system-x86_64 -bios /path/to/OVMF.fd -hda fat:/path/to/

-bios でOVMF.fdを指定することで仮想環境上でUEFIを再現できるようです. fat:/path/to/というのはEFIディレクトリのあるパスを指定しています.仮想環境が起動するとここがルートディレクトリとなり,システムはEFI/BOOT/以下にあるBOOTX64.EFIを探しで実行してくれるでしょう.

まとめ

UEFIを仮想環境で再現してHello World!!!を出力する手順を紹介しました.

参考

Specifications | Unified Extensible Firmware Interface Forum

UEFIベアメタルプログラミング - Hello UEFI!(ベアメタルプログラミングの流れについて) - へにゃぺんて@日々勉強のまとめ

ツールキットを使わずに UEFI アプリケーションの Hello World! を作る - 品川高廣(東京大学)のブログ

雑記

webフレームワークに興味のある人がいれば珍しくオープンソースで公開してるので見ていただけるとうれしいです.気が向いたときにゆったり開発してます.

web上で動作するtexコンパイルシステムを構築しており,その開発にkettleを採用しています(システムをつくっている間に締切が過ぎていましたが...).セッション処理周りはbeakerをつかっているのですが,やっぱりセッション管理くらいできたほうが良いよなと思いつつ外部ライブラリには頼らず作りたいなという思いもあって決めかねています.

https://github.com/HAYASAKA-Ryosuke/kettle