0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【ゼロからのOS自作入門をやってみる】第3章 画面表示の練習とブートローダ

Last updated at Posted at 2025-05-31

引き続き「ゼロからのOS自作入門」をやっていこうと思います。

この章のカーネルが動かなすぎて挫折しそうになりましたが、なんとかうごきました。
とはいっても、libc++の自力ビルドは挫折しましたが、、、

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

過去記事

QEMUモニタについて

QEMUモニタはCPUの設定を表示したり、メモリの中身を読み書きしたりすることができるため、デバッグ用途で利用されます。

QEMUモニタを起動するには qemuコマンドに -monitor studio オプションを付与します。

QEMUモニタを起動してみる

GUIありの場合

QEMUの起動コマンドに -monitor stdio オプションが付いているとQEMUを起動できる

make run

CLIでやる場合

make run-nographic
# ctrl-a + c でQEMUモニタに切り替え

QEMUモニタでレジスタの値を確認

CPUの各レジスタの値を表示してみます。

(qemu) info registers

# CPU#0
# RAX=0000000000000000 RBX=000000003df9fe20 RCX=000000003ed0eda0 RDX=000000003e657808
# RSI=000000003fe92680 RDI=000000003fe92638 RBP=0000000000000081 RSP=000000003fe925f0
# R8 =00000000000000af R9 =0000000000001000 R10=000000003dea2218 R11=0000000000000000
# R12=000000003fe93fb0 R13=000000003e98c000 R14=000000003e64e85a R15=000000003e64ed80
# RIP=000000003e64d411 RFL=00000206 [-----P-] 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=     000000003f5dc000 00000047
# IDT=     000000003f139018 00000fff
# CR0=80010033 CR2=0000000000000000 CR3=000000003f801000 CR4=00000668
# DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000 
# DR6=00000000ffff0ff0 DR7=0000000000000400
# EFER=0000000000000d00
# 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=8000000000000000 4006
# 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レジスタは次に実行される予定の機械語命令の位置を示します。
RIPレジスタに格納されているアドレスをメモリダンプ(xコマンド)で確認してみます。

# x /fmt addr
#   /fmt は /[個数][フォーマット][サイズ] 形式
#     - 個数 : 何個分表示するか
#     - フォーマット : x=16進数表記, d=10進数表記, i=機械語命令を逆アセンブル
#     - サイズ : 何バイトを1単位として解釈するか (b=1バイト, h=2バイト, w=4バイト, g=8バイト)
(qemu) x /4xb 0x000000003e64d411
# 000000003de82411: 0xeb 0xfe 0x48 0x83

RIPレジスタに登録されている機械語命令を逆アセンブルしてみます。

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

x86_64でやっているのだがなぜか動かない、、、

GDB(GNU Debugger)で逆アセンブルしてみる

CやC++のプログラムのデバッガで、ステップ実行、ブレークポイント設定、変数・メモリの監視・変更、関数呼び出し、逆アセンブルなどができます。

qemuの起動コマンドに -s(-gdb tcp::1234 のショートカット) を指定すると QEMUをGDBサーバーモードで起動でき、GDBで接続できるようになります。

GDBでよく使うコマンドには以下のようなものがあります。

コマンド 説明
info registers 全レジスタの値を表示
x/10i $pc 現在の命令ポインタから10命令を逆アセンブル表示
break <関数名> 指定した関数にブレークポイントを設定
continue 実行を再開
step / next ステップ実行(関数内に入る/入らない)
bt スタックトレースを表示
quit GDBを終了
# OSの起動
sudo qemu-system-x86_64 \
-m 1G \
-drive if=pflash,format=raw,file=/usr/share/OVMF/OVMF_CODE_4M.fd,readonly=on \
-drive if=pflash,format=raw,file=build/OVMF_VARS_4M.fd \
-drive if=ide,index=0,media=disk,format=raw,file=build/disk.img \
-nographic \
-s

別のターミナルからqemuのgdbサーバー(tcp::1234)に接続します。

gdb


# QEMUのGDBサーバーに接続 (QEMU実行時に-sオプションを指定すると接続できる)
(gdb) target remote :1234
# Remote debugging using localhost:1234
# warning: No executable has been specified and target does not support
# determining executable automatically.  Try using the "file" command.
# 0x000000003deb2411 in ?? ()

# RIPレジスタの示すメモリアドレスから4命令を逆アセンブル
(gdb) x /4i 0x000000003deb2411
# => 0x3de82411:	jmp    0x3de82411
#    0x3de82413:	sub    $0x28,%rsp
#    0x3de82417:	call   0x3de82240
#    0x3de8241c:	mov    %rdi,%rax

# 現在の命令ポインタから4命令を逆アセンブル
(gdb) x/4i $pc
# => 0x3de82411:	jmp    0x3de82411
#    0x3de82413:	sub    $0x28,%rsp
#    0x3de82417:	call   0x3de82240
#    0x3de8241c:	mov    %rdi,%rax


(gdb) exit

jmp 0x3de824110x3de82411 にジャンプするという命令ですが、 0x3de82411 はその命令がある場所そのものなので、結局は同じ場所をぐるぐると回ることになります。これは while (1); をコンパイルした結果です

レジスタとは

CPUには一般に汎用レジスタ特殊レジスタ が搭載されています。

  • 汎用レジスタ
    一般の演算に使えるレジスタ
  • 特殊レジスタ
    CPUの設定を行うためのものや、タイマーなどのCPUに内蔵された機能を制御するためのものがあります。

