【ゼロからのOS自作入門をやってみる】第2章 EDKII入門とメモリマップ

前々から興味があった「ゼロからのOS自作入門」をゆっくり読んでみようと思います。
挫折する可能性が非常に高いですが、作業メモをQiitaに書いていこうと思います。


https://www.amazon.co.jp/dp/B08Z3MNR9J/


作業は基本的にこちらのリポジトリで行います。

https://github.com/ng3rdstmadgke/mikanos/tree/day02a


- 過去記事
  - [【ゼロからのOS自作入門をやってみる】第1章 PCの仕組みとハローワールド](https://qiita.com/ktamido/items/56b3427827894021ed73)
- 次の記事
  - [【ゼロからのOS自作入門をやってみる】第3章 画面表示の練習とブートローダ](https://qiita.com/ktamido/items/2e25d505d475933dcd91)

# EDK IIについて

EDK2は、UEFI BIOS自体の開発とUEFIアプリケーション(ブートローダー)を開発するための開発キットです。

https://github.com/tianocore/edk2


## ディレクトリ構成

ざっくりなディレクトリ構成はこんな感じ

```ディレクトリ
- edk2/
  - edksetup.sh     EDK2のビルドコマンドを動かすための準備用スクリプト
  - Conf/
    - target.txt    ビルド設定 (edksetup.shを実行するとひな型が生成される)
    - tool_def.txt  コンパイラなどツールチェーンの設定 (edksetup.shを実行するとひな型が生成される)
  - MdePkg/         EDKの基本ライブラリ
  - AppPkg/         UEFIアプリケーションをいくつか含んだライブラリ。オリジナルEFIアプリケーションを作るときの参考になる。
  - OvmfPkg/        UEFI BIOSのオープンソース実装であるOVMFが収められています。
  - ...Pkg/         その他のライブラリ
```

## ブートローダーの作成に必要なファイル

### 必要なファイル
EDK2の開発ではソースコード以外に以下の3ファイルが必要です。

1. パッケージ宣言ファイル (`.dec`)
1. パッケージ記述ファイル (`.dsc`)
1. モジュール情報ファイル (`.inf`)

#### 1. パッケージ宣言ファイル (`.dec`)

サンプル: https://github.com/uchan-nos/mikanos/blob/osbook_day02a/MikanLoaderPkg/MikanLoaderPkg.dec

このファイルは、パッケージの名前を決めるのが主な役割です。
パッケージを公開したり、本家EDKIIに取り込んでもらう用途でなければ、`PACKAGE_NAME` さえ正しい値にすれば、ほかはデフォルトで問題ありません。


#### 2. パッケージ記述ファイル (`.dsc`)

サンプル: https://github.com/uchan-nos/mikanos/blob/osbook_day02a/MikanLoaderPkg/MikanLoaderPkg.dsc

※ このファイルの仕様は[EDK II Platform Description (DSC) File Specification](https://tianocore-docs.github.io/edk2-DscSpecification/draft/)  に詳しく書かれています。

このファイルの設定値はたくさんあるので基本的な情報をさらっておきます。

- **[Defines]** セクション
    - `SUPPORTED_ARCHITECTURES`
    UEFIアプリケーションが対象とするアーキテクチャを指定。 (`X64` , `IA32` , `ARM` , etc ...)
    - `OUTPUT_DIRECTORY`
        - ビルドしたファイル(`.efi`)が出力されるディレクトリを指定。`$(ARCH)` にはアーキテクチャが展開される。
- **[LibraryClasses]** セクション
UEFIアプリケーションの作成に必要なライブラリ名とファイルパスを設定します。  
`ライブラリ名|ライブラリのモジュール情報ファイルへのパス` のように記述します。
- **[Components]** セクション
パッケージを構成するコンポーネントを指定します。パッケージをビルドするとき、指定したコンポーネントがビルド対象となります。
例えばEDK IIのAppPkgにはHelloやMain、Luaといった複数のコンポーネントが含まれていますが、Componentsセクションで指定しないコンポーネントはビルドされません


#### 3. モジュール情報ファイル (`.inf`)

サンプル: https://github.com/uchan-nos/mikanos/blob/osbook_day02a/MikanLoaderPkg/Loader.inf

※ このファイルの仕様は[EDK II Module Information (INF) File Specification](https://tianocore-docs.github.io/edk2-InfSpecification/draft/)  に詳しく書かれています。

一つのパッケージは複数のモジュールを持つことができます。
※ パッケージ記述ファイルはパッケージごとに一つ。モジュール情報ファイルはモジュールごとに一つ作ります。

- **Defines** セクション
    - BASE_NAME: コンポーネントの名前
    - ENTRY_POINT: UEFIアプリケーションのエントリーポイント
- **Sources** セクション
UEFIアプリケーションを構成するソースファイルを1行1ファイルで列挙します。
- **Package** セクション
モジュールが依存するパッケージを指定します。
- **LibraryClasses** セクション
モジュールが使用するライブラリクラスを指定します。
ここで指定するライブラリ名はパッケージ記述ファイルのLibraryClassesセクションで定義した名前です。
- **Guids** セクション
モジュールで使用するGUIDを列挙します
- **Protocols** セクション
モジュールが使用するUEFIプロトコルを指定します。
    - gEfiLoadedImageProtocolGuid  
    ロードされたイメージに関する情報を提供するプロトコル
    - gEfiLoadFileProtocolGuid  
    ファイルのロードに関するプロトコル
    - gEfiSimpleFileSystemProtocolGuid  
    シンプルなファイルシステムへのアクセスを提供するプロトコル

# EDK II を使ってブートローダーを実装

完成したソースコードはこちら

https://github.com/ng3rdstmadgke/mikanos/tree/day02a/MikanLoaderPkg


ディレクトリ構成

```
- MikanLoaderPkg/
  - Loader.inf
  - Main.c
  - MikanLoaderPkg.dec
  - MikanLoaderPkg.dsc
- edk2/
```

## 必要なパッケージをインストール

```bash
sudo apt update

# とりあえず必要なパッケージ
sudo apt-get install -y git vim

# edk2のビルドで必要
sudo apt-get install -y make clang lld llvm nasm uuid-dev acpica-tools

# イメージの起動で必要
# - OVMF (Open Virtual Machine Firmware)
#   QEMU などの仮想マシンで UEFI をサポートするためのファームウェアで、EDK IIプロジェクトの一部として提供されています。
sudo apt-get install -y qemu-system-x86 ovmf
```

## ブートローダーの実装

EDK2における開発に必要な以下のファイルを作成します。

1. パッケージ宣言ファイル (`.dec`)
1. パッケージ記述ファイル (`.dsc`)
1. モジュール情報ファイル (`.inf`)

```bash
cd mikanos
mkdir -p MikanLoaderPkg
touch Loader.inf  Main.c  MikanLoaderPkg.dec  MikanLoaderPkg.dsc
```

### パッケージ宣言ファイル (`.dec`)
```MikanLoaderPkg/MikanLoaderPkg.dec
#@range_begin(defines)
[Defines]
  PLATFORM_NAME                  = MikanLoaderPkg
  PLATFORM_GUID                  = d3f11f4e-71e9-11e8-a7e1-33fd4f7d5a3e
  PLATFORM_VERSION               = 0.1
  DSC_SPECIFICATION              = 0x00010005
  OUTPUT_DIRECTORY               = Build/MikanLoader$(ARCH)
  SUPPORTED_ARCHITECTURES        = X64
  BUILD_TARGETS                  = DEBUG|RELEASE|NOOPT
#@range_end(defines)

#@range_begin(library_classes)
[LibraryClasses]
  UefiApplicationEntryPoint|MdePkg/Library/UefiApplicationEntryPoint/UefiApplicationEntryPoint.inf
  UefiLib|MdePkg/Library/UefiLib/UefiLib.inf
#@range_end(library_classes)

  BaseLib|MdePkg/Library/BaseLib/BaseLib.inf
  BaseMemoryLib|MdePkg/Library/BaseMemoryLib/BaseMemoryLib.inf
  DebugLib|MdePkg/Library/BaseDebugLibNull/BaseDebugLibNull.inf
  DevicePathLib|MdePkg/Library/UefiDevicePathLib/UefiDevicePathLib.inf
  MemoryAllocationLib|MdePkg/Library/UefiMemoryAllocationLib/UefiMemoryAllocationLib.inf
  PcdLib|MdePkg/Library/BasePcdLibNull/BasePcdLibNull.inf
  PrintLib|MdePkg/Library/BasePrintLib/BasePrintLib.inf
  UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf
  UefiRuntimeServicesTableLib|MdePkg/Library/UefiRuntimeServicesTableLib/UefiRuntimeServicesTableLib.inf

  # もとのコードから追加 (これがないとビルドに失敗する)
  RegisterFilterLib|MdePkg/Library/RegisterFilterLibNull/RegisterFilterLibNull.inf

#@range_begin(components)
[Components]
  MikanLoaderPkg/Loader.inf
#@range_end(components)
```

### パッケージ記述ファイル (`.dsc`)

```MikanLoaderPkg/MikanLoaderPkg.dec
[Defines]
  DEC_SPECIFICATION              = 0x00010005
  PACKAGE_NAME                   = MikanLoaderPkg
  PACKAGE_GUID                   = 452eae8e-71e9-11e8-a243-df3f1ffdebe1
  PACKAGE_VERSION                = 0.1
```

### モジュール情報ファイル (`.inf`)

```MikanLoaderPkg/Loader.inf
[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = Loader
  FILE_GUID                      = c9d0d202-71e9-11e8-9e52-cfbfd0063fbf
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 0.1
  ENTRY_POINT                    = UefiMain

#  VALID_ARCHITECTURES           = X64

[Sources]
  Main.c

[Packages]
  MdePkg/MdePkg.dec

[LibraryClasses]
  UefiLib
  UefiApplicationEntryPoint

[Guids]

[Protocols]
```

### ブートローダーのソースコード

ひとまず、第一章と同じく `"Hello, Mikan World!\n"` を表示しようと思います。


   
```MikanLoaderPkg/Main.c
#include <Uefi.h>
#include <Library/UefiLib.h>

# EFI_STATUS: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiBaseType.h#L110
#   - UEFI関数の戻り値として、処理の成否を表す型
# EFI_HANDLE: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiBaseType.h#L33
#   - EFI_HANDLEはUEFI内でリソースを一意に識別するためのポインタ型ハンドル
#   - 詳細な中身は抽象化されており、通常は直接アクセスや操作をせず、UEFI API経由で利用します
# EFI_SYSTEM_TABLE: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L1977
#   - UEFIファームウェアとUEFIアプリケーション(ブートローダー)間で情報やサービスをやり取りするための中心的な構造体
# EFIAPI: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/X64/ProcessorBind.h#L294
#   - EFIAPIは関数呼び出し規約を示すマクロで、関数がUEFIによって正しく呼び出されるようにするためのものです。
#   - `EFIAPI __cdecl` はWindows/MSVC系コンパイラで使われる呼び出し規約、`EFIAPI `のように未指定の場合は
EFI_STATUS EFIAPI UefiMain(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE *system_table) {
  Print(L"Hello, Mikan World!\n");
  while (1);
  return EFI_SUCCESS;
}
```





## edk2でMikanLoaderPkgをビルド

```bash
# EDK IIをクローン
# ※ 最新のソースだとCLANG38でビルドできないので、 `edk2-stable202302` タグを使います。
git clone --branch edk2-stable202302 https://github.com/tianocore/edk2.git
cd edk2

# サブモジュールを更新
git submodule update --init --recursive

# Conf/target.txtを生成
source edksetup.sh

# Conf/target.txtを以下のように編集
cat Conf/target.txt | grep -v -e "^#" | grep "^[A-Z]"
# ACTIVE_PLATFORM       = MikanLoaderPkg/MikanLoaderPkg.dsc
# TARGET                = DEBUG
# TARGET_ARCH           = X64
# TOOL_CHAIN_CONF       = Conf/tools_def.txt
# TOOL_CHAIN_TAG        = CLANG38
# BUILD_RULE_CONF = Conf/build_rule.txt

# MikanLoaderPkgへのシンボリックリンクを作成
ln -s ../MikanLoaderPkg ./

# ベースツールをビルド
make -C BaseTools/Source/C

# ビルド
build

# Loader.efiが生成されます。
ls Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi

cd ..
```

## qemuで起動するイメージを作成

```bash
IMAGE_PATH=build/disk.img
EFI_PATH=edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi

# OSイメージファイルを作成
qemu-img create -f raw $IMAGE_PATH 200M
# Formatting 'build/disk.img', fmt=raw size=209715200

# イメージファイルをFAT32でフォーマット
# mkfs.fat: FATファイルシステムをディスクイメージやパーティションに作成するコマンド
#   -n <LABEL>: ボリュームのラベルを指定
#   -s <セクタ数>: クラスタサイズを何セクタにするか指定します
#   -f <FATテーブルのコピー数>: FATテーブルはファイルがディスク上のどのクラスタに格納されているかを管理するテーブル。このオプションは冗長化のためのFATテーブルのコピー数を指定します。
#   -R <クラスタ数> : ルートディレクトリエントリ領域の予約クラスタ数を指定します。(FAT32では無視される)
#   -F <12|16|32> : フォーマットを指定します。(FAT12, FAT16, FAT32)
mkfs.fat -n "MIKAN OS" -s 2 -f 2 -R 32 -F 32 $IMAGE_PATH
# mkfs.fat 4.2 (2021-01-31)
# パーティションの確認
sudo parted $IMAGE_PATH print
# Model:  (file)
# Disk /home/ktamido/Projects/mikanos/mikanos/build/disk.img: 210MB
# Sector size (logical/physical): 512B/512B
# Partition Table: loop
# Disk Flags: 
# 
# Number  Start  End    Size   File system  Flags
#  1      0.00B  210MB  210MB  fat32

# OSイメージをマウント
mkdir -p build/mnt
sudo mount -o loop $IMAGE_PATH build/mnt
sudo mkdir -p build/mnt/EFI/BOOT

# UEFIアプリケーション(ブートローダー)をOSイメージにコピー
sudo cp $EFI_PATH build/mnt/EFI/BOOT/BOOTX64.EFI
sudo umount build/mnt
```

## qemuでイメージを起動


```bash
OVMF_CODE_PATH=/usr/share/OVMF/OVMF_CODE_4M.fd
OVMF_VARS_PATH=build/OVMF_VARS_4M.fd
cp /usr/share/OVMF/OVMF_VARS_4M.fd $OVMF_VARS_PATH
sudo qemu-system-x86_64 -m 1G \
    -drive if=pflash,format=raw,file=$OVMF_CODE_PATH,readonly=on \
    -drive if=pflash,format=raw,file=$OVMF_VARS_PATH \
    -drive if=ide,index=0,media=disk,format=raw,file=$IMAGE_PATH \
    -nographic
```

### OVMFとは、、、

> qemuでOSを起動するときに出てくるOVMFがわからなかったので少し調べてみました。


OVMF（Open Virtual Machine Firmware）は、仮想マシン上でUEFI（Unified Extensible Firmware Interface）環境を提供するためのオープンソースのファームウェアで、EDK IIプロジェクトの一部として提供されています。QEMUやKVMなどでUEFIブートのテストや開発を行うために利用されます。

OVMFは以下の2ファイルで構成されています。

- **OVMF_CODE.fd (OVMF_CODE_4M.fd)** ：UEFIファームウェア本体（Read Only）
- **OVMF_VARS.fd (OVMF_VARS_4M.fd)** : UEFI変数（NVRAM）を保持する領域（Read, Write)


#### 簡単な使い方

```bash
sudo apt install -y qemu-system-x86 ovmf

# OVMFファイルが存在することを確認
ll  /usr/share/OVMF/
# 合計 8736
# drwxr-xr-x   2 root root    4096  4月 22 16:30 ./
# drwxr-xr-x 254 root root   12288  5月 17 17:21 ../
# -rw-r--r--   1 root root 3653632 10月 26  2024 OVMF_CODE_4M.fd
# lrwxrwxrwx   1 root root      23 10月 26  2024 OVMF_CODE_4M.ms.fd -> OVMF_CODE_4M.secboot.fd
# -rw-r--r--   1 root root 3653632 10月 26  2024 OVMF_CODE_4M.secboot.fd
# lrwxrwxrwx   1 root root      23 10月 26  2024 OVMF_CODE_4M.snakeoil.fd -> OVMF_CODE_4M.secboot.fd
# -rw-r--r--   1 root root  540672 10月 26  2024 OVMF_VARS_4M.fd
# -rw-r--r--   1 root root  540672 10月 26  2024 OVMF_VARS_4M.ms.fd
# -rw-r--r--   1 root root  540672 10月 26  2024 OVMF_VARS_4M.snakeoil.fd

# UEFI変数格納用の領域はOSを起動するたびに書き込みが走って変更されるのでコピーしておく
sudo cp /usr/share/OVMF/OVMF_VARS_4M.fd ./OVMF_VARS_4M.fd

# Tiny Core Linuxをダウンロード
# http://www.tinycorelinux.net/downloads.html
wget http://www.tinycorelinux.net/16.x/x86/release/TinyCore-16.0.iso

# isoイメージの起動
qemu-system-x86_64 \
  -enable-kvm \
  -m 2048 \
  -cdrom TinyCore-16.0.iso \
  -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
  -drive if=pflash,format=raw,file=./OVMF_VARS_4M.fd \
  -boot d \
  --nographic
```



# メモリマップ

メモリマップにはメインメモリのどの部分がどんな用途で使われているかが載っており、イメージ的には以下の様な感じです。

| PhysicalStart | Type | NumberOfPages |
| --- | --- | --- |
| 0x00000000 | EfiBootServicesCode | 0x1 |
| 0x00001000 | EfiConventionalMemory | 0x9F |
| 0x00100000 | EfiConventionalMemory | 0x700 |
| 0x00800000 | EfiACPIMemoryNVS | 0x8 |
| ... | ... | ... |

※ メモリマップには歯抜けがあり、PhysicalStartにNumberOfPages * 4KiB を足しても次の行のPhysicalStartにならない場合もあります。

## メモリマップのデータ構造

メモリマップはメモリディスクリプタ([EFI_MEMORY_DESCRIPTOR](https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L128))の配列で、[EFI_GET_MEMORY_MAP](https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L239)で取得することができます。


メモリディスクリプタの構造体定義は以下の通り

```c
typedef struct {
  // メモリ領域の種別
  // EFI_MEMORY_TYPE型は、AllocatePages()関数の説明で定義されている。
  // 以下のような値が格納される
  //   EfiLoaderCode (Type値=1): UEFIアプリケーションの実行コード
  //   EfiLoaderData (Type値=2): UEFIアプリケーションが使うデータ領域
  //   EfiBootServiceCode (Type値=3): ブートサービスドライバの実行コード
  //   EfiBootServiceData (Type値=4): ブートサービスドライバが使うデータ領域
  //   EfiConventionalMemory (Type値=7): 空き領域
  UINT32                  Type;

  // メモリ領域の最初のバイトの物理アドレス。
  // PhysicalStartは4KiB境界にアライメントされなければならず、0xfffffffffff000以上であってはならない。
  // EFI_PHYSICAL_ADDRESS型は、AllocatePages()関数の説明で定義されています。
  EFI_PHYSICAL_ADDRESS    PhysicalStart;

  // メモリ領域の最初のバイトの仮想アドレス。
  // VirtualStartは4KiB境界にアラインされていなければならず、0xfffffffffff000以上であってはならない。
  EFI_VIRTUAL_ADDRESS     VirtualStart;


  // NumberOfPagesメモリ領域内の4KiBページの数。
  // NumberOfPagesは0であってはならず、また、開始アドレスが0xfffffffffff000を超えるメモリページを表すような値であってはならない。
  UINT64                  NumberOfPages;

  // メモリ領域の属性で、そのメモリ領域の能力のビットマスクを記述するもの。
  // そのメモリ領域の現在の設定とは限らない
  UINT64                  Attribute;
} EFI_MEMORY_DESCRIPTOR;
```


# メモリマップの取得プログラムを作成

OS作りに先立って、UEFIの機能を使ってメモリマップを取得するプログラムを作ります。
最終的には、取得したメモリマップはOSに渡すことになりますが、ひとまずファイルに保存することを目的とします。


UEFIには大きく分けて2つの機能があります。

1. **gBS (`EFI_BOOT_SERVICES`)**  
OSを起動するために必要な機能を提供するブートサービス
https://github.com/tianocore/edk2/blob/edk2-stable202208/MdePkg/Include/Uefi/UefiSpec.h#L1863
1. **gRS(`EFI_RUNTIME_SERVICES`)**  
OS起動前後どちらでも使える機能を提供するランタイムサービス
https://github.com/tianocore/edk2/blob/edk2-stable202208/MdePkg/Include/Uefi/UefiSpec.h#L1812

メモリマップの取得には **gBS->GetMemoryMap(...)** (定義: [EFI_GET_MEMORY_MAP](https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L239)) という関数を使います。




## 実装

メモリマップを読み込んで、 `/memmap` ファイルにCSV形式で保存する処理を `Main.c` に追加します。

> C言語を殆ど書いたことがなかったので、引数をポインタにして出力用変数を渡す方式が新鮮でした。(C言語の戻り値って単一値出ないとだめなんですね)
> C言語の文法をさらっておかないときついかな、、、

> 構造体のメンバや関数のシグネチャが全くわからないので、[edk2](https://github.com/tianocore/edk2)のソースコードからヘッダファイルを探して定義を確認しながら進めないと何をやっているのかさっぱりです。

```MikanLoaderPkg/Main.c
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/PrintLib.h>
#include <Protocol/LoadedImage.h>
#include <Protocol/DiskIo2.h>
#include <Protocol/BlockIo.h>

// UINT系: https://github.com/tianocore/edk2/blob/edk2-stable202302/EmbeddedPkg/Include/libfdt_env.h#L19
// UINT8 uint8_t
// UINT16 uint16_t
// UINT32 uint32_t
// UINT64 uint64_t
// UINTN uintptr_t CPUアーキテクチャ依存の符号なし整数型。32bitでは4byte , 64bitでは(8byte)

// メモリマップの構造体
struct MemoryMap {
  UINTN buffer_size;
  VOID* buffer;
  UINTN map_size;
  UINTN map_key;
  UINTN descriptor_size;
  UINT32 descriptor_version;
};

/**
 * メモリマップを取得する関数
 */
EFI_STATUS GetMemoryMap(struct MemoryMap *map) {
  if (map->buffer == NULL) {
    return EFI_BUFFER_TOO_SMALL;
  }

  map->map_size = map->buffer_size;
  // gBS
  //   OSを起動するために必要な機能を提供するブートサービスを表すグローバル変数
  //
  // EFI_GET_MEMORY_MAP: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L239
  //   呼び出し時点のメモリマップを取得し、MemoryMap（第二引数)で指定されたメモリ領域に書き込みます。
  //   正常にメモリマップが取得できると EFI_SUCCESS を返却。メモリ領域が小さくてメモリマップを書き込めない場合は EFI_BUFFER_TOO_SMALL を返却。
  //
  // EFI_MEMORY_DESCRIPTOR: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L128
  //   メモリディスクリプタはメモリマップの個々のエントリ
  //   UINT32                Type           メモリ領域の種別
  //   EFI_PHYSICAL_ADDRESS  PhysicalStart  メモリ領域先頭の物理メモリアドレス
  //   EFI_VIRTUAL_ADDRESS   VirtualStart   メモリ領域先頭の仮想メモリアドレス
  //   UINT64                NumberOfPages  メモリ領域の大きさ (4KiBページ単位)
  //   UINT64                Attribute      メモリ領域が使える用途を示すビット集合
  return gBS->GetMemoryMap(
    &map->map_size,                       // IN OUT UINTN                *MemoryMapSize メモリマップ書き込み用のメモリ領域のサイズ(バイト)
    (EFI_MEMORY_DESCRIPTOR*)map->buffer,  // OUT  EFI_MEMORY_DESCRIPTOR  *MemoryMap メモリマップ書込み用メモリ領域の先頭ポインタ(メモリディスクリプタの配列)
    &map->map_key,                        // OUT  UINTN                  *MapKey  メモリマップのハッシュ値的なもの (これに変化がなければメモリマップに変化はない)
    &map->descriptor_size,                // OUT  UINTN                  *DescriptorSize メモリディスクリプタ のサイズ
    &map->descriptor_version              // OUT  UINT32                 *DescriptorVersion メモリディスクリプタのバージョン番号
  );
}


const CHAR16* GetMemoryTypeUnicode(EFI_MEMORY_TYPE type) {
  switch (type) {
    case EfiReservedMemoryType: return L"EfiReservedMemoryType";
    case EfiLoaderCode: return L"EfiLoaderCode";
    case EfiLoaderData: return L"EfiLoaderData";
    case EfiBootServicesCode: return L"EfiBootServicesCode";
    case EfiBootServicesData: return L"EfiBootServicesData";
    case EfiRuntimeServicesCode: return L"EfiRuntimeServicesCode";
    case EfiRuntimeServicesData: return L"EfiRuntimeServicesData";
    case EfiConventionalMemory: return L"EfiConventionalMemory";
    case EfiUnusableMemory: return L"EfiUnusableMemory";
    case EfiACPIReclaimMemory: return L"EfiACPIReclaimMemory";
    case EfiACPIMemoryNVS: return L"EfiACPIMemoryNVS";
    case EfiMemoryMappedIO: return L"EfiMemoryMappedIO";
    case EfiMemoryMappedIOPortSpace: return L"EfiMemoryMappedIOPortSpace";
    case EfiPalCode: return L"EfiPalCode";
    case EfiPersistentMemory: return L"EfiPersistentMemory";
    case EfiMaxMemoryType: return L"EfiMaxMemoryType";
    default: return L"InvalidMemoryType";
  }
}


/**
 * メモリマップをCSV形式でファイルに保存する
 */
EFI_STATUS SaveMemoryMap(struct MemoryMap* map, EFI_FILE_PROTOCOL* file) {
  CHAR8 buf[256];
  UINTN len;

  CHAR8* header = "Index, Type, Type(name), PhysicalStart, NumberOfPages, Attribute\n";
  // https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Library/BaseLib.h#L1673
  len = AsciiStrLen(header);
  // EFI_FILE_WRITE: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Protocol/SimpleFileSystem.h#L220
  file->Write(
    file,  // IN EFI_FILE_PROTOCOL  *This        https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Protocol/SimpleFileSystem.h#L528
    &len,  // IN OUT UINTN          *BufferSize
    header // IN VOID               *Buffer
  );
  
  // %08lx : unsigned longの16進数をゼロ埋め8桁で表示 (例: 0000abcd)
  Print(L"map->buffer = %08lx, map->map_size = %08lx\n", map->buffer, map->map_size);

  // EFI_PHYSICAL_ADDRESS: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiBaseType.h#L50
  EFI_PHYSICAL_ADDRESS iter;
  int i;
  // メモリマップからメモリディスクリプタ(構造体)をいてレートして、ファイルに書き込む
  for (iter = (EFI_PHYSICAL_ADDRESS)map->buffer, i = 0;
       iter < (EFI_PHYSICAL_ADDRESS)map->buffer + map->map_size;
       iter += map->descriptor_size, i++
  ) {
    // 整数型のiterをEFI_MEMORY_DESCRIPTOR* (ポインタ型) にキャスト(型変換)している
    EFI_MEMORY_DESCRIPTOR* desc = (EFI_MEMORY_DESCRIPTOR*)iter;
    // AsciiSPrint: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Library/PrintLib.h#L677
    //   char配列に整形した文字列を書き込む (sprintf()とほぼ同じ)
    len = AsciiSPrint(
      buf,
      sizeof(buf),
      // %u : unsigned int の10進数(符号なし整数)を表示
      // %x : unsigned int の16進数(小文字)を表示
      // %-ls : wchar_t* のワイド文字列を左寄せで表示 (l は wchar_t* を意味しています)
      // %08lx : unsigned longの16進数をゼロ埋め8桁で表示 (例: 0000abcd)
      // %lx : unsigned longの16進数を表示
      "%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;
}

/**
 * 書き込み先のファイルを開く
 */
EFI_STATUS OpenRootDir(EFI_HANDLE image_handle, EFI_FILE_PROTOCOL** root) {
  EFI_LOADED_IMAGE_PROTOCOL* loaded_image;
  // https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Protocol/SimpleFileSystem.h#L73
  EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* fs;

  // EFI_OPEN_PROTOCOL: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L1330
  //   UEFI対応のファイルシステム上でファイルやディレクトリに対する入出力操作を行うためのインターフェース
  gBS->OpenProtocol(
    image_handle,                        // IN  EFI_HANDLE  Handle,               オープンされているプロトコルインタフェースのハンドル
    &gEfiLoadedImageProtocolGuid,        // IN  EFI_GUID    *Protocol             プロトコルのGUID(識別子)
    (VOID**)&loaded_image,               // OUT VOID        **Interface  OPTIONAL 対応するプロトコルインタフェースへのポインタが返されるアドレス
    image_handle,                        // IN  EFI_HANDLE  AgentHandle           Protocol と Interface で指定されたプロトコルとインターフェースを開いているエージェントのハンドル。
    NULL,                                // IN  EFI_HANDLE  ControllerHandle      エージェントがUEFIドライバモデルに従うドライバの場合はプロトコルインターフェイスを必要とするコントローラハンドル、そうでなければNULL
    EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL // IN  UINT32      Attributes            Handle と Protocol で指定されたプロトコルインタフェースのオープンモード。
  );

  gBS->OpenProtocol(
    loaded_image->DeviceHandle,
    &gEfiSimpleFileSystemProtocolGuid,
    (VOID**)&fs,
    image_handle,
    NULL,
    EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL
  );

  // EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_OPEN_VOLUME: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Protocol/SimpleFileSystem.h#L59
  //   EFI_SIMPLE_FILE_SYSTEM_PROTOCOL 構造体のメンバー関数
  //   ファイルシステムボリュームのルートディレクトリを開く
  fs->OpenVolume(
    fs,  // IN  EFI_SIMPLE_FILE_SYSTEM_PROTOCOL  *This,
    root // OUT EFI_FILE_PROTOCOL                **Root
  );

  return EFI_SUCCESS;
}


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

  /**
   * メモリマップを取得する
   */
  CHAR8 memmap_buf[4096 * 4]; // 4page分
  struct MemoryMap memmap = {sizeof(memmap_buf), memmap_buf, 0, 0, 0, 0};
  GetMemoryMap(&memmap);

  /**
   * メモリマップを保存するファイルを開く
   */
  // EFI_FILE_PROTOCOL: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Protocol/SimpleFileSystem.h#L528
  EFI_FILE_PROTOCOL* root_dir;
  OpenRootDir(image_handle, &root_dir);

  EFI_FILE_PROTOCOL* memmap_file;
  // https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Protocol/SimpleFileSystem.h#L113
  root_dir->Open(
    root_dir,     // IN EFI_FILE_PROTOCOL *This
    &memmap_file, // OUT EFI_FILE_PROTOCOL **NewHandle
    L"\\memmap",  // IN CHAR16 *FileName
    EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE,  // IN UINT64 OpenMode
    0             // IN UINT64 Attributes
  );

  /**
   * メモリマップを保存する
   */
  SaveMemoryMap(&memmap, memmap_file);
  memmap_file->Close(memmap_file);

  Print(L"All done\n");

  while(1);
  return EFI_SUCCESS;
}
```

## 実行

> Makefile を使って、ソースのビルド、イメージの作成、OS起動までを一括で実行できるようにしてみました。
> https://github.com/ng3rdstmadgke/mikanos/blob/day02a/Makefile#L60

```bash
# OS起動
make run-nographic
```

## メモリマップの結果

```bash
# 実行後のイメージをbuild/mntにマウント
mkdir -p build/mnt
sudo mount -o loop build/disk.img build/mnt

# メモリマップを書き込んだmemmapファイルを確認
cat build/mnt/memmap

# 実行後のイメージをアンマウント
sudo umount build/mnt
```

`memmap` の出力は以下
| Index | Type | Type(name) | PhysicalStart | NumberOfPages | Attribute |
| --- | --- | --- | --- | --- | --- |
| 0 | 3 | EfiBootServicesCode | 00000000 | 1 | F |
| 1 | 7 | EfiConventionalMemory | 00001000 | 9F | F |
| 2 | 7 | EfiConventionalMemory | 00100000 | 700 | F |
| 3 | A | EfiACPIMemoryNVS | 00800000 | 8 | F |
| 4 | 7 | EfiConventionalMemory | 00808000 | 3 | F |
| 5 | A | EfiACPIMemoryNVS | 0080B000 | 1 | F |
| 6 | 7 | EfiConventionalMemory | 0080C000 | 4 | F |
| 7 | A | EfiACPIMemoryNVS | 00810000 | F0 | F |
| 8 | 4 | EfiBootServicesData | 00900000 | E80 | F |
| 9 | 7 | EfiConventionalMemory | 01780000 | 3A3F5 | F |
| ... | ... | ... | ... | ... | ... |