ZigでOSを作るシリーズ
| Part1 基本 | Part2 ブート | Part3 割り込み | Part4 メモリ | Part5 プロセス | Part6 FS |
|---|---|---|---|---|---|
| ✅ Done | 👈 Now | - | - | - | - |
はじめに
前回はZigの基本とno_stdについて学んだ。
今回はx86_64のブートプロセスを実装するよ。Multiboot2対応のカーネルを作って、GRUBから起動できるようにする。
ここが結構泥くさいところなんだけど、一回理解すれば他のOSでも使える知識になるから頑張ろう。
ブートプロセスの概要
┌────────────────────────────────────────────────────────────────┐
│ x86_64 ブートシーケンス │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. BIOS/UEFI が起動 │
│ ↓ │
│ 2. ブートローダー(GRUB)がロードされる │
│ ↓ │
│ 3. ブートローダーがカーネルをロード │
│ ↓ │
│ 4. Multiboot2ヘッダを確認 │
│ ↓ │
│ 5. カーネルのエントリポイントにジャンプ │
│ ↓ │
│ 6. カーネル初期化開始 │
│ │
└────────────────────────────────────────────────────────────────┘
Multiboot2とは
Multiboot2は、ブートローダーとOSカーネルの間の標準インターフェース。
GRUBなどのブートローダーは、Multiboot2ヘッダを持つカーネルを認識し、適切にロードしてくれる。
Multiboot2ヘッダの構造
// multiboot2.zig
pub const MAGIC: u32 = 0xE85250D6;
pub const ARCHITECTURE_I386: u32 = 0;
pub const HeaderTag = extern struct {
type: u16,
flags: u16,
size: u32,
};
pub const Header = extern struct {
magic: u32 = MAGIC,
architecture: u32 = ARCHITECTURE_I386,
header_length: u32,
checksum: u32,
pub fn init(header_length: u32) Header {
return .{
.header_length = header_length,
.checksum = @bitCast(
-%(@as(i32, @bitCast(MAGIC)) +
@as(i32, @bitCast(ARCHITECTURE_I386)) +
@as(i32, @bitCast(header_length)))
),
};
}
};
// タグタイプ
pub const TAG_TYPE_END: u16 = 0;
pub const TAG_TYPE_INFORMATION_REQUEST: u16 = 1;
pub const TAG_TYPE_ADDRESS: u16 = 2;
pub const TAG_TYPE_ENTRY_ADDRESS: u16 = 3;
pub const TAG_TYPE_FRAMEBUFFER: u16 = 5;
// 終端タグ
pub const EndTag = extern struct {
type: u16 = TAG_TYPE_END,
flags: u16 = 0,
size: u32 = 8,
};
// フレームバッファタグ
pub const FramebufferTag = extern struct {
type: u16 = TAG_TYPE_FRAMEBUFFER,
flags: u16 = 0,
size: u32 = 20,
width: u32,
height: u32,
depth: u32,
};
ブートコードの実装
Zigだけじゃ最初のブートコードは書けないんだよね。アセンブリが必要。
; boot.s - アセンブリのブートストラップコード
.section .multiboot2
.align 8
multiboot_header_start:
.long 0xE85250D6 ; magic
.long 0 ; architecture (i386)
.long multiboot_header_end - multiboot_header_start ; header length
.long -(0xE85250D6 + 0 + (multiboot_header_end - multiboot_header_start)) ; checksum
; フレームバッファタグ
.align 8
.word 5 ; type = framebuffer
.word 0 ; flags
.long 20 ; size
.long 1024 ; width
.long 768 ; height
.long 32 ; depth
; 終端タグ
.align 8
.word 0 ; type = end
.word 0 ; flags
.long 8 ; size
multiboot_header_end:
.section .bss
.align 16
stack_bottom:
.space 16384 ; 16KB スタック
stack_top:
.section .text
.global _start
.type _start, @function
_start:
; スタックポインタを設定
mov $stack_top, %esp
; Multiboot情報を引数として渡す
push %ebx ; Multiboot info pointer
push %eax ; Multiboot magic
; Zigのカーネルメイン関数を呼び出し
call kernel_main
; 戻ってきたら停止
cli
1: hlt
jmp 1b
Zigから参照できる形に
// boot.zig - Zigのエントリポイント
const multiboot2 = @import("multiboot2.zig");
const vga = @import("vga.zig");
// Multiboot2ヘッダ(セクションに配置)
const MultibootHeader = extern struct {
header: multiboot2.Header,
framebuffer_tag: multiboot2.FramebufferTag,
end_tag: multiboot2.EndTag,
};
export const multiboot_header linksection(".multiboot2") = MultibootHeader{
.header = multiboot2.Header.init(@sizeOf(MultibootHeader)),
.framebuffer_tag = .{
.width = 1024,
.height = 768,
.depth = 32,
},
.end_tag = .{},
};
// カーネルのメインエントリポイント
export fn kernel_main(magic: u32, info: *multiboot2.BootInfo) callconv(.C) noreturn {
if (magic != multiboot2.BOOTLOADER_MAGIC) {
vga.print("Invalid Multiboot2 magic!\n");
halt();
}
vga.clear();
vga.print("Zig OS Kernel\n");
vga.print("=============\n\n");
vga.print("Multiboot2 boot successful!\n");
// Multiboot情報を解析
parseMultibootInfo(info);
halt();
}
fn halt() noreturn {
while (true) {
asm volatile ("cli; hlt");
}
}
Multiboot2情報の解析
GRUBはカーネルに様々な情報を渡してくれる。
// multiboot2.zig
pub const BOOTLOADER_MAGIC: u32 = 0x36D76289;
pub const BootInfo = extern struct {
total_size: u32,
reserved: u32,
};
pub const Tag = extern struct {
type: u32,
size: u32,
};
// タグタイプ
pub const BOOT_TAG_TYPE_END: u32 = 0;
pub const BOOT_TAG_TYPE_CMDLINE: u32 = 1;
pub const BOOT_TAG_TYPE_BOOT_LOADER_NAME: u32 = 2;
pub const BOOT_TAG_TYPE_MMAP: u32 = 6;
pub const BOOT_TAG_TYPE_FRAMEBUFFER: u32 = 8;
// メモリマップエントリ
pub const MmapEntry = extern struct {
base_addr: u64,
length: u64,
type: u32,
reserved: u32,
};
pub const MMAP_TYPE_AVAILABLE: u32 = 1;
pub const MMAP_TYPE_RESERVED: u32 = 2;
pub const MMAP_TYPE_ACPI_RECLAIMABLE: u32 = 3;
// フレームバッファ情報
pub const FramebufferInfo = extern struct {
tag: Tag,
addr: u64,
pitch: u32,
width: u32,
height: u32,
bpp: u8,
fb_type: u8,
reserved: u16,
};
// boot.zig
fn parseMultibootInfo(info: *multiboot2.BootInfo) void {
var current: [*]u8 = @ptrCast(info);
current += 8; // ヘッダをスキップ
const end = current + info.total_size;
while (@intFromPtr(current) < @intFromPtr(end)) {
const tag: *multiboot2.Tag = @ptrCast(@alignCast(current));
if (tag.type == multiboot2.BOOT_TAG_TYPE_END) {
break;
}
switch (tag.type) {
multiboot2.BOOT_TAG_TYPE_CMDLINE => {
const cmdline = @as([*]const u8, @ptrCast(current + 8));
vga.print("Cmdline: ");
printCString(cmdline);
vga.print("\n");
},
multiboot2.BOOT_TAG_TYPE_BOOT_LOADER_NAME => {
const name = @as([*]const u8, @ptrCast(current + 8));
vga.print("Bootloader: ");
printCString(name);
vga.print("\n");
},
multiboot2.BOOT_TAG_TYPE_MMAP => {
vga.print("Memory Map:\n");
parseMemoryMap(tag);
},
multiboot2.BOOT_TAG_TYPE_FRAMEBUFFER => {
const fb: *multiboot2.FramebufferInfo = @ptrCast(@alignCast(current));
vga.print("Framebuffer: ");
printNumber(fb.width);
vga.print("x");
printNumber(fb.height);
vga.print("\n");
},
else => {},
}
// 次のタグへ(8バイトアラインメント)
const size = (tag.size + 7) & ~@as(u32, 7);
current += size;
}
}
fn parseMemoryMap(tag: *multiboot2.Tag) void {
const entry_size = @as(*u32, @ptrCast(@alignCast(@as([*]u8, @ptrCast(tag)) + 8))).*;
const entries_start = @as([*]u8, @ptrCast(tag)) + 16;
const entries_end = @as([*]u8, @ptrCast(tag)) + tag.size;
var current = entries_start;
while (@intFromPtr(current) < @intFromPtr(entries_end)) {
const entry: *multiboot2.MmapEntry = @ptrCast(@alignCast(current));
vga.print(" ");
printHex(entry.base_addr);
vga.print(" - ");
printHex(entry.base_addr + entry.length);
switch (entry.type) {
multiboot2.MMAP_TYPE_AVAILABLE => vga.print(" [Available]\n"),
multiboot2.MMAP_TYPE_RESERVED => vga.print(" [Reserved]\n"),
multiboot2.MMAP_TYPE_ACPI_RECLAIMABLE => vga.print(" [ACPI]\n"),
else => vga.print(" [Unknown]\n"),
}
current += entry_size;
}
}
64ビットモードへの移行
x86_64で64ビットモードを使うには、いくつかの手順が必要。
┌─────────────────────────────────────────────────────────────┐
│ 64ビットモード移行手順 │
├─────────────────────────────────────────────────────────────┤
│ 1. GDT(Global Descriptor Table)を設定 │
│ 2. ページテーブルを設定 │
│ 3. PAE(Physical Address Extension)を有効化 │
│ 4. Long Mode を有効化 │
│ 5. ページングを有効化 │
│ 6. 64ビットコードセグメントにジャンプ │
└─────────────────────────────────────────────────────────────┘
GDTの設定
// gdt.zig
const GdtEntry = packed struct {
limit_low: u16,
base_low: u16,
base_middle: u8,
access: u8,
granularity: u8,
base_high: u8,
};
const GdtPtr = packed struct {
limit: u16,
base: u64,
};
var gdt: [5]GdtEntry = undefined;
var gdt_ptr: GdtPtr = undefined;
pub fn init() void {
// Null descriptor
gdt[0] = makeEntry(0, 0, 0, 0);
// Kernel code segment (64-bit)
gdt[1] = makeEntry(0, 0xFFFFF, 0x9A, 0xAF);
// Kernel data segment
gdt[2] = makeEntry(0, 0xFFFFF, 0x92, 0xCF);
// User code segment (64-bit)
gdt[3] = makeEntry(0, 0xFFFFF, 0xFA, 0xAF);
// User data segment
gdt[4] = makeEntry(0, 0xFFFFF, 0xF2, 0xCF);
gdt_ptr = .{
.limit = @sizeOf(@TypeOf(gdt)) - 1,
.base = @intFromPtr(&gdt),
};
loadGdt();
}
fn makeEntry(base: u32, limit: u32, access: u8, granularity: u8) GdtEntry {
return .{
.limit_low = @truncate(limit & 0xFFFF),
.base_low = @truncate(base & 0xFFFF),
.base_middle = @truncate((base >> 16) & 0xFF),
.access = access,
.granularity = @truncate(((limit >> 16) & 0x0F) | (granularity & 0xF0)),
.base_high = @truncate((base >> 24) & 0xFF),
};
}
fn loadGdt() void {
asm volatile (
\\lgdt (%[gdt_ptr])
\\mov $0x10, %%ax
\\mov %%ax, %%ds
\\mov %%ax, %%es
\\mov %%ax, %%fs
\\mov %%ax, %%gs
\\mov %%ax, %%ss
:
: [gdt_ptr] "r" (&gdt_ptr),
: "ax"
);
}
ページテーブルの設定
// paging.zig
const PAGE_SIZE: usize = 4096;
const PAGE_PRESENT: u64 = 1 << 0;
const PAGE_WRITABLE: u64 = 1 << 1;
const PAGE_HUGE: u64 = 1 << 7;
// ページテーブル構造
var pml4: [512]u64 align(PAGE_SIZE) = [_]u64{0} ** 512;
var pdpt: [512]u64 align(PAGE_SIZE) = [_]u64{0} ** 512;
var pd: [512]u64 align(PAGE_SIZE) = [_]u64{0} ** 512;
pub fn init() void {
// Identity mapping(最初の1GBを2MBページでマップ)
for (0..512) |i| {
pd[i] = (i * 0x200000) | PAGE_PRESENT | PAGE_WRITABLE | PAGE_HUGE;
}
pdpt[0] = @intFromPtr(&pd) | PAGE_PRESENT | PAGE_WRITABLE;
pml4[0] = @intFromPtr(&pdpt) | PAGE_PRESENT | PAGE_WRITABLE;
// CR3にPML4のアドレスを設定
asm volatile (
\\mov %[pml4], %%cr3
:
: [pml4] "r" (@intFromPtr(&pml4)),
);
}
pub fn enablePaging() void {
// CR4.PAEを有効化
var cr4: u64 = undefined;
asm volatile ("mov %%cr4, %[cr4]"
: [cr4] "=r" (cr4),
);
cr4 |= (1 << 5); // PAE
asm volatile ("mov %[cr4], %%cr4"
:
: [cr4] "r" (cr4),
);
// EFER.LMEを有効化
const MSR_EFER: u32 = 0xC0000080;
var efer: u64 = rdmsr(MSR_EFER);
efer |= (1 << 8); // LME
wrmsr(MSR_EFER, efer);
// CR0.PGを有効化
var cr0: u64 = undefined;
asm volatile ("mov %%cr0, %[cr0]"
: [cr0] "=r" (cr0),
);
cr0 |= (1 << 31); // PG
asm volatile ("mov %[cr0], %%cr0"
:
: [cr0] "r" (cr0),
);
}
fn rdmsr(msr: u32) u64 {
var low: u32 = undefined;
var high: u32 = undefined;
asm volatile ("rdmsr"
: [low] "={eax}" (low),
[high] "={edx}" (high),
: [msr] "{ecx}" (msr),
);
return (@as(u64, high) << 32) | low;
}
fn wrmsr(msr: u32, value: u64) void {
asm volatile ("wrmsr"
:
: [msr] "{ecx}" (msr),
[low] "{eax}" (@as(u32, @truncate(value))),
[high] "{edx}" (@as(u32, @truncate(value >> 32))),
);
}
シリアルポート出力
QEMUでデバッグするにはシリアル出力が便利。
// serial.zig
const COM1: u16 = 0x3F8;
pub fn init() void {
outb(COM1 + 1, 0x00); // 割り込み無効
outb(COM1 + 3, 0x80); // DLAB有効
outb(COM1 + 0, 0x03); // ボーレート 38400
outb(COM1 + 1, 0x00);
outb(COM1 + 3, 0x03); // 8N1
outb(COM1 + 2, 0xC7); // FIFO有効
outb(COM1 + 4, 0x0B); // DTR/RTS有効
}
pub fn write(char: u8) void {
// 送信バッファが空くまで待つ
while ((inb(COM1 + 5) & 0x20) == 0) {}
outb(COM1, char);
}
pub fn print(str: []const u8) void {
for (str) |char| {
write(char);
}
}
fn outb(port: u16, value: u8) void {
asm volatile ("outb %[value], %[port]"
:
: [value] "{al}" (value),
[port] "N{dx}" (port),
);
}
fn inb(port: u16) u8 {
return asm volatile ("inb %[port], %[result]"
: [result] "={al}" (-> u8),
: [port] "N{dx}" (port),
);
}
ビルドとテスト
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .freestanding,
.abi = .none,
});
const kernel = b.addExecutable(.{
.name = "kernel.elf",
.root_source_file = b.path("src/boot.zig"),
.target = target,
.optimize = .ReleaseSafe,
});
kernel.setLinkerScript(b.path("linker.ld"));
kernel.root_module.red_zone = false;
b.installArtifact(kernel);
// ISO作成コマンド
const iso_cmd = b.addSystemCommand(&.{
"grub-mkrescue",
"-o", "os.iso",
"iso/",
});
iso_cmd.step.dependOn(b.getInstallStep());
const iso_step = b.step("iso", "Create bootable ISO");
iso_step.dependOn(&iso_cmd.step);
// QEMUで実行
const qemu_cmd = b.addSystemCommand(&.{
"qemu-system-x86_64",
"-cdrom", "os.iso",
"-serial", "stdio",
});
qemu_cmd.step.dependOn(iso_step);
const run_step = b.step("run", "Run in QEMU");
run_step.dependOn(&qemu_cmd.step);
}
GRUB設定
# iso/boot/grub/grub.cfg
set timeout=0
set default=0
menuentry "Zig OS" {
multiboot2 /boot/kernel.elf
boot
}
ビルドと実行
# ビルド
zig build
# ISO作成してQEMUで実行
zig build run
まとめ
今回学んだこと:
| 項目 | 説明 |
|---|---|
| Multiboot2 | ブートローダーとの標準インターフェース |
| GDT | セグメント記述子テーブル |
| ページング | 仮想メモリの基礎 |
| 64ビットモード | Long Modeへの移行 |
| シリアル出力 | デバッグ用出力 |
次回は割り込み処理(IDT、ISR)を実装するよ。キーボード入力や例外処理ができるようになる。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!