4
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?

令和にCでOS書く狂人の記録 Part1 - VGA出力とMultiboot

Last updated at Posted at 2025-12-12

シリーズ一覧

Part タイトル 内容
Part1 令和にCでOS書く狂人の記録 本記事
Part2 アセンブリとCの橋渡し リンカスクリプト
Part3 メモリ管理とmalloc 動的メモリ
Part4 printfを自作する フォーマット出力
Part5 Rustで書き直したくなった 移行検討

はじめに

「令和にもなってCでOS書くとか、頭おかしいんじゃない?」

うん、おかしいかもしれない。Rustがある。Zigがある。でも、Cで書きたいんだ。

なぜなら:

  • 教科書的なOS本はだいたいC(またはC++)
  • メモリの挙動が直接見える
  • 「なんでこのコード動くの?」が全部わかる
  • 何より楽しい

ということで、令和の世にCでOSを書く狂人の記録、始まります。

開発環境

- WSL2 (Ubuntu 22.04)
- GCC (gcc-multilib)
- NASM (アセンブラ)
- QEMU (エミュレータ)
- GRUB (ブートローダー)
sudo apt install gcc-multilib nasm qemu-system-x86 grub-pc-bin grub-common

プロジェクト構成

~/c-os/
├── boot/
│   └── boot.asm      # エントリポイント(アセンブリ)
├── src/
│   └── kernel.c      # カーネル本体
├── linker.ld         # リンカスクリプト
└── build.sh          # ビルドスクリプト

Multibootって何?

GRUBからカーネルを起動するための規格。カーネルの先頭に「マジックナンバー」を置いておくと、GRUBが「お、これカーネルだな」と認識してくれる。

Multiboot Header:
┌──────────────────────────────────────┐
│ Magic Number (0x1BADB002)            │
├──────────────────────────────────────┤
│ Flags                                │
├──────────────────────────────────────┤
│ Checksum (magic + flags + checksum = 0) │
└──────────────────────────────────────┘

ブートローダー(アセンブリ部分)

カーネルのエントリポイントはアセンブリで書く。Cから始めることもできるけど、スタックの初期化とかはアセンブリのほうが確実。

boot/boot.asm
; Multiboot header for GRUB
MBALIGN  equ 1 << 0            ; ページ境界アライメント
MEMINFO  equ 1 << 1            ; メモリ情報を要求
FLAGS    equ MBALIGN | MEMINFO
MAGIC    equ 0x1BADB002        ; Multibootマジックナンバー
CHECKSUM equ -(MAGIC + FLAGS)  ; チェックサム(合計が0になるように)

section .multiboot
align 4
    dd MAGIC
    dd FLAGS
    dd CHECKSUM

section .bss
align 16
stack_bottom:
    resb 16384  ; 16KB のスタック領域を確保
stack_top:

section .text
global _start
extern kernel_main

_start:
    ; スタックポインタを設定
    mov esp, stack_top
    
    ; Cのカーネルメイン関数を呼び出し
    call kernel_main
    
    ; カーネルから戻ってきたら停止
    cli
.hang:
    hlt
    jmp .hang

ポイント:

  • _start がエントリポイント(リンカスクリプトで指定)
  • スタックは .bss セクションに確保(初期化不要)
  • kernel_main を呼んだ後は hlt で停止

カーネル本体(C言語)

ここからがCの世界!

src/kernel.c
// kernel.c - 令和に書くCのカーネル

// VGAテキストモードのアドレス
// 0xB8000 はVGAテキストバッファの物理アドレス
volatile unsigned short* vga_buffer = (unsigned short*)0xB8000;
int cursor_x = 0;
int cursor_y = 0;

// VGA色定義
enum vga_color {
    VGA_BLACK = 0,
    VGA_BLUE = 1,
    VGA_GREEN = 2,
    VGA_CYAN = 3,
    VGA_RED = 4,
    VGA_MAGENTA = 5,
    VGA_BROWN = 6,
    VGA_LIGHT_GREY = 7,
    VGA_DARK_GREY = 8,
    VGA_LIGHT_BLUE = 9,
    VGA_LIGHT_GREEN = 10,
    VGA_LIGHT_CYAN = 11,
    VGA_LIGHT_RED = 12,
    VGA_LIGHT_MAGENTA = 13,
    VGA_YELLOW = 14,
    VGA_WHITE = 15,
};

VGAテキストモードの仕組み

VGAテキストモードでは、画面上の各文字は2バイトで表現される:

1文字 = 2バイト
┌────────────────┬────────────────┐
│ 属性 (1byte)   │ 文字 (1byte)   │
├────────────────┼────────────────┤
│ BBBB FFFF      │ ASCII code     │
└────────────────┴────────────────┘
  背景色  前景色

画面サイズ: 80列 × 25行 = 2000文字 = 4000バイト

画面クリアと文字出力

// 画面クリア
void clear_screen(void) {
    for (int i = 0; i < 80 * 25; i++) {
        // 背景=黒、前景=白、文字=スペース
        vga_buffer[i] = (VGA_BLACK << 12) | (VGA_WHITE << 8) | ' ';
    }
    cursor_x = 0;
    cursor_y = 0;
}

