前回に続き、「ゼロからのOS自作入門」を読み進め、少し進捗があったので書いていきたいと思います。
自分のペースで進めているため、day02bとday03aという変な区切れ目になっていますがお許しください。

1. 実行環境

  • MacBook Air 2020

    • macOS Monterey 12.4
    • Chip: Apple M1 (arm64)
    • RAM: 8GB
  • 導入環境など

2. 今回やった範囲

  • day02b
  • day03a

3. OSの名前

せっかくなので自分で作ってるOSにも独自に名前をつけようと思い、
miCanopus(ミカノープス)」という名前にしました。

教科書で扱っている「MikanOS」(あるいは本の愛称である「みかん本」)と、3年ほど前に「30日でできる!OS自作入門」を基に自作していた「CanopusOS」からとって名付けました。

というわけで、今後miCanopusをよろしくお願いいたします。

4. day02b: メモリマップの内容を出力

OSが扱っているメモリマップの内容を出力させました。

少々苦労したのが、Macだとmountコマンドで「-o loop」が使えなかったところ。
結局、MacではFinderでimgファイルをダブルクリックするとマウントしてくれるので、mountコマンドは使わずに、ダブルクリックでマウント→マウントしたimgを開く→中身を確認、という手順で行いました。

スクリーンショット 2022-09-29 0.33.25
スクリーンショット 2022-09-29 0.33.40

中身をcatコマンドで見てみます。

% cat /Volumes/MIKAN\ OS/memmap 
Index, Type, Type(name), PhysicalStart, NumberOfPages, Attribute
45, 6, EfiRuntimeServicesData, FFC00000, 400, 1

メモリマップが書き出せている様子が確認できました。

5. day03a: 各レジスタの値を表示&カーネル作成

5.1 レジスタの中身を見る

miCanopus起動後、qemuでinfo registersを実行し現在のレジスタの状態を見てみる。

(qemu) info registers
RAX=0000000000000000 RBX=0000000000000001 RCX=000000003fb7b1c0 RDX=0000000000000002
RSI=000000003fea92a0 RDI=0000000000000400 RBP=000000000000002e RSP=000000003fea8870
R8 =000000003fea8678 R9 =000000003f20011e R10=0000000000000000 R11=000000000000ffff
R12=000000003fea8900 R13=000000003e696806 R14=000000003e696918 R15=000000003e7a76a0
RIP=000000003e69656b RFL=00000202 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
CS =0038 0000000000000000 ffffffff 00af9a00 DPL=0 CS64 [-R-]
SS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
FS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
GS =0030 0000000000000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
LDT=0000 0000000000000000 0000ffff 00008200 DPL=0 LDT
TR =0000 0000000000000000 0000ffff 00008b00 DPL=0 TSS64-busy
GDT=     000000003fbee698 00000047
IDT=     000000003f306018 00000fff
CR0=80010033 CR2=0000000000000000 CR3=000000003fc01000 CR4=00000668
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000 
DR6=00000000ffff0ff0 DR7=0000000000000400
EFER=0000000000000500
FCW=037f FSW=0000 [ST=0] FTW=00 MXCSR=00001f80
FPR0=0000000000000000 0000 FPR1=0000000000000000 0000
FPR2=0000000000000000 0000 FPR3=0000000000000000 0000
FPR4=0000000000000000 0000 FPR5=0000000000000000 0000
FPR6=0000000000000000 0000 FPR7=0000000000000000 0000
XMM00=0000000000000000 0000000000000000 XMM01=0000000000000000 0000000000000000
XMM02=0000000000000000 0000000000000000 XMM03=0000000000000000 0000000000000000
XMM04=0000000000000000 0000000000000000 XMM05=0000000000000000 0000000000000000
XMM06=0000000000000000 0000000000000000 XMM07=0000000000000000 0000000000000000
XMM08=0000000000000000 0000000000000000 XMM09=0000000000000000 0000000000000000
XMM10=0000000000000000 0000000000000000 XMM11=0000000000000000 0000000000000000
XMM12=0000000000000000 0000000000000000 XMM13=0000000000000000 0000000000000000
XMM14=0000000000000000 0000000000000000 XMM15=0000000000000000 0000000000000000

次に、RIPレジスタが指し示しているアドレス(0x3e69656b)にある命令を見てみる。

(qemu) x /4xb 0x3e69656b
000000003e69656b: 0xeb 0xfe 0x48 0x83

これを逆アセンブルしてみる。

(qemu) x /2i 0x3e69656b
0x000000003e69656b: Asm output not supported on this arch

「このアーキテクチャでは逆アセンブルは対応していない」的なことを言われてしまった。
Apple M1で動かしてるせいなのか?解決法は見つからず。

5.2 カーネルを書く

今まではブートローダたるUEFIアプリケーションのコードに直接処理を書いていたのだけど、ここからはいよいよカーネルを書いていきます。起動時に行われる手順としては、こんな感じ。

1. UEFIがブートローダを読み込み&起動
2. ブートローダがカーネルを起動
3. 以後、カーネルが制御

