ブートローダーは4行で実装される

この記事はUEFI Advent Calender自作OS Advent Calenderのためにかかれました。
22日(+3日)の記事です。

遅れてもうしわけないです。

ブートローダー

まず、自作OSにはブートローダーによってカーネルをロードしなければはじまりません。
特段複雑なローダーがなくとも、たとえばBIOSから直接起動するのなら
そのための形式で用意せねばなりません。
しかし、これが自作の入口としてかなりの関門になってしまっているのではないでしょうか。

なので自作OSからはちょっと離れますが、ブートローダーについて書きます。

UEFI

ということで、UEFI Appsとしてブートローダーを実装する話です。
まず、UEFIとは何かについてはUEFI Advent Calender 1日目の記事を、
そしてこの作業の前知識としてUEFI Advent Calender 12日目の記事を参照していただけると良いです。

一応ざっとUEFIについて触れておくと、
BIOSの代替規格のファームウェアで、多機能かつ高機能。また、可搬性も考えられています。

UEFIのファームウェア上で直接動作できるUEFI Appを記述するには、
tianocoreがBSDLで公開しているEDKというツールキットか、
Gnu toolchainと合わせて使えるgnu-efiというBSDLのライブラリを用います。
EDKでも一応gcc等が使えます。

EDKは、パッケージとして豊富にライブラリが揃っており、libcやbsd socket、pythonがつかえます。
gnu-efiは、Linux/OS Xの上で開発するにはEDKより手軽にはじめる事ができます。

UEFI AppはWindows同様PE形式のバイナリなので、
実はEDKやgnu-efiがなくとも*.dllなバイナリ(つまりrelocatableでsharedなPEバイナリ)を作成してから、objcopyコマンドとかで必要な部分だけ取り出して拡張子がefiのファイルにするとかでも一応動作できるバイナリを作る事は可能です(が、面倒です)

UEFIのAPIであるProtocolは初期状態でもFile I/OやGraphicsの処理まで揃っていますが、
自分でUEFI Driverを実装してこのドライバをロードすればProtocolを増やせます。
今回はDriverは実装しません。

実装

さて、そしてUEFI Appとして実装されたブートローダーがこちらになります。(料理番組風)
後でコードの説明をするので、最初は読み飛ばしても結構です。

いきなりコード貼ってしまいましたが、そんなにコード量はないですね。

Protocol

さて、UEFI Appですが、uefi_mainというエントリポイントが
EFI_IMAGE_HANDLE、EFI_SYSTEM_TABLEという引数をとって起動していますね。
InitializeLibを行なっているのは、gnu-efi独特の作法です。

その後早速Print();なんてやってますが、これでいきなり文字列出力ができてしまいます。
単純ですね。

その次からが、肝心のブートローダーです。

UEFIのAPIは、ただ関数コールすれば良いものではありません。
全てのProtocolには、それぞれの構造体が定義されています。
BIOSメーカーでありUEFIコンソーシアムの参加企業でもあるPhoenixのwikiに、
構造体の一覧が見やすく書いてあるのでこれを見ると良いです。

UEFIのProtocolは、まず使いたいProtocolと同名の型の構造体を宣言し、
OpenProtocol();にその構造体と、使いたいProtocolのGUIDと、いくつかのオプションを渡すことで、
渡した構造体のメンバに欲しい情報であったりとか、必要な関数ポインタが代入されます。

そこで、その情報を使うなり、関数ポインタから関数を呼ぶなりして、Protocolは使われます。

しかし、OpenProtocl();自体もある種のProtocolの関数であるため、そのような関数がグローバルに
宣言されているわけではないです。

ではどうやってProtocolを呼ぶかというと、UEFI Appの起動時に
BSという大域変数(EDKの場合はgBS)にEFI_BOOT_SERVICEのProtocol構造体に
必要な情報が詰められて最初から使えるようになっています。
よって、BS->OpenProtocol();のようにして、Protocolを呼び出します。

このブートローダーのコードでは、Print();の次にuefi_call_wrapper();という関数の
第一引数にBS->OpenProtocolを渡していますが、
これはUEFI Protocolの呼び出しが一般的なC言語の関数呼び出し(cdecl規約)ではなく
Win32API同様のstdcall規約を用いているためのwrapperです。

uefi_call_wrapper(つかいたいProtocolの関数ポインタ, その関数の引数の数, その関数への引数);
という使い方になります。

ブートローダー仕組み

では、ブートローダーの本格的な部分である17行目以降から解説します。

まず、エントリポイントの引数であったEFI_IMAGE_HANDLE型のImageHandleを使って、
EFI_LOADED_IMAGE_PROTOCOLを構造体LoadedImageParentに入れています。

このIMAGEというのは、実行メモリイメージの事(だと思われます)
ImageHandleについては実行している自分自身のハンドルになります。

LOADED_IMAGE_PROTOCOLというのは、自分自身やBS->LoadImage()によって
既にメモリ上にロード済みのイメージに対して情報を得る事ができます。

続く23行目では先程入手した自分自身のハンドルのDevicePathを使って、
\vmlinuzというファイルパスのファイルをEFI_DEVICE_PATH型の変数に代入しています。

これはどういうことかというと、
自分自身はブートローダーなので、かならずEFIのシステムパーティションに置かれています。
このパーティションはHDDの先頭のパーティションでFAT32です。
なので、自分自身のDevicePathはこのシステムパーティションのDeviceそのものを指す事になります。
更に、\vmlinuzということなので、このシステムパーティションのroot directoryにあるvmlinuzファイルについてのDevicePathをPathという変数に格納した事になります。
このDevicePathはブートローダーがカーネルをロードするのに用います。

29行目のBS->LoadImage()で呼び出しているのがそれです。これでカーネルはメモリに展開されています。

46行目のBS->StartImage()でこのロードされたイメージを起動するだけで、カーネルは起動されます。

29行目と46行目の間の処理は、ロードしたカーネルイメージについてLOADED_IMAGE_PROTOCOLを手に入れ、LoadOptionsというメンバに起動オプションを
指定することで、カーネルコマンドラインを指定しています。

自作OSをつくる上では、エラー処理や後処理を省くと、
17行目 23行目 29行目 46行目
この4行の処理のみでブートローダーは完成してしまっています。

さらに、USBメモリなどでEFIシステムパーティションのみで完結させる構成のディレクトリに
このようなブートローダーと自作カーネルとあと必要なバイナリや設定ファイルを適当に配置して
しまうだけで、おそらくさくっと自作OSの入口に立つ事が可能でしょう。



と、いうことで、自作OSをしたい方は是非、UEFIを前提に考えてみてください!

なお、このブートローダーでLinuxカーネルを起動してみたい場合、
EFIシステムパーティションのroot directoryにvmlinuzとrenameされたカーネルを配置し、
11行目のカーネルコマンドラインを自分の環境に合わせて書きなおしてください。
また、initrdについては度外視しているので、カーネルにroot partitionのファイルシステムのドライバを組み込んでおかないと、起動時にroot partitionがmountできずカーネルパニックを起こします。

このブートローダー自体は、EFIシステムパーティションの好きな所において、
bcdeditなりblessなりefibootmgrなりでUEFIにそのPathを登録するか、
UEFI Shellから直接このバイナリをたたいてやってください。

上記コードのMakefileも含めたリポジトリはこちらになります。
https://github.com/orumin/SimpleMyLoader