// 1文字出力
void putchar(char c) {
    if (c == '\n') {
        cursor_x = 0;
        cursor_y++;
    } else {
        int index = cursor_y * 80 + cursor_x;
        vga_buffer[index] = (VGA_BLACK << 12) | (VGA_LIGHT_GREEN << 8) | c;
        cursor_x++;
        if (cursor_x >= 80) {
            cursor_x = 0;
            cursor_y++;
        }
    }
    
    // スクロール処理
    if (cursor_y >= 25) {
        // 1行分上にずらす
        for (int i = 0; i < 80 * 24; i++) {
            vga_buffer[i] = vga_buffer[i + 80];
        }
        // 最終行をクリア
        for (int i = 80 * 24; i < 80 * 25; i++) {
            vga_buffer[i] = (VGA_BLACK << 12) | (VGA_WHITE << 8) | ' ';
        }
        cursor_y = 24;
    }
}

// 文字列出力
void print(const char* str) {
    while (*str) {
        putchar(*str++);
    }
}

カーネルメイン

// カーネルメイン
void kernel_main(void) {
    clear_screen();
    
    print("=====================================\n");
    print("  C-OS: Written in C, Running Wild\n");
    print("  Version 0.1.0\n");
    print("=====================================\n");
    print("\n");
    print("Hello from the kernel!\n");
    print("This OS is written in pure C.\n");
    print("\n");
    print("Features:\n");
    print("  - VGA text mode output\n");
    print("  - Multiboot compliant\n");
    print("  - Running on bare metal\n");
    print("\n");
    print("Kernel initialized successfully.\n");
    
    // 無限ループ
    while (1) {
        __asm__ volatile ("hlt");
    }
}

リンカスクリプト

アセンブリとCのオブジェクトファイルを結合するためのルール:

linker.ld
ENTRY(_start)

SECTIONS
{
    /* カーネルは1MBから配置(下位メモリはBIOSが使用) */
    . = 1M;

    /* テキストセクション(コード) */
    .text BLOCK(4K) : ALIGN(4K)
    {
        *(.multiboot)   /* Multibootヘッダを先頭に */
        *(.text)
    }

    /* 読み取り専用データ */
    .rodata BLOCK(4K) : ALIGN(4K)
    {
        *(.rodata)
    }

    /* 初期化済みデータ */
    .data BLOCK(4K) : ALIGN(4K)
    {
        *(.data)
    }

    /* 未初期化データ(BSS) */
    .bss BLOCK(4K) : ALIGN(4K)
    {
        *(COMMON)
        *(.bss)
    }
}

ポイント:

  • . = 1M でカーネルを1MBの位置に配置
  • .multiboot を先頭に(GRUBが認識できるように)
  • 4KB境界でアライメント(ページングのため)

ビルドスクリプト

build.sh
#!/bin/bash
set -e

echo "=== Building C-OS ==="

# アセンブル
echo "[1/4] Assembling boot.asm..."
nasm -f elf32 boot/boot.asm -o boot/boot.o

# コンパイル
echo "[2/4] Compiling kernel.c..."
gcc -m32 -ffreestanding -fno-pie -c src/kernel.c -o src/kernel.o -Wall -Wextra

# リンク
echo "[3/4] Linking..."
ld -m elf_i386 -T linker.ld -o c-os.bin boot/boot.o src/kernel.o

# 確認
echo "[4/4] Verifying multiboot..."
if grub-file --is-x86-multiboot c-os.bin; then
    echo "Multiboot confirmed!"
else
    echo "ERROR: Not multiboot compliant"
    exit 1
fi

echo ""
echo "=== Build successful! ==="
echo "Run with: qemu-system-i386 -kernel c-os.bin"

GCCのオプション解説:

  • -m32: 32ビットコード生成
  • -ffreestanding: 標準ライブラリなし
  • -fno-pie: 位置独立実行ファイルを無効化
  • -Wall -Wextra: 警告を厳しく

ビルドと実行

$ cd ~/c-os
$ chmod +x build.sh
$ ./build.sh
=== Building C-OS ===
[1/4] Assembling boot.asm...
[2/4] Compiling kernel.c...
[3/4] Linking...
[4/4] Verifying multiboot...
Multiboot confirmed!

=== Build successful! ===
Run with: qemu-system-i386 -kernel c-os.bin

QEMUで実行:

$ qemu-system-i386 -kernel c-os.bin

画面にこんな感じで表示される:

=====================================
  C-OS: Written in C, Running Wild
  Version 0.1.0
=====================================

Hello from the kernel!
This OS is written in pure C.

Features:
  - VGA text mode output
  - Multiboot compliant
  - Running on bare metal

Kernel initialized successfully.

動いた! 緑色の文字で表示されてるはず。

なぜCなのか(改めて)

正直、Rustで書いたほうが安全だし、最近の自作OS界隈はRust派が多い。でも:

  1. デバッグがわかりやすい - ポインタの中身が直接見える
  2. リソースが多い - 古い本も参考にできる
  3. 「なぜ動くか」がわかる - 抽象化が少ない
  4. 達成感 - 危険と隣り合わせで動かす緊張感

あと、Cで一度作っておくと、Rustに移植するときに「あ、Rustのこの機能はCのこれを安全にしたやつか」ってわかるようになる。

まとめ

Part1では、最小限のカーネルを作った:

  • Multibootヘッダ - GRUBから起動できるように
  • ブートストラップ - スタック初期化してCに制御を渡す
  • VGA出力 - 画面に文字を表示

次回Part2では、リンカスクリプトをもっと詳しく見て、アセンブリとCの連携を深掘りします。


次回: アセンブリとCの橋渡し、リンカスクリプト編 Part2

4
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
4
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?