OS自作入門 その1(ブートローダをつくる 中編)

こんにちは、turtlekazuです。ブートローダ作成のつづきを書いていきます。

   

第2章 EDKⅡ入門とメモリマップ

EDKⅡとは

EFI Development Kit の略称で、UEFI BIOSそのものの開発やUEFI BIOSの上で動くアプリケーションの作成に使用できる開発キットのこと。Intel社が作成。

EDKⅡの中には、〇〇Pkgというパッケージ単位での各種プログラムがたくさん詰まっています。中でも「MdePkg」というパッケージが基本ライブラリであり、他のパッケージは全てこのライブラリを使用しているそうです。MdeはModule Development Environmentの略です。

   

EDKⅡを使ってブートローダ作成

パッケージ宣言ファイル(.dec)、パッケージ記述ファイル(.dsc)、コンポーネント定義ファイル(.inf)と、ソースコードである「Main.c」がMikanLoaderPkgフォルダの中に入っています。
それぞれの役割としては、
・パッケージ宣言ファイルが、MikanLoaderPkgという名前でパッケージを定義する。
・パッケージ記述ファイルが、動作するアーキテクチャを定義したり、実行ファイルの出力先、依存するパッケージのパスを定義する。
・コンポーネント定義ファイルが、出力する実行ファイルの名前(Hogeと書いたらHoge.efiという名前で出力)を決めたり、読み込むソースコードのファイル名を指定し、アプリケーションとして最初に実行する関数である「エントリーポイント」を定義する(今回の場合、Main.cの中のUefiMain()という関数)。こちらにも詳しい情報が載っていました。

ソースコード「Main.c」

#include <Uefi.h>
#include <Library/UefiLib.h>

EFI_STATUS EFIAPI UefiMain( EFI_HANDLE image_handle, EFI_SYSTEM_TABLE *system_table) 
{
  Print(L"Hello, Turtle World!\n");
  while (1);  /* 無限ループ */
  return EFI_SUCCESS;
}

Printの中の文字列は好きな言葉を入れられます。EFI_STATUS型の実体はUINTN(符号なし8byte整数)型のステータスコードだそうです。EFIAPIと書かれた部分の意味がよく分からなかったので、試しに削除してbuildしたところ全然動きました。検索しても結局存在意義に関する情報が出てこなかったので、なくてもいいのかもしれません。
edk2フォルダに、作成したMikanLoaderPkgフォルダをシンボリックリンクすることで、edk2がMikanLoaderPkgを使えるようになります。

$ cd $HOME/edk2
$ln -s $HOME/workspace/mikanos/MikanloaderPkg ./
  -> edk2フォルダのルート(./)に、MikanLoaderPkgの場所のリンクを作成

次に、edksetup.shをsourceコマンドで読み込むことで、Conf/target.txtファイルを作成し、MikanLoaderPkgの中の.dscファイルの場所をその中に書き込むことで、ビルド対象として登録するようです。

$ source edksetup.sh
項目設定値
ACTIVE_PLATFORMMikanLoaderPkg/MikanLoaderPkg.dsc
TARGETDEBUG
TARGET_ARCHX64
TOOL_CHAIN_TAGCLANG38
作成されたtarget.txtに加える変更

最後に、edk2フォルダで、buildを実行すると、Buildフォルダの中のMikanLoaderX64/DEBUG_CLANG38/X64の中に、〇〇.efiという名前の実行ファイルが生成されます。

$ cd $HOME/edk2
$ build

こちらの実行ファイルをQEMUで起動させるには、以下のようにします。

$ $HOME/osbook/devenv/run_qemu.sh $HOME/edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi

   

メモリマップについて

メモリマップは、メインメモリ(いわゆるRAM)のどの部分がどんな用途で使われているかが載っている地図。領域の大きさは、「ページ」という単位で表現され、UEFIの場合は4KiB(=4096Byte)とされている。

