前々から興味があった「ゼロからのOS自作入門」をゆっくり読んでみようと思います。
挫折する可能性が非常に高いですが、作業メモをQiitaに書いていこうと思います。
作業は基本的にこちらのリポジトリで行います。
バイナリエディタでブートローダーを実装
MACなので Hex Fiend というアプリで書いてみました。
バイナリエディタを使ったことがないので適当にインストールしました
とりあえず、チェックサムが 12430
になっていればOKです。
チェックサムが全然合わなくて心折れそう
sum ~/Downloads/BOOTX64.EFI
# 12430 2 /Users/ktamido/Downloads/BOOTX64.EFI
できたファイルはこちら -> BOOTX64.EFI (1.5 kB)
BOOTX64.EFIの内容
出力文字列情報
Hello, World!の文字列情報は 0x400
から 0x041b
あたりに存在します。
UEFIでは文字コードはUCS-2を使うことが決まっています。
UCS-2は一文字を2バイトで表します。例えば H
はASCIIでは 0x48
ですが、 UCS-2では 0x0048
となります。
x86-64アーキテクチャはリトルエンディアンを採用しているので、バイナリエディタで入力するときは下位バイトを先に入力し、 0x4800
となります。
機械語命令
0x0200
から 0x022f
あたりが文字列を画面に表示する命令のようです。
実機でイメージを起動
USBメモリをGPTでフォーマットして、EFIシステムパーティションの /EFI/BOOT/BOOTX64.EFI
に先ほど作成したBOOTX64.EFIを配置すると、PCで起動できるようです。
ちょうどいいPCがなかったので、企業のリース落ちらしきパソコンを1.5万円で購入 (結構いいスペックしてる)
そもそもBIOSとUEFI、MBRとGPTの違いがよくわかっていなかったので調べてみました。
ESP(EFIシステムパーティション)とは
UEFIにおいて、起動プロセスに必須のパーティションで、ブートローダー(/EFI/BOOT/BOOTX64.EFI
)やデバイスドライバ、システムユーティリティを格納します。
ファイルシステムは FAT32
でフォーマットされ、パーティションサイズは一般的に100MB ~ 1GB程度。
パーティションタイプGUIDは C12A7328-F81F-11D2-BA4B-00A0C93EC93B
となります。
fdiskでパーティションがESPかを確認することができます。
fdisk -l
# Disk /dev/nvme0n1: 1.82 TiB, 2000398934016 bytes, 3907029168 sectors
# Disk model: KIOXIA-EXCERIA PLUS G3 SSD
# Units: sectors of 1 * 512 = 512 bytes
# Sector size (logical/physical): 512 bytes / 512 bytes
# I/O size (minimum/optimal): 512 bytes / 512 bytes
# Disklabel type: gpt
# Disk identifier: 38496A13-AD66-44C7-8B35-7C373B9B75AB
#
# Device Start End Sectors Size Type
# /dev/nvme0n1p1 2048 2203647 2201600 1G EFI System
# /dev/nvme0n1p2 2203648 2931892223 2929688576 1.4T Linux filesystem
MacでUSBメモリに書き込む方法
ディスクユーティリティをCLIで使います。
USBメモリをGPTでフォーマットします。
# デバイスの確認
diskutil list
# ...
#/dev/disk4 (external, physical):
# #: TYPE NAME SIZE IDENTIFIER
# 0: GUID_partition_scheme *31.0 GB disk4
# 1: EFI NO NAME 209.7 MB disk4s1
# 2: Microsoft Basic Data NO NAME 30.8 GB disk4s2
# ディスクをGPTで初期化(UEFIで起動できるように)
# diskutil eraseDisk ファイルフォーマット ボリューム名 パーティション方式 デバイスファイルパス
diskutil eraseDisk ExFAT VOL1 GPT /dev/disk4
# Started erase on disk4
# Unmounting disk
# Creating the partition map
# Waiting for partitions to activate
# Formatting disk4s2 as ExFAT with name VOL1
# Volume name : VOL1
# Partition offset : 411648 sectors (210763776 bytes)
# Volume size : 60215296 sectors (30830231552 bytes)
# Bytes per sector : 512
# Bytes per cluster: 32768
# FAT offset : 2048 sectors (1048576 bytes)
# # FAT sectors : 8192
# Number of FATs : 1
# Cluster offset : 10240 sectors (5242880 bytes)
# # Clusters : 940704
# Volume Serial # : 6801d255
# Bitmap start : 2
# Bitmap file size : 117588
# Upcase start : 6
# Upcase file size : 5836
# Root start : 7
# Mounting disk
# Finished erase on disk
# 生成されたパーティションを確認
# NOTE: TYPE が EFI となっているのが EFIシステムパーティション (ESP)
diskutil list
# ...
# /dev/disk4 (external, physical):
# #: TYPE NAME SIZE IDENTIFIER
# 0: GUID_partition_scheme *31.0 GB disk4
# 1: EFI NO NAME 209.7 MB disk4s1
# 2: Microsoft Basic Data VOL1 30.8 GB disk4s2
# EFIシステムパーティション(disk4s1)を確認
diskutil info disk4s1
# Device Identifier: disk4s1
# Device Node: /dev/disk4s1
# Whole: No
# Part of Whole: disk4
#
# Volume Name: NO NAME
# Mounted: No
#
# Partition Type: EFI
# File System Personality: MS-DOS FAT32
# Type (Bundle): msdos
# Name (User Visible): MS-DOS (FAT32)
#
# OS Can Be Installed: No
# Media Type: 一般
# Protocol: USB
# SMART Status: Not Supported
# Volume UUID: 0E239BC6-F960-3107-89CF-1C97F78BB46B
# Disk / Partition UUID: D8B649D9-3BFE-4B21-B2DB-595251E72687
# Partition Offset: 20480 Bytes (40 512-Byte-Device-Blocks)
#
# Disk Size: 209.7 MB (209715200 Bytes) (exactly 409600 512-Byte-Units)
# Device Block Size: 512 Bytes
#
# Volume Total Space: 0 B (0 Bytes) (exactly 0 512-Byte-Units)
# Volume Free Space: 0 B (0 Bytes) (exactly 0 512-Byte-Units)
#
# Media OS Use Only: No
# Media Read-Only: No
# Volume Read-Only: Not applicable (not mounted)
#
# Device Location: External
# Removable Media: Removable
# Media Removal: Software-Activated
#
# Solid State: Info not available
# disk4s2を確認
diskutil info disk4s2
# Device Identifier: disk4s2
# Device Node: /dev/disk4s2
# Whole: No
# Part of Whole: disk4
#
# Volume Name: VOL1
# Mounted: Yes
# Mount Point: /Volumes/VOL1
#
# Partition Type: Microsoft Basic Data
# File System Personality: ExFAT
# Type (Bundle): exfat
# Name (User Visible): ExFAT
#
# OS Can Be Installed: No
# Media Type: 一般
# Protocol: USB
# SMART Status: Not Supported
# Volume UUID: 9EBED486-32B4-3369-A19E-2D42E1429BE7
# Disk / Partition UUID: 985066A8-8392-481A-9EF9-780E7566AF13
# Partition Offset: 210763776 Bytes (411648 512-Byte-Device-Blocks)
#
# Disk Size: 30.8 GB (30830231552 Bytes) (exactly 60215296 512-Byte-Units)
# Device Block Size: 512 Bytes
#
# Volume Total Space: 30.8 GB (30825021440 Bytes) (exactly 60205120 512-Byte-Units)
# Volume Used Space: 3.6 MB (3637248 Bytes) (exactly 7104 512-Byte-Units) (0.0%)
# Volume Free Space: 30.8 GB (30821384192 Bytes) (exactly 60198016 512-Byte-Units) (100.0%)
# Allocation Block Size: 512 Bytes
#
# Media OS Use Only: No
# Media Read-Only: No
# Volume Read-Only: No
#
# Device Location: External
# Removable Media: Removable
# Media Removal: Software-Activated
#
# Solid State: Info not available
EFIシステムパーティション(disk4s1)をマウントしてブートローダー(/EFI/BOOT/BOOTX64.EFI
)を配置します。
mkdir -p ~/mnt/esp
# EFIシステムパーティションをマウント
sudo diskutil mount -mountPoint ~/mnt/esp /dev/disk4s1
# ブートローダー(BOOTX64.EFI)を/EFI/BOOT/BOOTX64.EFIに配置
mkdir -p ~/mnt/esp/EFI/BOOT
cp BOOTX64.EFI ~/mnt/esp/EFI/BOOT/BOOTX64.EFI
# アンマウント
diskutil unmountDisk /dev/disk4
Linux(Ubuntu24.04)でUSBメモリに書き込む方法
USBメモリをGPTでフォーマットします。
# ディスクがマウントされてしまっている場合はアンマウント (GUI付きのLinuxだと必要)
sudo df -h
# ...
# /dev/sdb2 29G 3.5M 29G 1% /media/ktamido/VOL1
sudo umount /media/ktamido/VOL1
# 接続されているディスクを確認
sudo fdisk -l
# ...
# ディスク /dev/sda: 28.91 GiB, 31042043904 バイト, 60628992 セクタ
# Disk model: USB Flash Disk
# 単位: セクタ (1 * 512 = 512 バイト)
# セクタサイズ (論理 / 物理): 512 バイト / 512 バイト
# I/O サイズ (最小 / 推奨): 512 バイト / 512 バイト
# ディスクラベルのタイプ: gpt
# ディスク識別子: 1B5CA5F7-2AE4-4ACE-B9AC-15FC7FE1E041
DEVICE_FILE=/dev/sda
sudo parted $DEVICE_FILE
# パーティションテーブルを作成
(parted) mklabel gpt
(parted) print
# モデル: BUFFALO USB Flash Disk (scsi)
# ディスク /dev/sda: 31.0GB
# セクタサイズ (論理/物理): 512B/512B
# パーティションテーブル: gpt
# ディスクフラグ:
#
# 番号 開始 終了 サイズ ファイルシステム 名前 フラグ
# EFIシステムパーティション(ESP)を作成
(parted) mkpart primary fat32 1MiB 1GiB
(parted) set 1 esp on
(parted) print
# モデル: BUFFALO USB Flash Disk (scsi)
# ディスク /dev/sda: 31.0GB
# セクタサイズ (論理/物理): 512B/512B
# パーティションテーブル: gpt
# ディスクフラグ:
#
# 番号 開始 終了 サイズ ファイルシステム 名前 フラグ
# 1 1049kB 1074MB 1073MB primary boot, esp
# partedを終了
(parted) quit
# パーティションが作成できていることを確認
sudo fdisk -l $DEVICE_FILE
# ディスク /dev/sda: 28.91 GiB, 31042043904 バイト, 60628992 セクタ
# Disk model: USB Flash Disk
# 単位: セクタ (1 * 512 = 512 バイト)
# セクタサイズ (論理 / 物理): 512 バイト / 512 バイト
# I/O サイズ (最小 / 推奨): 512 バイト / 512 バイト
# ディスクラベルのタイプ: gpt
# ディスク識別子: 832B57BC-0A3B-442D-BD4B-5597B99950AB
#
# デバイス 開始位置 最後から セクタ サイズ タイプ
# /dev/sda1 2048 2097151 2095104 1023M EFI システム
# ファイルシステムを作成
sudo mkfs.vfat -F32 ${DEVICE_FILE}1
# ファイルシステムがfat32になっていることを確認
sudo parted $DEVICE_FILE print
# モデル: BUFFALO USB Flash Disk (scsi)
# ディスク /dev/sda: 31.0GB
# セクタサイズ (論理/物理): 512B/512B
# パーティションテーブル: gpt
# ディスクフラグ:
#
# 番号 開始 終了 サイズ ファイルシステム 名前 フラグ
# 1 1049kB 1074MB 1073MB fat32 primary boot, esp
EFIシステムパーティション(disk4s1)をマウントしてブートローダー(/EFI/BOOT/BOOTX64.EFI
)を配置します。
# /EFI/BOOT/BOOTX64.EFIを配置
sudo mkdir -p /mnt/esp
sudo mount ${DEVICE_FILE}1 /mnt/esp
sudo mkdir -p /mnt/esp/EFI/BOOT
sudo cp /home/ktamido/BOOTX64.EFI /mnt/esp/EFI/BOOT/BOOTX64.EFI
sudo umount /mnt/esp
UEFI BIOSで起動ディスクの1番目にUSBを設定
USBを起動するPCのUEFI BIOS画面を開いて以下の設定を行います。
※ 富士通FMVは起動時にF2でUEFI BIOSが起動します。
- 起動ディスクの1番目をUSBメモリに設定
- セキュアブートをOFFにする
起動
USBを挿して起動するとこんな感じに文字が表示されます。
QEMUでイメージを起動
QEMUでもイメージを起動できるようなので試してみます。
sudo apt install qemu-system-x86 git
mkdir tmp
cd tmp
# イメージの作成
qemu-img create -f raw disk.img 200M
mkfs.fat -n "MIKAN OS" -s 2 -f 2 -R 32 -F 32 disk.img
# 作成したイメージの確認
sudo parted disk.img print
# モデル: (file)
# ディスク /home/ktamido/Projects/mikanos/tmp/disk.img: 210MB
# セクタサイズ (論理/物理): 512B/512B
# パーティションテーブル: loop
# ディスクフラグ:
#
# 番号 開始 終了 サイズ ファイルシステム フラグ
# 1 0.00B 210MB 210MB fat32
# EFIシステムパーティション(ESP)の/ EFI/BOOT に BOOTX64.EFI を配置
mkdir -p mnt
sudo mount -o loop disk.img mnt
sudo mkdir -p mnt/EFI/BOOT
sudo cp BOOTX64.EFI mnt/EFI/BOOT/
sudo umount mnt
# OVMF_CODE.fd, OVMF_VARS.fdを参照するためにリポジトリをクローン
git clone https://github.com/uchan-nos/mikanos-build.git
# CLIモードで実行する場合は -nographicをつける
sudo qemu-system-x86_64 \
-drive if=pflash,file=mikanos-build/devenv/OVMF_CODE.fd,format=raw \
-drive if=pflash,file=mikanos-build/devenv/OVMF_VARS.fd,format=raw \
-drive file=disk.img,format=raw \
-nographic
起動するとこんな感じになる
CLIモードのQEMUを終了するときは Ctrl + a
を押して x
C言語でブートローダを実装
先ほどバイナリエディタで作成したブートローダーを今度はC言語で実装してみます。
C言語は書いたことがないですが、とりあえずやりながら調べて覚える方針。
この時点では何を書いているかさっぱりわからない。
エントリーポイントでprint("Hello, world")をするために必要な依存を無理やり定義している感じだろうか。
UEFIアプリケーション(ブートローダー)のソースコード
typedef unsigned short CHAR16;
typedef unsigned long long EFI_STATUS;
typedef void *EFI_HANDLE;
// _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL構造体の宣言。(定義は後で)
// この構造体をポインタ型として先に使いたいため宣言のみ行う
struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;
typedef EFI_STATUS (*EFI_TEXT_STRING)(
struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
CHAR16 *String
);
// _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL 構造体に EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL という別名をつける
typedef struct _EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL {
void *dummy; // void* 型はどんな方のポインタにもなれる汎用ポインタ。サイズ合わせや将来の拡張のために存在する
EFI_TEXT_STRING OutputString;
} EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL;
// 構造体を定義して EFI_SYSTEM_TABLE という別名を付ける
typedef struct {
char dummy[52];
EFI_HANDLE ConsoleOutHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
} EFI_SYSTEM_TABLE;
// エントリーポイント
EFI_STATUS EfiMain(
EFI_HANDLE IageHandle,
EFI_SYSTEM_TABLE *SystemTable
) {
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Hello, world!\n");
while(1);
return 0;
}
ブートローダーのビルド & イメージ作成 & 実行
sudo apt update && sudo apt install -y make clang lld qemu-system-x86
# hello.cをコンパイルしてCOFF形式のhello.oを生成する
# -target x86_64-pc-win32-coff: 出力を Windows向けの x86_64 COFF形式(=UEFIで使える形式)にする
# -mno-red-zone: red zone(スタックの下の128バイト)を使わない。割り込みが入る環境(UEFIなど)では red zone は危険なのでオフにする
# -fno-stack-protector: スタックプロテクションをオフにする。UEFIでは実行時ライブラリがないので不要(むしろ動かない)
# -fshort-wchar: wchar_t を 2バイト(UTF-16)にする。UEFI は CHAR16 = wchar_t(16bit)と前提しているため
# -Wall: 全ての一般的な警告を有効化(コードの品質チェック)
# -o $@ 出力ファイル名を指定
# -c: コンパイルのみ実行(リンクはしない)
clang -target x86_64-pc-win32-coff \
-mno-red-zone \
-fno-stack-protector \
-fshort-wchar \
-Wall \
-o hello.o -c hello.c
# hello.oをリンクしてPE形式の実行可能ファイル (hello.efi) を生成する
# -subsystem:efi_application UEFIファームウェアで起動可能な .efi 形式にする
# -entry:EfiMain エントリーポイントを EfiMain にする
# -out:hello.efi 出力ファイル名を指定
lld-link /subsystem:efi_application /entry:EfiMain /out:hello.efi hello.o
# イメージの作成
IMAGE_FILE=disk.img
qemu-img create -f raw $IMAGE_FILE 200M
# イメージにファイルシステム(FAT32)を作成
# -n "MIKAN OS" ボリュームラベルを "MIKAN OS" に設定します。エクスプローラーなどでドライブ名として表示される名前です。
# -s 2 セクタサイズを指定します。1セクタは通常 512 バイトなので、1 クラスタ = 1024 バイトになります。クラスタが小さいと空き容量の効率は良くなりますが、FAT テーブルのサイズは大きくなります。
# -f 2 FAT テーブルの数を指定します。通常 2 つ(FAT の冗長性を確保)。1 つが壊れても、もう 1 つでリカバリ可能。
# -R 32 ルートディレクトリエントリの予約数。FAT12/16ではルートディレクトリが固定サイズでしたが、FAT32ではこの数で予約されるクラスタ数を指定します(ファイル数とは直接関係しません)。
# -F 32 FAT の種類を指定します。ここでは FAT32。FAT12 や FAT16 にも変更可能ですが、容量が大きいディスクイメージでは FAT32 が推奨されます。
mkfs.fat -n "MIKAN OS" -s 2 -f 2 -R 32 -F 32 $IMAGE_FILE
sudo parted $IMAGE_FILE print
mkdir -p mnt
sudo mount -o loop $IMAGE_FILE mnt
sudo mkdir -p mnt/EFI/BOOT
sudo cp hello.efi mnt/EFI/BOOT/BOOTX64.EFI
sudo umount mnt
# qemuでイメージを実行
# Ctrl + a のあとに x でしゅう
sudo qemu-system-x86_64 \
-drive if=pflash,file=OVMF_CODE.fd,format=raw \
-drive if=pflash,file=OVMF_VARS.fd,format=raw \
-drive file=disk.img,format=raw \
-nographic
# お掃除
sudo rm -rf hello.efi hello.o disk.img mnt
最終的に出来上がったソースコード
Makefileを作って、ビルド、イメージ作成、QEMUで起動する一連の処理を1コマンドでできるようにしてみました。
make run