汎用レジスタ

x86_64の汎用レジスタは次の16個です。
RAX, RBX, RCX, RDX, RBP, RSI, RDI, RSP, R8 ~ R15

これらの汎用レジスタは以下のようにCPUの演算対象に指定できます。

add rax, rbx
; オペコード オペランド1, オペランド2

一般に x86-64の演算命令はオペコード(アセンブリ命令)が2つのオペランド(引数)を取り、左が書き込み先、右が読み込み元となります。
add rax, rbxRAXRBX を加えるという意味になるため、ちょうど rax += rbx のようになります。

x86-64の汎用レジスタはすべて8バイトですが、charやintなど8バイトよりも小さいデータ型を扱うために小さなレジスタが欲しい場合は、汎用レジスタの一部を小さなレジスタとしてアクセスできるようになっています。

register.png

特殊レジスタ

特殊レジスタは種類が多いため、一部紹介します。

レジスタ 説明
RIP CPUが次に実行する命令のメモリアドレスを保持するレジスタ
IPはInstruction Pointerの略
RFLAGS 命令の実行結果によって変化するフラグを集めたレジスタ
ビット0(CF: キャリーフラグ) 加算がオーバーフローするとCFが1になる。
ビット6(ZF: ゼロフラグ) 命令の実行結果が0になるとZFが1になる。
CR0 CPUの重要な設定を集めたレジスタ
ビット0 (PE) 1にするとCPUは保護モードに遷移
ビット31(PG) 1にするとページングが有効になる

フラグレジスタの普通の使い方

演算系の命令やcmpなどのフラグレジスタに影響を与える命令の直後にjzやcmovzなどのフラグレジスタの内容によって動作が変わる命令を配置します。

次のプログラムはRAXから1を引いた結果が0でなければループします。

  • dec: レジスタの値を1だけ減らします
  • jnz: Jump if Not Zeroの略で、RFLAGSのビット6(ZF: ゼロフラグ)が0の場合にのみジャンプします。
loop:
    dec rax
    jnz loop

※ RFRAGSレジスタは上記のプログラムに明示的には登場しませんが暗黙的に利用されています。

環境整備

この章に来て、ライブラリのバージョン違いなどでエラーが連発したので一旦環境を整備しました。
必要なライブラリのインストールはansibleで行います。 (Dockerでやればよかったかも)

必要なコマンド関連はすべてMakefile にまとめました

make build-bootloader     MikanLoaderPkg を edk2 でビルドします
make build-image          OSのイメージファイル(build/disk.img) を作成します
make build-kernel         kernel/ 配下のソースコードをビルドし、build/kernel/kernel.elf を作成します。
make clean                ビルドしたファイルを削除します
make help                 HELP表示
make mount-image          OSのイメージファイル(build/disk.img) を build/mnt にマウントします
make run-legacy           OSイメージをQEMUで起動します (OVMF_CODE.fd, OVMF_VARS.fd を利用)
make run-nographic-legacy OSイメージをQEMUのノーグラフィックモードで起動します (OVMF_CODE.fd, OVMF_VARS.fd を利用)
make run-nographic        OSイメージをQEMUのノーグラフィックモードで起動します (OVMF_CODE_4M.fd, OVMF_VARS_4M.fdを利用)
make run                  OSイメージをQEMUで起動します (OVMF_CODE_4M.fd, OVMF_VARS_4M.fdを利用)
make umount-image         OSのイメージファイル(build/disk.img) をbuild/mnt からアンマウントします

初めてのカーネル

ブートローダーはUEFIアプリとして、カーネルはELFバイナリとして別々に開発し、ブートローダーからカーネルを呼び出す形式にします。
UEFIアプリはUEFIの規格で決められたとおりに作らなければなりません。カーネルをブートローダーから分けることでUEFIの制約を気にせず自由に作ることができます。

カーネルの実装

一番最初のカーネルはhltを無限ループするカーネルです。

kernel/main.cpp
// extern "C" はC言語からこの関数呼び出すためマングリングを行わないようにする記述 (c++だと関数はデフォルトでマングリングされてしまう)
extern "C" void KernelMain() {
  // __asm__() はインラインアセンブリ。C言語からアセンブリ命令を呼び出すことができる
  // hlt はCPUを停止させる命令で省電力状態になり、割り込みがあると動作が再開します。(永久ループにするとCPUが100%に張り付いてしまう)
  while (1) __asm__("hlt");
}

カーネルのコンパイルとリンク

コンパイルとリンクの違い

  • コンパイル
    文法チェック、最適化、関数の中身の機械語化などを行い、 .o (オブジェクトファイル)を生成します。
    ※ ただし、この時点では外部関数や変数の定義は未解決のままで、実行はできません。
  • リンク
    外部関数や変数の実態を標準ライブラリなどから探して解決し、実行可能ファイルを生成します。

build.png

カーネルをコンパイル & リンクする