PhysicalStartTypeType値NumberOfPages意味
0x00000000EfiBootServiceCode30x1(=1×4KiB=4096Byte)ブートサービスドライバの実行コード
0x00001000EfiConvensionalMemory70x9F(=159×4KiB)空き領域
0x00100000EfiConvensionalMemory70x700空き領域
0x00800000EfiACPIMemoryNVSA0x8ACPI(電源管理の規格)用データ領域
:::::
メモリマップの例

UEFIの機能を使ってメモリマップが取得できるそうです。

   

メモリマップを取得するブートローダをつくる

$ cd $HOME/workspace/mikanos
$ git checkout osbook_day02b

上記のコマンドで、「osbook_day02b」というタグのついたブランチに移動することで、ソースコードが入手できます。それでは、Main.cを読み解いていきましょう。

ソースコード「Main.c」

/* メモリマップの構造体を定義 Main.c */

struct MemoryMap {  
  UINTN buffer_size;  /* メモリマップを書き込むバッファ領域の大きさ(実際よりも大きめにとる) */
  VOID* buffer;  /* メモリマップを書き込む領域の先頭ポインタ */
  UINTN map_size;  /* 実際のメモリマップの大きさ */
  UINTN map_key;  /* メモリマップの状態によって一意に決まる識別値 */
  UINTN descriptor_size;  /* メモリマップの各要素(行)のサイズ(バイト数) */
  UINT32 descriptor_version;  /* メモリディスクリプタの構造体のバージョン(特に使わない) */
}

※UINTN:8byteの符号なし整数、UINT32:4Byteの符号なし整数

/* メモリマップを取得する関数 Main.c */

EFI_STATUS GetMemoryMap(struct MemoryMap* map) {  /* メモリマップ型のポインタ変数mapを引数に取る */
  if (map->buffer == NULL) {
    return EFI_BUFFER_TOO_SMALL;  /* バッファサイズがNULLだと警告を出す */
  }

  map->map_size = map->buffer_size;  /* マップのサイズにバッファのサイズを代入 */
  return gBS->GetMemoryMap(  /* UEFIが用意してくれている関数を利用する */
    &map->map_size,
    (EFI_MEMORY_DESCRIPTOR*)map->buffer,  /* EFIのディスクリプタの機能を利用 */
    &map->map_key,
    &map->descriptor_size,
    &map->descriptor_version
  );
}

変数の頭に&をつけたものは、その変数のアドレスを意味する。

/* メモリマップをファイルに保存する関数 Main.c */

EFI_STATUS SaveMemoryMap(struct MemoryMap* map, EFI_FILE_PROTOCOL* file) {  /* 取得したメモリマップとファイルの雛形を引数に取る */
  CHAR8 buf[256];  /* 要素数256個の文字配列 */
  UINTN len;

  /* 表の見出しを登録 */
  CHAR8* header =  "Index, Type, Type(name), PhysicalStart, NumberOfPages, Attribute\n";
  len = AsciiStrLen(header);
  file->Write(file, &len, header);  /* ファイルに書き出し */

  Print(
      L"map->buffer = %08lx, map->map_size = %08lx\n",  /* メモリマップの先頭ポインタとマップサイズを表示 */
      map->buffer,
      map->map_size
  );

  EFI_PHYSICAL_ADDRESS iter;
  int i;
  for (iter = (EFI_PHYSICAL_ADDRESS)map->buffer, i = 0;  /* iterの初期値は先頭アドレス */
       iter < (EFI_PHYSICAL_ADDRESS)map->buffer + map->map_size;  /* iterが先頭アドレス+マップサイズよりも小さい時 */
       iter += map->descriptor_size, i++)  /* iterは要素サイズ分加算 */
  {
    EFI_MEMORY_DESCRIPTOR* desc = (EFI_MEMORY_DESCRIPTOR*)iter;  /* EFIのディスクリプタの機能を利用 */
    len = AsciiSPrint(  /* メモリディスクリプタの中身を文字列に変換 */
        buf, sizeof(buf),
        "%u, %x, %-ls, %08lx, %lx, %lx\n",  /* 下に列挙した値を代入 */
        i, desc->Type, GetMemoryTypeUnicode(desc->Type),
        desc->PhysicalStart, desc->NumberOfPages,
        desc->Attribute & 0xffffflu);
    file->Write(file, &len, buf);  /* ファイルに書き出し */
  }

  return EFI_SUCCESS;
}

