最近自分も自作OSを 作り始めた ので、少しずつ記録を残していきたいと思う。
まずはせっかくピクセル描画レベルで0から始めたいところに立ちはだかるOSローダを最小限で作成する。
まあ本当に最小限にしようと思えば、EFI boot stubなどの方法でカーネル本体をEFIアプリケーションと認識させることでブートローダなしでも起動できてしまうが、あくまでUEFIのブートサービスを利用してファイルを読み込んで実行するという方法で作成する。
なおターゲットはx86_64、UEFI BIOSのみサポートする。QEMUでエミュレートするには、OVMF.fd
という名前のオープンソースのUEFIファームウェア実装があるのでそちらをBIOSに指定、また-hda fat:
に任意のディレクトリを指定すれば、配下をFATディスクイメージとして認識してくれる。
qemu-system-x86_64 -bios path/to/OVMF.fd -hda fat:rw:path/to/image
また、あまり低レイヤに詳しくない方のために説明すると、UEFIとは一種のAPI仕様であり、提供側は基盤にのせるファームウェア、クライアントが今回実装するローダなどのアプリケーションとなる。ファームウェアからすると 「引数に必要な情報へのポインタを入れてそちらのエントリポイントをcallするから、あとは頑張ってね」 というノリである。そのうちメモリ管理などもファームウェアの機能を止めてOSの方で行うことになるが、始めは用意してくれたUEFIの実装に頼ることにする。
ファイルを開く(EFI_FILE_PROTOCOL)
まずファイルを開くのが鬼門だった。UEFIの仕様書1とにらめっこしながら「そもそもHandleとかProtocolってなんぞや」となる。個人的にはこの辺が分かりやすかった(コード例もある)。
UEFIではHandle(ハンドル) とProtocol(プロトコル) と呼ばれるオブジェクトが中心となって登場する。Handleとは1つ以上のProtocolの集まりで、ProtocolはGUIDで紐づけられる、データや関数ポインタを含むデータ構造である。
Handleは、例えば動作中のUEFIアプリケーション、ドライバやファームウェアなどを表したり、ディスクなどの物理デバイスや仮想デバイスを表したりする。ProtocolはそれらのHandleに紐づいた機能のようなもので、分かりやすいところではブートディスクが対応するファイル読み書き用のSimple File System Protocol などがある。(こちらの表が分かりやすい)
なお以下で色々なHandle, Protocolが登場するが、私も完璧には理解していないので分かりにくいところはあるかもしれません。誤りがあれば教えてください。
ボリュームの特定
メディアアクセス用のProtocolについてのセクション(spec#page=5672)及びSimple File System Protocol(spec#page=577)を見ると、これがディスクに対するメインのファイルアクセス方法であることが分かる。
Simple File System ProtocolのメンバはRevision(バージョン番号)とOpenVolumeという関数型変数の2つのみで、後者の関数はルートディレクトリのHandle(注:に対応するFile Protocolだと思われる)へのポインタを返す。
File Protocol (spec#page=579)というのはファイルやディレクトリ自身を表すHandleに紐づき、Open, Close, Delete, Read, Writeなどの処理を行える。とりあえずこれを使えるようになればゴールっぽい (もうすでに混乱しそう。。)
つまり、こういうイメージ。
EFI_STATUS status;
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *file_system;
EFI_FILE_PROTOCOL *root_dir;
status = file_system->OpenVolume(file_system, root_dir);
// ファイル操作が可能!!
status = root_dir->Open(root_dir, &file, L"AIOS.ELF", EFI_FILE_MODE_READ, 0);
ローダ自身がいるボリューム
というわけで、ファイルを開くにはまず読み書きしたいディスク(のパーティション)=ボリュームを表すHandleを特定して、それが対応するSimple File System Protocolを使えばよさそう。基本的に今動いているローダが読み込まれたボリュームに一緒にカーネル本体を置いて読み込みたいのだが、そのHandleは一体どこにあるか?
UEFIアプリケーションのエントリポイントでは、アプリケーション自身を表すHandleと、System Tableと呼ばれる様々なサービスなどへのポインタを含むテーブルが渡される(spec#page=162)。
前述した図表を見れば分かるが、このHandleはAgent Handleの1つであるImage Handleというタイプで、Loaded Image Protocol (spec#page=353)と呼ばれるProtocolに対応している。
このProtocolのメンバに、DeviceHandle(The device handle that the EFI image was loaded from)というまさに目的のHandleがあった。 つまり、こんな具合にHandleを取得できればよさそうである。
EFI_LOADED_IMAGE_PROTOCOL *loaded_image;
loaded_image->DeviceHandle // ローダが読み込まれたデバイスを表すDevice Handleを得る
Protocolを開く
ところで、先ほどから出てきている様々なProtocolは、一体どうやって使えばいいのか?
ブートサービスの中にProtocol操作用関数の一覧(spec#page=244)が見つかる。色々あってぱっと見ではよく分からないが、続く説明を読み込むとOpenProtocol(), CloseProtocol() が主な関数であることが分かる。
(はじめはHandleの対応するProtocolデータ構造を返すだけのHandleProtocol()が使われていたが、どのHandleがそのProtocolを使っているかトラッキングするため拡張された)
他にもProtocolに対応するHandleをファーストヒットで返すLocateProtocol()などがある。今回は特定のHandleに対して検索をかけたいので使わないが、これものちに使用する。
OpenProtocol()
OpenProtocol()(spec#page=259)では、以下の6つの引数を受け取る。
型 | 名前 | 説明 |
---|---|---|
IN EFI_HANDLE | Handle | Protocolを検索したいHandle |
IN EFI_GUID* | Protocol | 検索したいProtocol(のGUID) |
OUT VOID** | Interface | Protocolデータ構造のポインタへのポインタ |
IN EFI_HANDLE | AgentHandle | Protocolを利用するAgent Handle |
IN EFI_HANDLE | ControllerHandle | (略) |
IN UINT32 | Attributes | (略) |
第3引数まではHandleProtocol()と同様で、前述した拡張が第4引数のAgentHandleなどに表れている形だ。
なおHandleProtocol()を呼ぶと、AgentHandleがUEFIファームウェアのImage Handleとして実行されるらしい。今は実行中のローダ自身のImage Handleを指定する。
これを使って何がしたかったかというと、、まずローダ自身のImage Handleが対応するLoaded Image Protocolのデータ構造を得る
EFI_STATUS status;
EFI_LOADED_IMAGE_PROTOCOL *loaded_image;
// Open the Loaded Image Protocol of the Image Handle (to get its Device Handle)
status = gBS->OpenProtocol(
image_handle,
&gEfiLoadedImageProtocolGuid,
(void**)&loaded_image,
image_handle,
NULL,
EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL
);
image_handle
はエントリポイントに渡される第1引数を利用している。
次に、そのメンバである、ローダが読み込まれたデバイスを表すDevice Handleを得て、そのDevice Handleが対応するSimple File System Protocolのデータ構造を得る
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *file_system;
// Open the Simple File System Protocol of the Device Handle obtained above
// You can get the File Protocol instance using OpenVolume provided by the SFSP.
status = gBS->OpenProtocol(
loaded_image->DeviceHandle, // ローダが読み込まれたデバイスを表すDevice Handle
&gEfiSimpleFileSystemProtocolGuid,
(void**)&file_system,
image_handle,
NULL,
EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL
);
これで、ファイルを操作できるようになった!
なお以上で述べたような構造体等をすべて自前で定義してもよいが、今回はEDK II3というオープンソースのUEFI開発用SDKを使用した
(自前で書く方法は、品川先生のツールキットを使わずに UEFI アプリケーションの Hello World! を作るなどが参考になる)。
動確済みコミット: https://github.com/ryo-naka/aios/commit/73a8a73ba67e5d18166266f530a4e70bf3248344
まとめ
想像以上に長くなってしまったので、今回はこのあたりにする。
以上の手順は、osdevのLoading files under UEFIなどにも載っているが、少々天下り的であったり情報が古かったりしたので、自分で仕様書と格闘しながら実装した手順を記した。
-
UEFI spec https://uefi.org/sites/default/files/resources/UEFI_Spec_2_8_final.pdf ↩
-
ページ番号は上記pdfをビューワで開いたときのpdfページの番号 ↩