# コンパイル (オブジェクトファイルの生成)
# -O2 レベル2の最適化を行う
# -Wall 警告をたくさん出す
# -g デバッグ情報付きでコンパイルする
# --target=x86_64-elf x86_64向けの機械語を生成する。出力ファイルの形式をELFとする
# -ffreestanding フリースタンディング環境(OSがない環境)向けにコンパイルする
# -mno-red-zone Red Zone機能を無効にする (OSを作るときはとりあえずつけておく)
# -fno-exceptions C++の例外機能を使わない (OSのサポートが必要な言語機能は無効にする)
# -fno-rtti C++の動的型情報を使わない (OSのサポートが必要な言語機能は無効にする)
# -std=c++17 C++のバージョンをC++17とする
# -c コンパイルのみする。リンクはしない。
# -o build/kernel/main.o 出力先を指定
mkdir -p build/kernel
clang++-18 \
  -O2 \
  -Wall \
  -g \
  --target=x86_64-elf \
  -ffreestanding \
  -mno-red-zone \
  -fno-exceptions \
  -fno-rtti \
  -std=c++17 \
  -c \
  -o build/kernel/main.o \
  kernel/main.cpp

# リンク (オブジェクトファイルから実行ファイルを生成。オブジェクトファイルは単体では実行できない)
# --entry KernelMain KernelMain()をエントリーポイントとする
# -z norelro リロケーション情報を読み込み専用にする機能を使わない
# --image-base 0x100000 出力されたバイナリのベースアドレスを0x100000番地とする
# -o build/kernel/kernel.elf 出力先を指定
# --static 静的リンクを行う
ld.lld-18 \
  --entry KernelMain \
  -z norelro \
  --image-base 0x100000 \
  -static \
  -o build/kernel/kernel.elf \
  build/kernel/main.o

# kernel.elfという実行ファイルが生成されます
ll build/kernel/kernel.elf 
# -rwxrwxr-x 1 ktamido ktamido 1968  5月 24 10:50 build/kernel/kernel.elf*

ELF(Executable and Linkable Format)ファイルとは

ELFについてくわしく調べてみました。

kernel.elfのヘッダ

ELFヘッダ

readelf -h build/kernel/kernel.elf 
# ELF Header:
#   Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
#   Class:                             ELF64
#   Data:                              2's complement, little endian
#   Version:                           1 (current)
#   OS/ABI:                            UNIX - System V
#   ABI Version:                       0
#   Type:                              EXEC (Executable file)
#   Machine:                           Advanced Micro Devices X86-64
#   Version:                           0x1
#   Entry point address:               0x101120
#   Start of program headers:          64 (bytes into file)
#   Start of section headers:          1072 (bytes into file)
#   Flags:                             0x0
#   Size of this header:               64 (bytes)
#   Size of program headers:           56 (bytes)
#   Number of program headers:         4
#   Size of section headers:           64 (bytes)
#   Number of section headers:         14
#   Section header string table index: 12

プログラムヘッダ

readelf -l build/kernel/kernel.elf 
# Elf file type is EXEC (Executable file)
# Entry point 0x101120
# There are 4 program headers, starting at offset 64
# 
# Program Headers:
#   Type           Offset             VirtAddr           PhysAddr
#                  FileSiz            MemSiz              Flags  Align
#   PHDR           0x0000000000000040 0x0000000000100040 0x0000000000100040
#                  0x00000000000000e0 0x00000000000000e0  R      0x8
#   LOAD           0x0000000000000000 0x0000000000100000 0x0000000000100000
#                  0x0000000000000120 0x0000000000000120  R      0x1000
#   LOAD           0x0000000000000120 0x0000000000101120 0x0000000000101120
#                  0x0000000000000013 0x0000000000000013  R E    0x1000
#   GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
#                  0x0000000000000000 0x0000000000000000  RW     0x0
# 
#  Section to Segment mapping:
#   Segment Sections...
#    00     
#    01     
#    02     .text 
#    03

セクションヘッダ

readelf -S build/kernel/kernel.elf 
# There are 14 section headers, starting at offset 0x430:
# 
# Section Headers:
#   [Nr] Name              Type             Address           Offset
#        Size              EntSize          Flags  Link  Info  Align
#   [ 0]                   NULL             0000000000000000  00000000
#        0000000000000000  0000000000000000           0     0     0
#   [ 1] .text             PROGBITS         0000000000101120  00000120
#        0000000000000013  0000000000000000  AX       0     0     16
#   [ 2] .debug_abbrev     PROGBITS         0000000000000000  00000133
#        000000000000002d  0000000000000000           0     0     1
#   [ 3] .debug_info       PROGBITS         0000000000000000  00000160
#        000000000000002f  0000000000000000           0     0     1
#   [ 4] .debug_str_o[...] PROGBITS         0000000000000000  0000018f
#        0000000000000018  0000000000000000           0     0     1
#   [ 5] .debug_str        PROGBITS         0000000000000000  000001a7
#        0000000000000069  0000000000000001  MS       0     0     1
#   [ 6] .debug_addr       PROGBITS         0000000000000000  00000210
#        0000000000000010  0000000000000000           0     0     1
#   [ 7] .comment          PROGBITS         0000000000000000  00000220
#        0000000000000042  0000000000000001  MS       0     0     1
#   [ 8] .debug_frame      PROGBITS         0000000000000000  00000268
#        0000000000000038  0000000000000000           0     0     8
#   [ 9] .debug_line       PROGBITS         0000000000000000  000002a0
#        000000000000005e  0000000000000000           0     0     1
#   [10] .debug_line_str   PROGBITS         0000000000000000  000002fe
#        0000000000000037  0000000000000001  MS       0     0     1
#   [11] .symtab           SYMTAB           0000000000000000  00000338
#        0000000000000048  0000000000000018          13     2     8
#   [12] .shstrtab         STRTAB           0000000000000000  00000380
#        0000000000000097  0000000000000000           0     0     1
#   [13] .strtab           STRTAB           0000000000000000  00000417
#        0000000000000015  0000000000000000           0     0     1
# Key to Flags:
#   W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
#   L (link order), O (extra OS processing required), G (group), T (TLS),
#   C (compressed), x (unknown), o (OS specific), E (exclude),
#   D (mbind), l (large), p (processor specific)   

