2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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)を実装するよ。キーボード入力や例外処理ができるようになる。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?