UEFI完全入門シリーズ
Part1【環境構築】 | Part2【GOP】 | Part3【ファイル読み込み】| Part4【メモリマップ】| Part5【カーネルロード】
はじめに
ついに最終回!
これまで学んだことを総動員して、 カーネルをロードして実行 します。
- Part1: 環境構築、Hello World
- Part2: GOP(グラフィックス)
- Part3: ファイル読み込み
- Part4: メモリマップ
今回はこれらを組み合わせて、UEFIから自作カーネルに制御を渡します。
全体の流れ
1. GOPを取得(フレームバッファ情報)
2. カーネルファイルを開く
3. メモリを確保してカーネルをロード
4. ブート情報を準備
5. メモリマップを取得
6. ExitBootServices()
7. カーネルにジャンプ!
簡易カーネル
まずは超シンプルなカーネルを作ります。フレームバッファに直接描画するだけ。
kernel.c
typedef unsigned long long uint64_t;
typedef unsigned int uint32_t;
// ブートローダーから渡される情報
typedef struct {
uint64_t framebuffer_base;
uint32_t horizontal_resolution;
uint32_t vertical_resolution;
uint32_t pixels_per_scan_line;
} BootInfo;
void kernel_main(BootInfo *boot_info) {
// フレームバッファを取得
uint32_t *fb = (uint32_t *)boot_info->framebuffer_base;
uint32_t width = boot_info->horizontal_resolution;
uint32_t height = boot_info->vertical_resolution;
uint32_t pitch = boot_info->pixels_per_scan_line;
// 画面を青で塗りつぶす
for (uint32_t y = 0; y < height; y++) {
for (uint32_t x = 0; x < width; x++) {
fb[y * pitch + x] = 0x000080FF; // BGR形式の青
}
}
// 中央に白い四角を描画
uint32_t rect_w = 200;
uint32_t rect_h = 100;
uint32_t rect_x = (width - rect_w) / 2;
uint32_t rect_y = (height - rect_h) / 2;
for (uint32_t y = rect_y; y < rect_y + rect_h; y++) {
for (uint32_t x = rect_x; x < rect_x + rect_w; x++) {
fb[y * pitch + x] = 0x00FFFFFF; // 白
}
}
// 停止
while (1) {
__asm__ volatile("hlt");
}
}
kernel.ld(リンカスクリプト)
ENTRY(kernel_main)
SECTIONS
{
. = 0x100000;
.text : { *(.text) }
.rodata : { *(.rodata) }
.data : { *(.data) }
.bss : { *(.bss) }
}
ビルド
$ gcc -ffreestanding -nostdlib -mno-red-zone -c kernel.c -o kernel.o
$ ld -nostdlib -T kernel.ld -o kernel.elf kernel.o
$ objcopy -O binary kernel.elf kernel.bin
$ ls -la kernel.bin
-rwxr-xr-x 1 root root 352 Dec 10 14:55 kernel.bin
わずか352バイトの超軽量カーネル!
ブートローダーの実装
コード全体
#include <efi.h>
#include <efilib.h>
// GUIDs
EFI_GUID LoadedImageProtocolGuid = EFI_LOADED_IMAGE_PROTOCOL_GUID;
EFI_GUID SimpleFileSystemProtocolGuid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
EFI_GUID FileInfoGuid = EFI_FILE_INFO_ID;
EFI_GUID GraphicsOutputProtocolGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
// カーネルに渡す情報
typedef struct {
UINT64 framebuffer_base;
UINT32 horizontal_resolution;
UINT32 vertical_resolution;
UINT32 pixels_per_scan_line;
} BootInfo;
// カーネルのエントリポイント型
typedef void (*KernelEntry)(BootInfo *);
#define KERNEL_LOAD_ADDRESS 0x100000
EFI_STATUS EFIAPI
efi_main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
EFI_STATUS Status;
EFI_LOADED_IMAGE_PROTOCOL *LoadedImage;
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *FileSystem;
EFI_FILE_PROTOCOL *Root, *KernelFile;
EFI_GRAPHICS_OUTPUT_PROTOCOL *Gop;
InitializeLib(ImageHandle, SystemTable);
ST->ConOut->ClearScreen(ST->ConOut);
Print(L"=== UEFI Bootloader ===\n\n");
// 1. GOPを取得
Status = uefi_call_wrapper(
BS->LocateProtocol, 3,
&GraphicsOutputProtocolGuid, NULL, (VOID **)&Gop
);
Print(L"GOP: %dx%d, FB=0x%lx\n",
Gop->Mode->Info->HorizontalResolution,
Gop->Mode->Info->VerticalResolution,
Gop->Mode->FrameBufferBase);
// 2. ファイルシステムを取得
uefi_call_wrapper(BS->HandleProtocol, 3,
ImageHandle, &LoadedImageProtocolGuid, (VOID **)&LoadedImage);
uefi_call_wrapper(BS->HandleProtocol, 3,
LoadedImage->DeviceHandle, &SimpleFileSystemProtocolGuid, (VOID **)&FileSystem);
uefi_call_wrapper(FileSystem->OpenVolume, 2, FileSystem, &Root);
// 3. カーネルファイルを開く
Status = uefi_call_wrapper(
Root->Open, 5, Root, &KernelFile, L"kernel.bin", EFI_FILE_MODE_READ, 0
);
Print(L"kernel.bin opened\n");
// 4. ファイルサイズを取得
UINT8 InfoBuffer[256];
UINTN InfoSize = sizeof(InfoBuffer);
uefi_call_wrapper(KernelFile->GetInfo, 4, KernelFile, &FileInfoGuid, &InfoSize, InfoBuffer);
EFI_FILE_INFO *FileInfo = (EFI_FILE_INFO *)InfoBuffer;
UINTN KernelSize = FileInfo->FileSize;
Print(L"Kernel size: %d bytes\n", KernelSize);
// 5. メモリを確保
UINTN Pages = (KernelSize + 4095) / 4096;
EFI_PHYSICAL_ADDRESS KernelAddress = KERNEL_LOAD_ADDRESS;
uefi_call_wrapper(BS->AllocatePages, 4,
AllocateAddress, EfiLoaderData, Pages, &KernelAddress);
Print(L"Memory allocated at 0x%lx\n", KernelAddress);
// 6. カーネルを読み込み
UINTN ReadSize = KernelSize;
uefi_call_wrapper(KernelFile->Read, 3, KernelFile, &ReadSize, (VOID *)KernelAddress);
Print(L"Kernel loaded!\n\n");
uefi_call_wrapper(KernelFile->Close, 1, KernelFile);
uefi_call_wrapper(Root->Close, 1, Root);
// 7. ブート情報を準備
BootInfo *boot_info;
uefi_call_wrapper(BS->AllocatePool, 3, EfiLoaderData, sizeof(BootInfo), (VOID **)&boot_info);
boot_info->framebuffer_base = Gop->Mode->FrameBufferBase;
boot_info->horizontal_resolution = Gop->Mode->Info->HorizontalResolution;
boot_info->vertical_resolution = Gop->Mode->Info->VerticalResolution;
boot_info->pixels_per_scan_line = Gop->Mode->Info->PixelsPerScanLine;
Print(L"Press any key to boot kernel...\n");
UINTN Index;
uefi_call_wrapper(BS->WaitForEvent, 3, 1, &ST->ConIn->WaitForKey, &Index);
// 8. ExitBootServices
UINTN MemoryMapSize = 0;
EFI_MEMORY_DESCRIPTOR *MemoryMap = NULL;
UINTN MapKey, DescriptorSize;
UINT32 DescriptorVersion;
uefi_call_wrapper(BS->GetMemoryMap, 5,
&MemoryMapSize, NULL, &MapKey, &DescriptorSize, &DescriptorVersion);
MemoryMapSize += 2 * DescriptorSize;
uefi_call_wrapper(BS->AllocatePool, 3, EfiLoaderData, MemoryMapSize, (VOID **)&MemoryMap);
uefi_call_wrapper(BS->GetMemoryMap, 5,
&MemoryMapSize, MemoryMap, &MapKey, &DescriptorSize, &DescriptorVersion);
Status = uefi_call_wrapper(BS->ExitBootServices, 2, ImageHandle, MapKey);
// 9. カーネルにジャンプ!
KernelEntry kernel_entry = (KernelEntry)KernelAddress;
kernel_entry(boot_info);
while (1) { __asm__ volatile("hlt"); }
return EFI_SUCCESS;
}
実行結果
=== UEFI Bootloader - Kernel Loader ===
GOP: 1024x768, FB=0x80000000
FileSystem opened
kernel.bin opened
Kernel size: 352 bytes
Memory allocated at 0x100000 (1 pages)
Kernel loaded at 0x100000
BootInfo prepared:
FB: 0x80000000
Resolution: 1024x768
Press any key to jump to kernel...
キーを押すとExitBootServicesが呼ばれ、カーネルにジャンプします。
カーネルは画面を青く塗りつぶし、中央に白い四角形を描画します。
重要なポイント解説
AllocatePages vs AllocatePool
カーネルをロードするには AllocatePages を使います:
EFI_PHYSICAL_ADDRESS KernelAddress = KERNEL_LOAD_ADDRESS;
Status = uefi_call_wrapper(
BS->AllocatePages, 4,
AllocateAddress, // 指定アドレスに確保
EfiLoaderData, // メモリタイプ
Pages, // ページ数
&KernelAddress // [in/out] アドレス
);
AllocateAddress を指定することで、特定のアドレス(0x100000)にカーネルをロードできます。
ExitBootServices
Status = uefi_call_wrapper(BS->ExitBootServices, 2, ImageHandle, MapKey);
これを呼ぶと:
- Boot Services が使えなくなる
-
Print()などのUEFI関数が使えなくなる -
EfiBootServicesCode/Dataのメモリが解放される - Runtime Services は引き続き使える
注意: MapKey が古いと失敗します。失敗したら再取得してリトライ:
Status = BS->ExitBootServices(ImageHandle, MapKey);
if (EFI_ERROR(Status)) {
// メモリマップを再取得
GetMemoryMap(&MemoryMapSize, MemoryMap, &MapKey, ...);
// リトライ
BS->ExitBootServices(ImageHandle, MapKey);
}
カーネルへのジャンプ
typedef void (*KernelEntry)(BootInfo *);
KernelEntry kernel_entry = (KernelEntry)KernelAddress;
kernel_entry(boot_info);
C言語の関数ポインタでカーネルのエントリポイントを呼び出します。
ディレクトリ構造
uefi-bootloader/
├── Makefile
├── src/
│ └── main.c (ブートローダー)
├── kernel/
│ ├── kernel.c
│ ├── kernel.ld
│ └── kernel.bin (ビルド成果物)
└── esp/
├── EFI/
│ └── BOOT/
│ └── BOOTX64.EFI
└── kernel.bin
QEMUで実行
$ qemu-system-x86_64 \
-bios /usr/share/OVMF/OVMF_CODE.fd \
-drive format=raw,file=fat:rw:esp \
-m 256
GUIモードで実行すると、カーネルが描画した青い画面と白い四角形が見えるはずです!
この先の展望
ここまでの知識で、以下のことができるようになりました:
- ✅ UEFIアプリケーションの作成
- ✅ グラフィックス出力(GOP)
- ✅ ファイル読み込み
- ✅ メモリマップ取得
- ✅ カーネルのロードと実行
次のステップ:
- ページング設定
- GDT/IDTの設定
- 割り込みハンドラ
- キーボード入力
- メモリアロケータ
これらを実装していけば、本格的なOSに近づいていきます。
まとめ
5回にわたるUEFIシリーズ、いかがでしたか?
「BIOSは死んだ、UEFIの時代だ」
...と言いましたが、実際にはUEFIの理解はこれからの自作OS開発で必須の知識です。
GRUBなどのブートローダーに頼らず、ブートの仕組みから理解することで、OSの動作原理がより深く理解できるようになります。
ぜひ自分でも動かしてみてください!
参考資料
UEFI完全入門シリーズ 完
次はRISC-V自作CPUシリーズもよろしく!