ブートローダーからカーネルを起動する (day03a)

この章のソースコードはこちら

カーネルを起動できるように、ブートローダーに機能を追加していくのですが、第3章のブートローダーのコードだとカーネルが起動しません。
この問題はissueでも上がっていますが、本の環境(Ubuntu18.04, リンカ: lld-7)と私の環境(Ubuntu24.04, リンカ: lld-18)が異なることに起因しており、lld-18でリンクした実行ファイルではLOAD領域を正確にメモリにロードしなければならないようです。

day03a以降、カーネルの起動まで進まない - issue | Github

この対応は、第4章の「ローダーを改良する」という項に書かれていますのでそちらも合わせて対応していきます。

ELFファイル(カーネル)のロードの流れ

ELFファイルのロードは以下の手順で行います。

  1. カーネルファイル(kernel.elf) を一時領域に読み込む
  2. 一時領域に読み込んだ カーネルファイルの.ELFヘッダーの解析
    1. ELFヘッダを読み取りプログラムヘッダーテーブルの位置( e_phoff ) 、エントリサイズ( e_phentsize )、要素数( e_phnum )を取得します。
  3. プログラムヘッダーテーブルの読み込みセグメントを最終目的地のメモリにロード
    1. p_typePT_LOAD のセグメントを対象とする
    2. p_offset から p_filesz バイトまでをメモリ上の p_vaddr にロード
    3. p_memszp_filesz よりも大きい場合は残りの領域を 0 で初期化
  4. エントリーポイントへのジャンプ
    1. ELFヘッダーの e_entry のアドレスに制御を移し、プログラムの実行を開始

ELFヘッダとプログラムヘッダの構造体を定義

ELFファイルのヘッダ関連(ELFヘッダ・プログラムヘッダ)の構造を定義します。

MikanLoaderPkg/elf.hpp
#pragma once

#include <stdint.h>

// NOTE: uintptr_t, uint64_tなどは stdint.h で定義されている
typedef uintptr_t Elf64_Addr;
typedef uint64_t  Elf64_Off;
typedef uint16_t  Elf64_Half;
typedef uint32_t  Elf64_Word;
typedef int32_t   Elf64_Sword;
typedef uint64_t  Elf64_Xword;
typedef int64_t   Elf64_Sxword;

#define EI_NIDENT 16

// 64bit ELFファイルのヘッダ
typedef struct {
  unsigned char e_ident[EI_NIDENT];
  Elf64_Half    e_type;
  Elf64_Half    e_machine;
  Elf64_Word    e_version;
  Elf64_Addr    e_entry;
  Elf64_Off     e_phoff;      // プログラムヘッダ(配列)のオフセット
  Elf64_Off     e_shoff;
  Elf64_Word    e_flags;
  Elf64_Half    e_ehsize;
  Elf64_Half    e_phentsize;  // プログラムヘッダ(配列)の要素のサイズ
  Elf64_Half    e_phnum;      // プログラムヘッダ(配列)の要素数
  Elf64_Half    e_shentsize;
  Elf64_Half    e_shnum;
  Elf64_Half    e_shstrndx;
} Elf64_Ehdr;

// 64bit ELFファイルのプログラムヘッダの要素
typedef struct {
  Elf64_Word  p_type;    // PHDR, LOADなどのセグメント種別
  Elf64_Word  p_flags;   // フラグ
  Elf64_Off   p_offset;  // オフセット
  Elf64_Addr  p_vaddr;   // 仮想 Addr
  Elf64_Addr  p_paddr;
  Elf64_Xword p_filesz;  // ファイルサイズ
  Elf64_Xword p_memsz;   // メモリサイズ
  Elf64_Xword p_align;
} Elf64_Phdr;

#define PT_NULL    0
#define PT_LOAD    1
#define PT_DYNAMIC 2
#define PT_INTERP  3
#define PT_NOTE    4
#define PT_SHLIB   5
#define PT_PHDR    6
#define PT_TLS     7

カーネルファイルを一時領域に読み込み