カーネルはOSを司るコアな部分で、ユーザアプリケーションに対しシステムコール、プロセス切り替え、ファイルやデバイスとの入出力といった基本的な機能を実行する重要な部分です。

とりあえず今回は、本に従って「起動したら処理を停止させる」だけのカーネルにしておきました。
kernel/main.cpp:

extern "C" void KernelMain() {
    while(1) __asm__("hlt");
}

今後はここを拡張させていくのでしょう。いやー、楽しみ。

んで、ブートローダ側にもカーネルを起動するためのプログラムを書かなければならないので、そこも書いていきます。教科書ではUefiMainに直接書いていましたが、ゴチャゴチャしてしまうので役割ごとに3つの関数に分けておきました。

// カーネル起動前にブートサービスを停止させる
void disable_boot_service(EFI_HANDLE image_handle) {
  CHAR8 memmap_buf[4096 * 4];
  struct MemoryMap memmap = {sizeof(memmap_buf), memmap_buf, 0, 0, 0, 0};
  
  // ブートサービス停止
  EFI_STATUS status;
  status = gBS->ExitBootServices(image_handle, memmap.map_key);

  // 失敗したら再度メモリマップを取得して再実行
  if (EFI_ERROR(status)) {
    status = GetMemoryMap(&memmap);
    // それでも失敗したらエラー
    if (EFI_ERROR(status)) {
      Print(L"failed to get memory map: %r\n", status);
      while(1);
    }
    status = gBS->ExitBootServices(image_handle, memmap.map_key);
    if (EFI_ERROR(status)) {
      Print(L"Could not exit boot service: %r\n", status);
      while(1);
    }
  }
}

// カーネルの読み込み
EFI_PHYSICAL_ADDRESS load_kernel(EFI_FILE_PROTOCOL* root_dir) {
  EFI_FILE_PROTOCOL* kernel_file;
  root_dir->Open(root_dir, &kernel_file, L"\\kernel.elf", EFI_FILE_MODE_READ, 0);

  UINTN file_info_size = sizeof(EFI_FILE_INFO) + sizeof(CHAR16) * 12;
  UINT8 file_info_buffer[file_info_size];
  kernel_file->GetInfo(kernel_file, &gEfiFileInfoGuid, &file_info_size, file_info_buffer);

  EFI_FILE_INFO* file_info = (EFI_FILE_INFO*)file_info_buffer;
  UINTN kernel_file_size = file_info->FileSize;

  EFI_PHYSICAL_ADDRESS kernel_base_addr = 0x100000;         // カーネルのベースアドレスは0x100000(ld.lldのオプションで指定)
  gBS->AllocatePages(
    AllocateAddress, EfiLoaderData,
    (kernel_file_size + 0xfff) / 0x1000,                    // allocateするページ数
    &kernel_base_addr
  );
  kernel_file->Read(kernel_file, &kernel_file_size, (VOID*)kernel_base_addr);   // ファイル全体の読み込み
  Print(L"Kernel: 0x%0lx (%lu bytes)\n", kernel_base_addr, kernel_file_size);

  return kernel_base_addr;
}

// カーネルを起動
void boot_kernel(EFI_PHYSICAL_ADDRESS kernel_base_address) {
  UINT64 entry_addr = *(UINT64*)(kernel_base_address + 24);   // EFIの仕様よりエントリポイントはオフセット+24バイトの位置から

  typedef void EntryPointType(void);
  EntryPointType* entry_point = (EntryPointType*)entry_addr;
  entry_point();
}

EFI_STATUS UefiMain(EFI_HANDLE        image_handle,
                   EFI_SYSTEM_TABLE  *system_table) {
  Print(L"Hello, miCanopus!\n");

  // ルートディレクトリを開く
  EFI_FILE_PROTOCOL* root_dir;
  OpenRootDir(image_handle, &root_dir);

  // カーネルの読み込み
  EFI_PHYSICAL_ADDRESS kernel_base_addr = load_kernel(root_dir);

  // ブートサービス停止
  disable_boot_service(image_handle);

  // カーネルを起動
  boot_kernel(kernel_base_addr);

  Print(L"All done\n");

  while (1);
  return EFI_SUCCESS;
}

boot_kernel()でカーネルを起動後、動作はここで静止するはずなので、「All done」と表示されなければカーネルの起動に成功。
スクリーンショット 2022-09-29 0.06.04
「All done」と表示されていないのでうまくいった(はず)。

本当はQEMUのxコマンドを使って今実行している命令を見てみたいのですが、x /2iを実行すると前述の通り「Asm output not supported on this arch」と出てしまうので諦めました。まあ、せっかくなので命令のバイト列だけでも。

(qemu) x /4xb 0x3fb73016
000000003fb73016: 0x48 0x83 0x7c 0x24

ちなみにここでLoader.infが以下の内容が追記されているので写経勢の方々は要注意。自分で本から書き写している場合、Loader.infにこれを追記しないとビルドできません。

[Guids]
  gEfiFileInfoGuid
  gEfiAcpiTableGuid

6. おわりに

今回はメモリマップの書き出しとカーネルの作成などをしました。
day03bからはいよいよピクセルの描画です。楽しみ。