※CHAR8は8bit文字、%08lxは小数点8桁の倍精度整数(16進数)

/* メイン関数 Main.c */

EFI_STATUS EFIAPI UefiMain( EFI_HANDLE image_handle, EFI_SYSTEM_TABLE* system_table) {
  Print(L"Hello, Turtle World!\n");  /* おなじみの文字列出力 */

  CHAR8 memmap_buf[4096 * 4];  /* 4ページ分の領域を確保 */
  struct MemoryMap memmap = {sizeof(memmap_buf), memmap_buf, 0, 0, 0, 0};
  GetMemoryMap(&memmap);

  EFI_FILE_PROTOCOL* root_dir;  /* ファイルの作成に関するプロトコル(EFI_FILE_PROTOCOL)に準拠 */
  OpenRootDir(image_handle, &root_dir);  /* ルートディレクトリを開く */

  EFI_FILE_PROTOCOL* memmap_file;
  root_dir->Open(
    root_dir,  /* 自分自身(ルートディレクトリ) */
    &memmap_file,  /* 新しいファイルを設置する場所(ポインタ) */
    L"\\memmap",  /* 「memmap」というファイル名にする*/
    EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE, /* 読み/書き/作成モード */
    0  /* ファイル属性は標準(参考) */
  )

  SaveMemoryMap(&memmap, memmap_file);
  memmap_file->Close(memmap_file);

  Print(L"All done\n");  /* 完了の表示 */

  while (1);
  return EFI_SUCCESS;
}

こちらの記事が非常に詳しく解説してくださっていたので、参考にさせていただきました。

   

ブートローダのビルド&起動

以下のコマンドでパッケージをビルドします。

$ cd $HOME/edk2
$ source edksetup.sh
$ build

これで、実行ファイル(Loader.efi)が生成されるので、これをQEMUで起動します。

$ $HOME/osbook/devenv/run_qemu.sh Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi

マップサイズは(0x)00000840とのことなので、2112Byteという理解でいいんですかね。
しかし、Printで表示されているのはメモリマップの先頭ポインタとマップサイズだけです。作成されたファイルを見るには、Loader.efiを元に上記コマンドで作成されたイメージファイル(disk.img)の中にアクセスする必要があります。そのためには、このイメージファイルをシステム(QEMU”を”起動しているOS)にマウントします。

$ mkdir -p mnt
  -> mntディレクトリを作成(-pをつけると既存でもエラーにならない)
$ sudo mount -o loop disk.img mnt
  -> mntディレクトリにdisk.imgをマウント
$ ls mnt
  -> mntディレクトリの中身を列挙

作成したファイルは、「memmap」という名前なので、以下のコマンドで中身を見ることができます。

$ cat mnt/memmap
  -> mnt/memapの中身をターミナルに表示

中身が無事に確認できたら、忘れずにマウント解除しておきます。

$ sudo umount mnt

   

結び

面倒な関数はEDKⅡが用意してくれているため、EDKⅡを使うことで楽にEFIプログラムを作成できるんですね。
自作感を追求したい人はこのような開発キットすら使わずにOS開発をされるみたいですね。びっくりです。
メモリの中身がどうなってるかとか考えたことすらなかったので、メモリマップを見れたのは鮮烈な経験になりました。

引き続き、ブートローダに加えて画面表示を行うカーネルをつくっていきたいと思います。

turtlekazu

モノづくりが大好きです。オーディオや車、ガジェットなども。ミシシッピニオイガメを飼育中。最近、ほうじ茶にハマっています。

おすすめ記事

コメントを残す

メールアドレスが公開されることはありません。