MikanLoaderPkg/Main.c
  /**
   * カーネルファイルを読み取り専用で開く
   */
  EFI_FILE_PROTOCOL* kernel_file;
  root_dir->Open(
    root_dir,
    &kernel_file,
    L"\\kernel.elf",
    EFI_FILE_MODE_READ,
    0
  );

  /**
   * カーネルファイルのファイルサイズを取得
   */
  // EFI_FILE_INFO型を十分格納できる大きさのメモリを確保
  // FileNameは可変長なのでファイル名が収まるくらいのメモリを追加で確保する必要がある
  //
  // EFI_FILE_INFO: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Guid/FileInfo.h#L19
  //   UINT64    Size;
  //   UINT64    FileSize;
  //   UINT64    PhysicalSize;
  //   EFI_TIME  CreateTime;
  //   EFI_TIME  LastAccessTime;
  //   EFI_TIME  ModificationTime;
  //   UINT64    Attribute;
  //   CHAR16    FileName[1];
  UINTN file_info_size = sizeof(EFI_FILE_INFO) + sizeof(CHAR16) * 12;
  // UINT8: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/X64/ProcessorBind.h#L167
  // 1byte符号なし整数
  UINT8 file_info_buffer[file_info_size];

  // kernel_file を file_info_buffer に展開
  // EFI_FILE_GET_INFO: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Protocol/SimpleFileSystem.h#L287
  kernel_file->GetInfo(
    kernel_file,        // IN EFI_FILE_PROTOCOL  *This
    &gEfiFileInfoGuid,  // IN EFI_GUID           *InformationType
    &file_info_size,    // IN OUT UINTN          *BufferSize
    file_info_buffer    // OUT VOID              *Buffer
  );

  EFI_FILE_INFO* file_info = (EFI_FILE_INFO*)file_info_buffer;
  // カーネルファイルのファイルサイズを取得
  UINTN kernel_file_size = file_info->FileSize;

  /**
   * カーネルファイルを一時領域に読み込む
   */
  VOID* kernel_buffer;
  // EFI_ALLOCATE_POOL: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L268
  gBS->AllocatePool(
    EfiLoaderData,     // IN  EFI_MEMORY_TYPE  PoolType,
    kernel_file_size,  // IN  UINTN            Size,
    &kernel_buffer     // OUT VOID             **Buffer
  );
  kernel_file->Read(
    kernel_file,             // IN EFI_FILE_PROTOCOL  *This
    &kernel_file_size,       // IN OUT UINTN          *BufferSize
    (VOID*)kernel_buffer     // OUT VOID              *Buffer
  );

カーネルファイルの最終ロード先のメモリ領域の確保

MikanLoaderPkg/Main.c
  /**
   * コピー先のメモリを確保
   */
  Elf64_Ehdr* kernel_ehdr = (Elf64_Ehdr*)kernel_buffer;
  UINT64 kernel_first_addr, kernel_last_addr;
  CalcLoadAddressRange(kernel_ehdr, &kernel_first_addr, &kernel_last_addr);
  
  UINTN num_pages = (kernel_last_addr - kernel_first_addr + 0xfff) / 0x1000;
  // EFI_ALLOCATE_PAGES: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L186
  status = gBS->AllocatePages(
    AllocateAddress,   // IN     EFI_ALLOCATE_TYPE    Type : https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L29
                       //   AllocateAnyPages: どこでもいいから空いている場所に確保
                       //   AllocateMaxAddress: 指定したアドレス以下で空いている場所に確保
                       //   AllocateAddress: 指定したアドレスに確保
    EfiLoaderData,     // IN     EFI_MEMORY_TYPE      MemoryType : https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiMultiPhase.h#L38
                       //   UEFI アプリケーションやドライバがメモリを割り当てる際、どのような目的で使用するメモリかを指定する
                       //   EfiLoaderCode: ロードされたアプリケーションのコードセクション
                       //   EfiLoaderData: ロードされたアプリケーションのデータセクション
    num_pages,         // IN     UINTN                Pages
    &kernel_first_addr // IN OUT EFI_PHYSICAL_ADDRESS *Memory
  );

CalcLoadAddressRangeの実装

最終ロード先のメモリ領域を特定する CalcLoadAddressRange を実装します。

  1. プログラムヘッダテーブルの要素を1つづつ走査します。
  2. LOADセグメントを見つけたら、そのセグメントの先頭アドレスと末尾アドレスを取得します。
  3. first よりも先頭アドレスが小さければ first を更新、 last よりも末尾アドレスが大きければ last を更新します。
    この作業を繰り返していくと、全LOADセグメントを収めるのに必要なメモリ領域の最初と最後のアドレスが分かります。
MikanLoaderPkg/Main.c
/**
 * カーネルファイル内のすべてのLOADセグメント(p_type が PT_LOADであるセグメント)を走査し、
 * アドレス範囲を更新します。
 */
VOID CalcLoadAddressRange(Elf64_Ehdr* ehdr, UINT64* first, UINT64* last) {
  Elf64_Phdr* phdr = (Elf64_Phdr*)((UINT64)ehdr + ehdr->e_phoff);
  *first = MAX_UINT64;
  *last = 0;

  for (Elf64_Half i = 0; i < ehdr->e_phnum; ++i) {
    if (phdr[i].p_type != PT_LOAD) {
      continue;
    }
    *first = MIN(*first, phdr[i].p_vaddr);
    *last = MAX(*last, phdr[i].p_vaddr + phdr[i].p_memsz);
  }
}

最終ロード先にカーネルファイルをロード

MikanLoaderPkg/Main.c
  CopyLoadSegments(kernel_ehdr);
  Print(L"Kernel: 0x%0lx - 0x%lx\n", kernel_first_addr, kernel_last_addr);

  // EFI_FREE_POOL: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h?utm_source=chatgpt.com#L285
  status = gBS->FreePool(kernel_buffer);

CopyLoadSegmentsの実装

一次領域にロードしたカーネルファイルのロードセグメントを最終目的地のメモリアドレスにコピーします。

  1. LOADセグメントに対して2つの処理を行います
  2. segm_in_fileが指す一時領域から p_vaddr が指す最終目的地へデータをコピーします
  3. セグメントのメモリ上のサイズがファイル上のサイズより大きい場合(remain_bytes > 0)、残りを0で埋めます(SetMem())
