前々から興味があった「ゼロからのOS自作入門」をゆっくり読んでみようと思います。
挫折する可能性が非常に高いですが、作業メモをQiitaに書いていこうと思います。
作業は基本的にこちらのリポジトリで行います。
EDK IIについて
EDK2は、UEFI BIOS自体の開発とUEFIアプリケーション(ブートローダー)を開発するための開発キットです。
ディレクトリ構成
ざっくりなディレクトリ構成はこんな感じ
- 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ファイルが必要です。
- パッケージ宣言ファイル (
.dec
) - パッケージ記述ファイル (
.dsc
) - モジュール情報ファイル (
.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 に詳しく書かれています。
このファイルの設定値はたくさんあるので基本的な情報をさらっておきます。
-
[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 に詳しく書かれています。
一つのパッケージは複数のモジュールを持つことができます。
※ パッケージ記述ファイルはパッケージごとに一つ。モジュール情報ファイルはモジュールごとに一つ作ります。
-
Defines セクション
- BASE_NAME: コンポーネントの名前
- ENTRY_POINT: UEFIアプリケーションのエントリーポイント
-
Sources セクション
UEFIアプリケーションを構成するソースファイルを1行1ファイルで列挙します。 -
Package セクション
モジュールが依存するパッケージを指定します。 -
LibraryClasses セクション
モジュールが使用するライブラリクラスを指定します。
ここで指定するライブラリ名はパッケージ記述ファイルのLibraryClassesセクションで定義した名前です。 -
Guids セクション
モジュールで使用するGUIDを列挙します -
Protocols セクション
モジュールが使用するUEFIプロトコルを指定します。- gEfiLoadedImageProtocolGuid
ロードされたイメージに関する情報を提供するプロトコル - gEfiLoadFileProtocolGuid
ファイルのロードに関するプロトコル - gEfiSimpleFileSystemProtocolGuid
シンプルなファイルシステムへのアクセスを提供するプロトコル
- gEfiLoadedImageProtocolGuid
EDK II を使ってブートローダーを実装
完成したソースコードはこちら
ディレクトリ構成
- MikanLoaderPkg/
- Loader.inf
- Main.c
- MikanLoaderPkg.dec
- MikanLoaderPkg.dsc
- edk2/
必要なパッケージをインストール
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における開発に必要な以下のファイルを作成します。
- パッケージ宣言ファイル (
.dec
) - パッケージ記述ファイル (
.dsc
) - モジュール情報ファイル (
.inf
)
cd mikanos
mkdir -p MikanLoaderPkg
touch Loader.inf Main.c MikanLoaderPkg.dec MikanLoaderPkg.dsc
パッケージ宣言ファイル (.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
)
[Defines]
DEC_SPECIFICATION = 0x00010005
PACKAGE_NAME = MikanLoaderPkg
PACKAGE_GUID = 452eae8e-71e9-11e8-a243-df3f1ffdebe1
PACKAGE_VERSION = 0.1
モジュール情報ファイル (.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"
を表示しようと思います。
#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をビルド
# 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で起動するイメージを作成
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でイメージを起動
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)
簡単な使い方
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)の配列で、EFI_GET_MEMORY_MAPで取得することができます。
メモリディスクリプタの構造体定義は以下の通り
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つの機能があります。
-
gBS (
EFI_BOOT_SERVICES
)
OSを起動するために必要な機能を提供するブートサービス
https://github.com/tianocore/edk2/blob/edk2-stable202208/MdePkg/Include/Uefi/UefiSpec.h#L1863 -
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) という関数を使います。
実装
メモリマップを読み込んで、 /memmap
ファイルにCSV形式で保存する処理を Main.c
に追加します。
C言語を殆ど書いたことがなかったので、引数をポインタにして出力用変数を渡す方式が新鮮でした。(C言語の戻り値って単一値出ないとだめなんですね)
C言語の文法をさらっておかないときついかな、、、
構造体のメンバや関数のシグネチャが全くわからないので、edk2のソースコードからヘッダファイルを探して定義を確認しながら進めないと何をやっているのかさっぱりです。
#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
# OS起動
make run-nographic
メモリマップの結果
# 実行後のイメージを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 |
... | ... | ... | ... | ... | ... |