MikanLoaderPkg/Main.c
/**
 * カーネルファイルのLOADセグメントを一時領域から最終目的地のメモリアドレスにデータをコピーします
 */
VOID CopyLoadSegments(Elf64_Ehdr* ehdr) {
  Elf64_Phdr* phdr = (Elf64_Phdr*)((UINT64)ehdr + ehdr->e_phoff);
  for (Elf64_Half i = 0; i < ehdr->e_phnum; i++) {
    if (phdr[i].p_type != PT_LOAD) {
      continue;
    }

    // 一時領域から最終目的地へデータをコピー
    UINT64 segm_in_file = (UINT64)ehdr + phdr[i].p_offset;
    // CopyMem: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Library/BaseMemoryLib.h#L33
    CopyMem(
      (VOID*)phdr[i].p_vaddr,  // IN VOID   *Destination
      (VOID*)segm_in_file,     // IN VOID   *Source
      phdr[i].p_filesz         // IN UINTN  Length
    );

    // 残りのメモリを0埋め
    UINTN remain_bytes = phdr[i].p_memsz - phdr[i].p_filesz;
    // SetMem: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Library/BaseMemoryLib.h#L55
    SetMem(
      (VOID*)(phdr[i].p_vaddr + phdr[i].p_filesz),  // IN VOID   *Buffer
      remain_bytes,                                 // IN UINTN  Size
      0                                             // IN UINT8  Value
    );
  }
}

ブートサービスの停止とカーネルの起動

ブートサービスを停止して、カーネルを起動し、処理をOSに移譲します。

MikanLoaderPkg/Main.c
  /**
   * カーネルの起動
   */
  // UINT64: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/X64/ProcessorBind.h#L180
  // メモリ上でエントリーポイントがおいてあるアドレス
  // ELF形式の仕様では64bit用のELFのエントリポイントアドレスはオフセット24byteの位置から8バイト整数として書かれる事になっている
  // ELFの情報は readelf -h build/kernel/kernel.elf で確認できる
  UINT64 entry_addr = *(UINT64*)(kernel_first_addr + 24);

  // エントリポイントをC言語の関数として呼び出すために、関数ポインタにキャスト
  typedef void EntryPointType(void);
  EntryPointType* entry_point = (EntryPointType*)entry_addr;
  Print(L"entry_point: 0x%p\n", entry_point);

  /**
   * カーネル起動前にUEFI BIOSのブートサービスを停止
   */
  // EFI_EXIT_BOOT_SERVICES: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L983
  //   ブートサービスを停止させる。この関数が成功した場合、以降にブートサービスの機能を使うことはできない。
  status = gBS->ExitBootServices(
    image_handle,  // IN  EFI_HANDLE ImageHandle
    memmap.map_key // IN  UINTN      MapKey       最新のメモリマップのマップキーを要求。マップキーが最新のメモリマップに紐づく値でない場合は失敗する
  );
  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);
    }
  }

  // 関数ポインタを実行
  entry_point();

起動

Makefile で カーネルのビルド、ブートローダーのビルド、イメージ作成、QEMUでイメージの起動を一括して行います。

make run-nographic
# BdsDxe: loading Boot0001 "UEFI QEMU HARDDISK QM00001 " from PciRoot(0x0)/Pci(0x1,0x1)/Ata(Primary,Master,0x0)
# BdsDxe: starting Boot0001 "UEFI QEMU HARDDISK QM00001 " from PciRoot(0x0)/Pci(0x1,0x1)/Ata(Primary,Master,0x0)
# Hello, MikanOS!
# map->buffer = 3FE92610, map->map_size = 00001860
# Kernel: 0x100000 - 0x101133
# entry_point: 0x101120

gdbでhltが無限ループしているかを確認

gdb

# QEMUモニタに接続
(gdb) targetremote :1234
# Remote debugging using :1234
# warning: No executable has been specified and target does not support
# determining executable automatically.  Try using the "file" command.
# 0x0000000000101131 in ?? ()

# 現在のRIPレジスタを確認
(gdb) x /2i $pc
# => 0x101131:    jmp    0x101130
#    0x101133:    add    %al,(%rax)

# 1バイト遡ってみると hlt が確認できる
(gdb) x /2i 0x101130
#    0x101130:    hlt
# => 0x101131:    jmp    0x101130  ★ hltにjmpして無限ループしている

ブートローダからピクセルを描く (day03b)

この章のソースコードはこちら

UEFIでピクセル単位で画面を描画するにはGOP(Graphics Output Protocol) という機能を利用します。

ピクセル描画には次の情報が必要になります。

  • フレームバッファの先頭アドレス
  • フレームバッファの表示領域の幅と高さ
    いわゆる解像度
  • フレームバッファの非表示領域を含めた幅
    フレームバッファには、表示領域の右側に表示されない余分な横幅が存在する場合がある
  • 1ピクセルのデータ形式
    フレームバッファの中で1ピクセルが何バイトで表現されているか。RGBの3色が何ビットずつどんな順番で並んでいるかという情報。1ピクセルがRGB各8bitであれば、256階調の3乗で1677万色を表示可能

※ フレームバッファ(Frame Buffer)
ピクセルに描画するためのメモリ領域。
フレームバッファの各メモリに値を書き込むとそれがディスプレイのピクセルに反映されます。

実装

gopの取得しそれを使ってピクセルを描画する処理をUefiMainに実装します。

MikanLoaderPkg/Main.c
  /**
   * GOPを取得して画面描画する
   */
  // EFI_GRAPHICS_OUTPUT_PROTOCOL: https://github.com/tianocore/edk2/blob/edk2-stable202208/MdePkg/Include/Protocol/GraphicsOutput.h#L258
  EFI_GRAPHICS_OUTPUT_PROTOCOL* gop;
  OpenGOP(image_handle, &gop);
  // EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE: https://github.com/tianocore/edk2/blob/edk2-stable202208/MdePkg/Include/Protocol/GraphicsOutput.h#L224
  Print(L"Resolution: %ux%u, Pixel Format: %s, %u pixels/line\n",
    gop->Mode->Info->HorizontalResolution,  // 水平方向のピクセル数
    gop->Mode->Info->VerticalResolution,    // 垂直方向のピクセル数
    GetPixelFormatUnicode(gop->Mode->Info->PixelFormat),  // フレームバッファで1ピクセルを表すデータ形式
    gop->Mode->Info->PixelsPerScanLine  // ビデオメモリラインあたりのピクセル数
  );
  Print(L"Frame Buffer: 0x%0lx - 0x%0lx, Size: %lu bytes\n",
    gop->Mode->FrameBufferBase,  // フレームバッファの先頭アドレス
    gop->Mode->FrameBufferBase + gop->Mode->FrameBufferSize,  // フレームバッファの末尾アドレス
    gop->Mode->FrameBufferSize  // フレームバッファの全体サイズ
  );

  UINT8* frame_buffer = (UINT8*)gop->Mode->FrameBufferBase;
  for (UINTN i = 0; i < gop->Mode->FrameBufferSize; i++) {
    frame_buffer[i] = 255;
  }

OpenGOPGetPixelFormatUnicode の実装

MikanLoaderPkg/Main.c
EFI_STATUS OpenGOP(EFI_HANDLE image_handle, EFI_GRAPHICS_OUTPUT_PROTOCOL** gop) {
  UINTN num_gop_handles = 0;
  EFI_HANDLE* gop_handles = NULL;
  // EFI_LOCATE_HANDLE_BUFFER: https://github.com/tianocore/edk2/blob/edk2-stable202208/MdePkg/Include/Uefi/UefiSpec.h#L1570
  // リクエストされたプロトコルをサポートするハンドルの配列を取得する
  gBS->LocateHandleBuffer(
    ByProtocol,                      // IN  EFI_LOCATE_SEARCH_TYPE  SearchType            返却されるハンドルの指定
    &gEfiGraphicsOutputProtocolGuid, // IN  EFI_GUID                *Protocol   OPTIONAL  検索するプロトコルを指定(SearchType=ByProtocolの場合のみ有効)
    NULL,                            // IN  VOID                    *SearchKey  OPTIONAL  SearchType に応じた検索キーを指定
    &num_gop_handles,                // OUT UINTN                   *NoHandles            取得したハンドルの数
    &gop_handles                     // OUT EFI_HANDLE              **Buffer              バッファから返却されたハンドルの配列
  );
  // EFI_OPEN_PROTOCOL: https://github.com/tianocore/edk2/blob/edk2-stable202302/MdePkg/Include/Uefi/UefiSpec.h#L1330
  //   ハンドルが指定されたプロトコルをサポートしているかを問い合わせ、 サポートしている場合、呼び出し元のエージェントに代わってプロトコルをオープンする
  gBS->OpenProtocol(
    gop_handles[0],
    &gEfiGraphicsOutputProtocolGuid,
    (VOID**)gop,
    image_handle,
    NULL,
    EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL
  );
  return EFI_SUCCESS;
}

/**
 * EFI_GRAPHICS_PIXEL_FORMATを文字列に変換する
 */
const CHAR16* GetPixelFormatUnicode(EFI_GRAPHICS_PIXEL_FORMAT fmt) {
  // EFI_GRAPHICS_PIXEL_FORMAT: https://github.com/tianocore/edk2/blob/edk2-stable202208/MdePkg/Include/Protocol/GraphicsOutput.h#L28
  switch (fmt) {
    case PixelRedGreenBlueReserved8BitPerColor:
      return L"PixelRedGreenBlueReserved8BitPerColor";
    case PixelBlueGreenRedReserved8BitPerColor:
      return L"PixelBlueGreenRedReserved8BitPerColor";
    case PixelBitMask:
      return L"PixelBitMask";
    case PixelBltOnly:
      return L"PixelBltOnly";
    case PixelFormatMax:
      return L"PixelFormatMax";
    default:
      return L"InvalidPixelFormat";
  }
}

イメージの起動

make run

image.png

カーネルからピクセルを描く (day03c)

この章のソースコードはこちら

libc,libc++のビルド

カーネルでC++の標準ライブラリを利用するため、libc++を自力でビルドしようと思ったのですが、知識が足りなすぎて断念、、、

一応、このサイトを参考にやろうとしました。

Newlib

libcのビルドでNewlibというものをビルドしていたので、気になって調べてみると、組み込みシステム向けに設計されたC標準ライブラリの実装で、自作OSでC標準ライブラリが必要なときはよく使われているようです。

結局Dockerでビルド

著者の方がビルド用のイメージを出してくれてました。

docker pull uchannos/stdlib-builder:1.1
docker run -v ./build/x86_64-elf:/usr/local/x86_64-elf uchannos/stdlib-builder:1.1

実装

Main.cでカーネルのエンドポイントにフレームバッファのアドレスとサイズを渡すように修正します。

MikanLoaderPkg/Main.c
diff --git a/MikanLoaderPkg/Main.c b/MikanLoaderPkg/Main.c
index b2cc87c..696a913 100644
--- a/MikanLoaderPkg/Main.c
+++ b/MikanLoaderPkg/Main.c
@@ -438,7 +438,7 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE* system_tab
   UINT64 entry_addr = *(UINT64*)(kernel_first_addr + 24);
 
   // エントリポイントをC言語の関数として呼び出すために、関数ポインタにキャスト
-  typedef void EntryPointType(void);
+  typedef void EntryPointType(UINT64, UINT64);
   EntryPointType* entry_point = (EntryPointType*)entry_addr;
   Print(L"entry_point: 0x%p\n", entry_point);
 
@@ -467,7 +467,10 @@ EFI_STATUS EFIAPI UefiMain(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE* system_tab
   }
 
   // 関数ポインタを実行
-  entry_point();
+  entry_point(
+    gop->Mode->FrameBufferBase,
+    gop->Mode->FrameBufferSize
+  );
 
   Print(L"All done\n");

カーネル側では、渡されたフレームバッファのアドレスとサイズを利用してピクセルを描画します。

kernel/main.cpp
#include <cstdint>

// extern "C" はC言語からこの関数呼び出すためマングリングを行わないようにする記述
extern "C" void KernelMain(uint64_t frame_buffer_base, uint64_t frame_buffer_size) {
  // reinterpret_cast:
  //   ある型のビットパターンをそのまま別の型として再解釈するキャスト。C言語の(uint8_t*)frame_buffer_baseと効果は全く同じ
  uint8_t* frame_buffer = reinterpret_cast<uint8_t*>(frame_buffer_base);
  for (uint64_t i = 0; i < frame_buffer_size; i++) {
    frame_buffer[i] = i % 256;
  }
  // __asm__() はインラインアセンブリ。C言語からアセンブリ命令を呼び出すことができる
  // hlt はCPUを停止させる命令で省電力状態になる。割り込みがあると動作が再開する。(永久ループにするとCPUが100%に張り付いてしまう)
  while (1) __asm__("hlt");
}

実行

#include <cstdint> のためにlibc++が必要なので、Makefileにlibc++のビルドとlibc++のインクルードを設定します。

Makefile
diff --git a/Makefile b/Makefile
index 5728134..6aa9b31 100644
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,10 @@ build/BOOTX64.EFI: $(MIKAN_LOADER_PKG)
        cp edk2/Build/MikanLoaderX64/DEBUG_CLANG38/X64/Loader.efi $@
        chmod +x $@
 
+build/x86_64-elf/include/c++/v1:
+       docker pull uchannos/stdlib-builder:1.1
+       docker run -v ./build/x86_64-elf:/usr/local/x86_64-elf uchannos/stdlib-builder:1.1
+
 # -O2 レベル2の最適化を行う
 # -Wall 警告をたくさん出す
 # -g デバッグ情報付きでコンパイルする
@@ -17,9 +21,13 @@ build/BOOTX64.EFI: $(MIKAN_LOADER_PKG)
 # -std=c++17 C++のバージョンをC++17とする
 # -c コンパイルのみする。リンクはしない。
 # -o build/kernel/main.o 出力先を指定
-build/kernel/main.o: kernel/main.cpp
+build/kernel/main.o: kernel/main.cpp build/x86_64-elf/include/c++/v1
        mkdir -p build/kernel
        clang++-18 \
+         -Ibuild/x86_64-elf/include/c++/v1 -Ibuild/x86_64-elf/include -Ibuild/x86_64-elf/include/freetype2 \
+         -Iedk2/MdePkg/Include -Iedk2/MdePkg/Include/X64 \
+         -nostdlibinc -D__ELF__ -D_LDBL_EQ_DBL -D_GNU_SOURCE -D_POSIX_TIMERS \
+         -DEFIAPI='__attribute__((ms_abi))' \
          -O2 \
          -Wall \
          -g \

OSの起動

make run

image.png

エラー処理 (day03d)

この章のソースコードは こちら
https://github.com/ng3rdstmadgke/mikanos/tree/day03

edk2の関数は基本的に戻り値に EFI_STATUS(実態はRETURN_STATUS) を返します。
このステータスは EFI_ERROR関数(実態はRETURN_ERROR) で失敗かどうかを判定できます。
ここでは、edk2の関数の戻り値のステータスを判定して、エラーステータスであれば、while (1) __asm__("hlt"); でプログラムを停止するという処理を追加していきます。

実装

ソースコードはこちら

ブートローダーを停止する Halt 関数

Main.c
void Halt(void) {
  while (1) __asm__("hlt");
}

エラーハンドリング

%r フォーマット指定子は RETURN_STATUS の値を展開します。(参考: Print関数のフォーマット指定子の仕様 | edk2)

Main.c
  status = OpenRootDir(image_handle, &root_dir);
  if (EFI_ERROR(status)) {
    Print(L"failed to open root directory: %r\n", status);
    Halt();
  }

実行

OSの起動

make run

image.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?