Closed22

【失敗】100日チャレンジ day42(自作OS)

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/785b7be81f8eac


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

  • 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
  • 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
  • 生成aiをつかったバイブコーティングになれる
  • 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)

できたもの

TBD

riddle_tecriddle_tec

いや M1 mac 上の qemu で動くもので、ブートローダは limine
イメージはシンプルな vx6 を使いたい

やりたいこと

  • os をつくり qemu 上で起動できること
  • スケジューラーがありプロセスが起動できること
  • 独自シェル上でコマンドが実行できること
  • ls とか echo ができること
  • 仮想メモリに対応していること

こんにちは!C言語での自作OS開発、しかも仮想メモリ、シェル、スケジューラまでとは、非常に野心的で素晴らしいプロジェクトですね!M1 Mac上のQEMUでLimineブートローダーを使い、xv6を参考にするという具体的な構想もよく練られていると感じます。

このプロジェクトは100日チャレンジの1日分としては非常に大規模ですので、段階的に進めていく形になるかと思います。まずは、ご指定の技術スタックでOSの骨組みを作り、QEMU上で最小限のカーネルを起動するところから始めましょう。

プロジェクトの仕様決定について

最初にいくつか確認させてください。

  1. 今回のOS開発プロジェクトは、100日チャレンジの基本的なルール(Next.js/TypeScriptフレームワーク、SQLiteデータベースなど)からは大きく逸脱しますが、C言語でのOS開発を優先して進めてよろしいでしょうか?(templateディレクトリのコピーとpackage.jsonname変更、README.mdの更新といった基本的なプロジェクト管理ルールは踏襲します)
  2. 「シンプルなxv6をベース」というのは、具体的にどのxv6のバージョンやリポジトリ(例: MITのxv6-publicxv6-riscvなど)を参照されるご予定ですか?それとも、xv6の設計思想や一部のモジュール(例えばファイルシステムやスケジューラのコンセプト)を参考にしつつ、主要部分はスクラッチに近い形でC言語で開発を進めるイメージでしょうか?
  3. C言語でのOS開発に必要な環境(x86_64-elf-gccなどのクロスコンパイラ、makeqemu-system-x86_64xorriso、Limine関連ツールなど)は既にお手元のM1 Macにセットアップ済みでしょうか? もし未整備の場合、どの部分から導入のサポートが必要か教えていただけますか?

本日の目標と作業手順案 (Day 42 と仮定)

上記ご確認いただいた後、本日は以下の目標で進めたいと考えています。

目標: Limineブートローダーを使い、QEMU上でごく簡単なC言語カーネル(画面に "Hello from Day42 OS!" と表示する程度)を起動させる。

作業手順案:

  1. プロジェクト初期化:

    • template ディレクトリをコピーして day42_custom_os ディレクトリを作成します。
    • day42_custom_os/package.jsonname フィールドを day42_custom_os に変更し、Next.js特有の依存関係やスクリプトは削除またはコメントアウトします。
    • day42_custom_os/README.md を今回のOS開発プロジェクトの概要、目標(最終目標と本日の目標)、開発環境について記載する形に全面的に書き換えます。
    • Next.js関連の不要なファイルやディレクトリ(app/, components/, public/, next.config.mjs, tsconfig.jsonなど)を day42_custom_os/ 内から削除します。
  2. Cプロジェクト用ディレクトリ構造作成:

    • day42_custom_os/ 直下に以下のディレクトリ・ファイルを作成します。
      • kernel/: カーネルのソースコードを格納。
        • kernel/entry.c: カーネルのエントリーポイントとなるCファイル。
        • kernel/linker.ld: カーネルをリンクするためのリンカスクリプト。
      • iso_root/: ISOイメージに含めるファイルを配置するディレクトリ。
      • limine.cfg: Limineブートローダーの設定ファイル(iso_root/ またはプロジェクトルートに配置)。
      • Makefile: プロジェクトのビルドスクリプト。
  3. Limineブートローダーの準備:

    • Limineの最新リリースからバイナリ (limine.sys, limine-cd.bin, limine-eltorito-efi.bin など) をダウンロードし、プロジェクト内に配置します (例: day42_custom_os/boot/iso_root/boot/)。Makefileから参照できるようにします。
      • (もしよろしければ、こちらでダウンロードと配置を行うコマンドを提案します)
  4. 最小限のカーネル作成:

    • kernel/entry.c: Limineプロトコルに沿ったエントリー関数を定義し、コンソールに文字列を出力する処理を記述します。(Limineが提供するターミナル書き出し機能を利用)
    • kernel/linker.ld: カーネルを適切なメモリ配置でリンクするための基本的な設定を記述します。
  5. Makefile作成:

    • クロスコンパイラ (x86_64-elf-gcc) を使って kernel/entry.c をコンパイルし、kernel/linker.ld を使ってリンクし、カーネル実行ファイル (kernel.elfなど) を生成します。
    • xorrisogrub-mkrescue (Limineの場合は xorriso が一般的) を使って、Limineブートローダーとカーネル実行ファイルを含むブート可能なISOイメージ (day42_custom_os.iso) を作成します。
    • QEMU (qemu-system-x86_64) で作成したISOイメージを起動する run ターゲットも定義します。
  6. Limine設定ファイル作成 (limine.cfg):

    • 作成したカーネル実行ファイルを指定して起動するための最小限の設定を記述します。
  7. ビルドとQEMUでの起動テスト:

    • make でビルドし、make run でQEMUを起動。コンソールに "Hello from Day42 OS!" のようなメッセージが表示されることを確認します。
  8. コミット:

    • 作業内容をコミットします (例: git commit -m "day42: step 1/X Initialize project and boot minimal kernel with Limine")。

今後の大まかなステップの展望 (数日~数週間かけて):

本日の目標が達成できたら、以下のような機能を段階的に実装していくことになるかと思います。

  • 基本的なコンソールI/Oの実装 (スクロール、カーソル制御など)
  • CPU例外と割り込み処理のためのGDT (Global Descriptor Table) とIDT (Interrupt Descriptor Table) のセットアップ
  • PIC (Programmable Interrupt Controller) またはAPIC (Advanced PIC) の設定と、タイマー割り込みやキーボード割り込みのハンドリング
  • 物理メモリ管理 (ページフレームアロケータ)
  • 仮想メモリ管理 (ページングの有効化、ページディレクトリとページテーブルの構造定義と管理、TLBの考慮)
  • カーネルモードのヒープメモリ管理 (kmalloc/kfree)
  • スケジューラの設計と実装 (プロセスの概念、タスク構造体、コンテキストスイッチ)
  • シンプルなファイルシステム (例えばinitramfsから読み込む、またはxv6のシンプルなファイルシステム構造を参考に実装)
  • ユーザーモードへの移行とシステムコールインターフェースの設計
  • 独自シェルの実装 (コマンドパーサー、echols といった基本的な組み込みコマンドの実装)

まずは上記の確認事項について、ご返信いただけますでしょうか?特に開発環境の状況と、xv6の参照範囲について教えていただけると、より具体的なサポートが可能です。

この壮大なプロジェクト、一緒に少しずつ形にしていきましょう!

riddle_tecriddle_tec

o3 に相談

この順序で提案してもらったのですが、自分にとっても結構大きなチャレンジなので徐々に拡張していく方式でやりたいです。かつ途中で失敗しないように達成しやすい(複雑度が急にあがらないようにしたい、手戻りが増えないようにしたい)と思っています。

作業計画を立てていただけないでしょうか?具体的な手順というよりは、大項目・中項目レベル、そこで気をつけることなどをまとめて欲しいです


自作OS開発サポートプロンプト

あなたの役割

  • 私(ユーザー)は C 言語で x86‑64 向けの小型 OS をゼロから実装しています
    • Limine ブートローダを使用
    • QEMU(M1 Mac 上)で実行
    • xv6 の思想を参考にしますが、コードは極力スクラッチで書きます
  • あなた(LLM)は 「毎ステップ必ずブートして動くものを残す」 を最優先に、手戻りが少ない拡張順序・注意点を提示してください
  • 具体的なコード断片は必要に応じて出して構いませんが、大項目→中項目→チェックポイント の粒度で整理し、複雑度が急上昇しないよう助言してください
  • 出力フォーマットは以下のテンプレに従ってください

テンプレ

1. フェーズ名

  • 目的:
  • 中項目
  • チェックポイント

2. フェーズ名

(以下同様)

共通ベストプラクティス

進め方のコツ


フェーズ構成と要点

1. 準備と安全網

  • 目的: クロス開発環境とデバッグ基盤を固め、「Hello, bare metal」を画面に出す
  • 中項目
    1. クロスコンパイラ (x86_64-elf-gcc)・QEMU・Limine の導入
    2. 最小 Makefile(make run で QEMU 起動)
    3. gdb / シリアル or VGA ログの配線
  • チェックポイント
    • printf 相当で文字列が表示される
    • gdb でシンボルが見える

2. カーネルの骨格

  • 目的: 例外・割り込みが発火しても死なない最小カーネル
  • 中項目
    1. C エントリとスタック初期化
    2. コンソール I/O(スクロール・カーソル制御)
    3. GDT / IDT 設定と CPU 例外ハンドラ
    4. PIT or APIC タイマ割り込み → 秒カウンタ表示
  • チェックポイント
    • 任意の例外で番号が画面に出る
    • タイマ割り込みでカウンタが進む

3. メモリ管理

  • 目的: ページングを有効化し、物理・仮想メモリを制御可能にする
  • 中項目
    1. E820 解析と物理メモリビットマップ
    2. 4 階層ページテーブルを用いたカーネル高半アドレスマップ
    3. kmalloc / kfree(ガード領域つき簡易ヒープ)
  • チェックポイント
    • 1 ページ allocate → free の単体テストがパス

4. タスクとスケジューラ

  • 目的: 最小の preemptive マルチタスクを実装
  • 中項目
    1. タスク構造体(汎用レジスタ保存領域・ページテーブル)
    2. 割り込み駆動のコンテキストスイッチ
    3. Round‑Robin スケジューリング(後でアルゴリズム差替え可能に設計)
  • チェックポイント
    • タイマ割込みごとにタスク ID が切り替わるログ

5. ユーザーモードとシステムコール

  • 目的: リング 3 に遷移し、ユーザプロセスを実行
  • 中項目
    1. syscall / sysret 導入(または int 0x80
    2. write, read, exit, fork など最小セット
    3. ユーザ空間用 C ランタイム(newlib 部分摘出)
  • チェックポイント
    • ユーザランドから write で文字列出力が成功

6. ファイルシステム & VFS

  • 目的: 読み取り専用 initramfs 経由でファイルアクセス
  • 中項目
    1. VFS 抽象 (inode / dentry)
    2. シンプル FS (xv6-style か FAT12) 読み取り実装
  • チェックポイント
    • cat /hello.txt が期待どおり出力

7. シェルとユーザーランド拡充

  • 目的: 独自シェルと基本ユーティリティで対話操作
  • 中項目
    1. 行編集・トークナイズ・内蔵コマンド
    2. exec で elf バイナリを起動
  • チェックポイント
    • ls, echo, sleep が動作

8. 仕上げ & 拡張

  • 目的: 保護機構・性能・デバイス対応を強化し安定版へ
  • 中項目
    1. Copy‑on‑Write と ELF ローダ最適化
    2. スケジューラの優先度化・統計ログ
    3. ブロック (AHCI/virtio‑blk) やネットワーク (virtio‑net) ドライバ
  • チェックポイント
    • ブート〜シェル起動が 100 % 再現

共通ベストプラクティス

  • “壊れにくい小枝”戦略:動くコードを 壊さず拡張
  • フェーズ終端ごとに ISO と期待出力を CI でテスト
  • 早期から serial_log(level, fmt, …) を整備
  • xv6 や Limine を 読む日 を設け、差分で理解
  • ブートサイズとビルド時間を常に計測・記録

進め方のコツ

  • 幅より深さ:1機能を完遂してから次へ
  • 理論集中日を設けて図・メモを書く
  • 失敗実装は branch に隔離しメインをクリーンに
  • 毎日 “動く・テスト緑・学び1つ記録” を守る
riddle_tecriddle_tec

はい、承知いたしました。「自作OS開発サポートプロンプト」のフェーズ1「準備と安全網」について、具体的な計画を提示します。


1. フェーズ名: 準備と安全網

  • 目的: クロス開発環境とデバッグ基盤を固め、「Hello, bare metal」を画面に出す

  • 中項目

    1. クロスコンパイラ (x86_64-elf-gcc)・QEMU・Limine の導入
      • 各ツールのインストール手順を確認し、実行します。
      • Limine プロトコルに準拠した最小限のブートローダー設定ファイル (limine.cfg) を作成します。
      • 簡単なアセンブリコード (boot.s) でエントリーポイントを定義し、無限ループさせます。
    2. 最小 Makefile(make run で QEMU 起動)
      • boot.s をアセンブルし、オブジェクトファイル (boot.o) を生成するルールを定義します。
      • boot.o と Limine をリンクして、ブート可能なカーネルイメージ (kernel.elf) を生成するルールを定義します。
      • kernel.elflimine.cfg を含む ISO イメージ (os.iso) を作成するルールを定義します。(例: xorrisogrub-mkrescue を利用)
      • make runos.iso を QEMU で起動するルールを定義します。QEMU の起動オプションには、デバッグ用のポート (例: -gdb tcp::1234 -S) やシリアルポートの設定を含めます。
    3. gdb / シリアル or VGA ログの配線
      • シリアルログ:
        • QEMU の起動オプションでシリアルポートをホストの標準出力やファイルにリダイレクトするように設定します (例: -serial stdio または -serial file:serial.log)。
        • カーネル側でシリアルポートを初期化し、文字を送信する簡単な関数をアセンブリまたはC言語で実装します。
        • 「Hello, bare metal via serial!」のような文字列をシリアルポートに出力します。
      • VGA テキストモードログ (オプション、より直接的):
        • VGA テキストバッファ (アドレス 0xB8000) に直接文字を書き込む簡単な関数を実装します。
        • 「Hello, bare metal on VGA!」のような文字列を画面に表示します。
      • GDB:
        • make run で QEMU が起動し、-S オプションにより CPU が停止していることを確認します。
        • 別のターミナルから x86_64-elf-gdb kernel.elf を起動し、target remote :1234 で QEMU に接続します。
        • b _start (エントリーポイントのラベル) でブレークポイントを設定し、c (continue) で実行を再開し、ブレークポイントで停止することを確認します。
        • レジスタの値やメモリの内容が読めることを確認します。
  • チェックポイント

    • QEMU の画面またはシリアルログに「Hello, bare metal!」 (または同等のメッセージ) が表示される。
    • x86_64-elf-gdbkernel.elf のシンボルを認識し、エントリーポイントでブレークポイントを設定・停止できる。

共通ベストプラクティス

  • 開発環境のセットアップ手順を詳細に記録し、再現性を確保する。
  • Limine のドキュメントを熟読し、ブートプロトコルの基本を理解する。
  • Makefile の各ターゲットが期待通りに動作することを個別に確認する。
  • エラーメッセージや予期せぬ動作に遭遇した場合、原因を特定するための最小限の変更を試す。

進め方のコツ

  • 最初はアセンブリ言語でごく小さなコード片を書き、QEMU での起動と Limine によるロードを確認することから始める。
  • シリアル出力は、画面表示よりも早い段階で導入できるデバッグ手段として非常に有効。
  • GDB の接続と基本的な操作 (ブレークポイント、ステップ実行、レジスタ確認) に慣れておく。

ご不明な点や、さらに詳細な情報が必要な場合はお気軽にお尋ねください。

riddle_tecriddle_tec

GDBの接続とブレークポイントの設定、continue までは成功していますね。素晴らしいです!

Breakpoint 1 at 0x1000b0: file src/kernel.c, line 13. と表示されていることから、kmain 関数の先頭(src/kernel.c の13行目、__asm__ __volatile__ ( "outb %0, %1" : : "a"(val), "Nd"(port) ); の行)にブレークポイントが設定されました。

そして、(gdb) c (continue) を実行した後、QEMU上で実行されているOSがそのブレークポイントに到達するのをGDBが待っている状態です。

もし、c を実行してもGDBのプロンプトに戻ってこない(=ブレークポイントで止まらない)場合、以下の可能性が考えられます。

  1. カーネルが kmain に到達していない:
    • _start (in boot.s) から call kmain に至るどこかで問題が発生している(例: スタック設定ミス、Limineプロトコル関連の問題)。
    • Limineがカーネルをロードできていない。
  2. ブレークポイントのアドレスが間違っている:
    • コンパイラやリンカの最適化により、ソースコードの行番号と実際の命令アドレスがずれている(ただし、今回は -O2 -g なので、通常は問題なく対応するはずです)。
  3. QEMUがフリーズまたはクラッシュしている:
    • カーネルコードのバグにより、ブレークポイントに到達する前にCPUが例外を発生させて停止したり、無限ループに陥っている。

現状のGDBは何をしていますか?

  • GDBはQEMUに接続し、kmain 関数の開始アドレス (0x1000b0) にブレークポイントを設定しました。
  • c (continue) コマンドにより、QEMU内のCPUに実行再開を指示しました。
  • 現在、CPUがブレークポイントのアドレス (0x1000b0) の命令を実行しようとすると、QEMUがそれを検知してCPUを停止させ、GDBに制御を戻すのを待機しています。

もしGDBがプロンプトを返さず、QEMUのウィンドウも反応がないように見える場合、QEMU側で何か問題が起きている可能性が高いです。

ユーザー様がGDBの画面でCtrl+Cを押すと、GDBはQEMUに割り込みをかけ、現在の実行位置で停止させることができます。
停止させた後、bt (バックトレース) コマンドを実行すると、どこで実行が止まっているかの手がかりが得られるかもしれません。


make debug が失敗した件について
先ほどの make debug の失敗ログ(make: *** No rule to make target 'debug'. Stop.)は、やはりカレントディレクトリがプロジェクトルート (100day_challenge_backend) であったために Makefile が見つからなかったことが原因です。
私が killall qemu-system-x86_64 || true && make debug を実行する際に、cd day42_os_dev_support を含めるべきでした。重ねて申し訳ありません。

現在のGDBのセッションは、QEMUが以前に make run で正常に起動した(そしてCtrl+Cで終了させられた)セッションとは別の、新たに行った make debug で起動したQEMU(ただし、これは失敗している)に接続しようとしている可能性があります。

一度、現在のGDBとQEMUを整理しましょう。

  1. GDBの終了: GDBのプロンプトで quit と入力してGDBを終了してください。
  2. QEMUの完全終了: 念のため、killall qemu-system-x86_64 を実行してください(エラーが出ても問題ありません)。

その後、私が正しい手順でQEMUをデバッグモードで起動します。
準備ができたらお知らせください。

riddle_tecriddle_tec
(gdb) target remote :1234
Remote debugging using :1234
0x000000000000fff0 in ?? ()
(gdb) info registers rip
rip            0xfff0              0xfff0
(gdb) x/10i $pc
=> 0xfff0:      add    %al,(%rax)
   0xfff2:      add    %al,(%rax)
   0xfff4:      add    %al,(%rax)
   0xfff6:      add    %al,(%rax)
   0xfff8:      add    %al,(%rax)
   0xfffa:      add    %al,(%rax)
   0xfffc:      add    %al,(%rax)
   0xfffe:      add    %al,(%rax)
   0x10000:     add    %al,(%rax)
   0x10002:     add    %al,(%rax)
(gdb) 
   0x10004:     add    %al,(%rax)
   0x10006:     add    %al,(%rax)
   0x10008:     add    %al,(%rax)
   0x1000a:     add    %al,(%rax)
   0x1000c:     add    %al,(%rax)
   0x1000e:     add    %al,(%rax)
   0x10010:     add    %al,(%rax)
   0x10012:     add    %al,(%rax)
   0x10014:     add    %al,(%rax)
   0x10016:     add    %al,(%rax)
(gdb) si
0x000000000000e05b in ?? ()
(gdb) info registers rip
rip            0xe05b              0xe05b
(gdb) si
0x000000000000e062 in ?? ()
(gdb) info registers rip
rip            0xe062              0xe062
(gdb) si
0x000000000000e066 in ?? ()
(gdb) info registers rip
rip            0xe066              0xe066
(gdb) si
0x000000000000e068 in ?? ()
(gdb) info registers rip
rip            0xe068              0xe068
(gdb) si
0x000000000000e06a in ?? ()
(gdb) info registers rip
rip            0xe06a              0xe06a
(gdb) info registers rax
rax            0x0                 0
(gdb) 
rax            0x0                 0
(gdb) x/i $pc
=> 0xe06a:      add    %al,(%rax)

謎のコマンドうってる

riddle_tecriddle_tec

0. 全体像

フェーズ 到達目標 主要コンポーネント 参考章(MikanOS)
Phase 1 “Hello, kernel!” が QEMU で起動 UEFI ブートローダ, カーネルの main Day 01–03
Phase 2 VGA/Framebuffer 文字表示 & 例外ハンドラ コンソール, IDT, PIC/IO-APIC Day 04–07
Phase 3 物理メモリ管理 & ページング開始 PMM, VMM, PML4 設定 Day 08–12
Phase 4 タイマ割り込み & ラウンドロビン・スケジューラ PIT/APIC, TSS, context switch Day 13–17
Phase 5 ELF 形式のユーザプロセス起動 Syscall, ユーザ空間, fork/exec Day 18–22
Phase 6 ミニシェル + echo / ls 仮想ファイルシステム, デバイス層 Day 23–28

ゴール
Phase 6 までを 8〜12 週で完走すると、質問に挙げられた機能セットが一通りそろいます。


1. ホスト環境(M1 Mac)構築

1.1. パッケージ管理

brew install llvm qemu gnu-sed coreutils cdrtools

1.2. クロスツールチェーン (x86_64-elf)

  • Homebrew 公式フォーミュラではなく osdev-binutilsosdev-gcc を使うと衝突しにくい。
  • MikanOS と同じく Intel 64(x86_64) をターゲットにするのが学習コスト・情報量ともに有利。
    • aarch64 ネイティブで書きたい場合は Limine + UEFI の手順が大幅に変わるので要相談。

1.3. Python / Ninja (ビルドスクリプト用)


2. ブートチェーン

レイヤ 採用候補 理由
FW UEFI (OVMF.fd for x86_64) Apple Silicon + QEMU でも 安定/BIOS不要
ブートローダ - MikanLoaderPkg (そのまま流用)<br>- Limine (最新 UEFI 対応) 前者は教科書どおりに追従しやすい/後者は Starters が多い
カーネル C (一部 asm) ご希望に合わせ C に統一(MikanOS は C++)

MikanOS のブートローダ部は UEFI アプリとして実装されており、Apple Silicon + QEMU の組み合わせでも動作確認例が多い。


3. 推奨ツール & ライブラリ

目的 ツール メモ
GCC/Clang llvm (Clang + lld) LLD はリンクスクリプト不要で --lto が楽
アセンブラ nasm または gnu as UEFI スタブ程度なら最小で済む
デバッグ gdb-multiarc, lldb QEMU -s -S でリモート接続
イメージ生成 xorriso, mtools, objcopy fat12.img or esp.qcow2
CI GitHub Actions + brew M1 ローカルとの差分を最小化
ドキュメント mkdocs or mdBook 日々のメモを HTML 化

4. フェーズ別タスク詳細

Phase 1 — “Hello, kernel!”

[UEFI] → MikanLoaderPkg → Kernel C entry

  • UEFI アプリ (EFIAPI efi_main) で LoadKernel.efi を読み込み
  • GDT + Long mode 切り替え
  • QEMU 起動コマンド例

qemu-system-x86_64 \
  -machine q35,accel=hvf \
  -cpu host \
  -m 512M \
  -drive if=pflash,format=raw,unit=0,file=OVMF_CODE.fd,readonly=on \
  -drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \
  -drive format=raw,file=fat:rw:./hddimg

Phase 2 — 画面出力 & 例外

  • UEFI から framebuffer 情報を受け取り、簡易 printf 実装
  • IDT を設定して #DE/#GP などを自前でハンドリング
  • 構造体:struct FrameBuffer, struct IDTR, struct InterruptFrame

Phase 3 — 物理/仮想メモリ

  • UEFI Memory Map → ビットマップ PMM (alloc_page / free_page)
  • 4 level page table を動的生成、カーネルを Higher Half (0xFFFF8000_00000000) に再配置
  • CR3 切替で仮想メモリ有効化 → fork( ) への布石

Phase 4 — スケジューラ

  • APIC タイマ (x2APIC) 100 Hz 割り込み
  • struct Task { regs_t regs; uint64_t *stack; ... }
  • 単純 ラウンドロビン で task_switch()
  • sleep() はタイマティック方式で実装

Phase 5 — ユーザプロセス & Syscall

  • ELF64 loader: .text/.data をユーザ空間 (例 0x00000000400000) にマッピング
  • int 0x80 風に sys_write, sys_exit など実装
  • init プロセスから /bin/shfork+exec

Phase 6 — シェル & コマンド

  • ミニシェル (char *readline(), parse(argv), exec)
  • 仮想 FS 層
  • RAMFS を root に
  • /dev/console, /proc/meminfo などはデバイスファイル
  • ls: RAMFS のディレクトリエントリ列挙
  • echo: write(1, argv[1], ...)

5. QEMU (Apple Silicon) Tips

課題 対応策
HVF は x86_64 ゲストでしか使えず若干遅い -accel hvf を有効にするだけで JIT 化され 3–5 倍高速
VGA 出力が崩れる OVMF の解像度を固定 (-display cocoa,gl=off)
GDB でレジスタ名が違う target remote :1234 後に set architecture i386:x86-64

6. 参考リソース

  • Deep Wiki / MikanOS — 日本語で UEFI-OS 開発を解説。図解が豊富。
  • MikanOS GitHub — 完全な C++ ソースとブートチェーン。ビルドスクリプトも公開。
  • Limine Bootloader — 最新 UEFI/BIOS 両対応の汎用ブートローダ。
  • osdev.org Wiki — ページング・割り込み・APIC の実装例が豊富。

7. 次のアクション

  1. ターゲットアーキテクチャ確認
    • 本計画は x86_64 ゲスト を前提にしています。
    • aarch64 ネイティブ で行いたい場合はブートローダから変更が必要です。
  2. フェーズ 1 の環境セットアップ
    • Homebrew でクロスツールチェーンを導入し、hello_kernel.efi が起動するまでをゴールに。
  3. 疑問点・追加要望があれば教えてください!
    • 例: 「C++ でも良い?」「シェルにパイプ機能を入れたい」など

🎉 Happy hacking!

riddle_tecriddle_tec

Day42 OS開発手順と用語解説

1. はじめに

Day42では、x86-64アーキテクチャ向けのシンプルなOSカーネルを作成し、QEMUエミュレータ上で起動して画面に文字を表示することを目標としました。
このドキュメントは、その過程で行った具体的な手順と、関連する主要な技術用語について解説し、理解を深めることを目的としています。

2. 主要な用語解説

  • OS (Operating System / オペレーティングシステム):
    コンピュータのハードウェア(CPU、メモリ、ディスクなど)とソフトウェアリソースを効率的に管理し、ユーザーやアプリケーションに対して統一的なインターフェースを提供する基本的なソフトウェアです。OSがなければ、アプリケーションはハードウェアを直接制御する必要があり、開発が非常に複雑になります。

  • カーネル (Kernel):
    OSの中核部分であり、最も低いレベルでハードウェアと対話します。主な役割は以下の通りです。

    • プロセス管理: プログラムの実行を管理し、CPU時間を割り当てます。
    • メモリ管理: 各プロセスが使用するメモリ空間を割り当て、保護します。
    • デバイスドライバ: ハードウェアデバイス(キーボード、ディスク、ネットワークカードなど)を制御するためのインターフェースを提供します。
    • システムコール: アプリケーションがカーネルの機能を利用するための窓口を提供します。
  • ブートローダー (Bootloader):
    コンピュータの電源が投入された後、OSを起動するための小さなプログラムです。通常、マザーボード上のファームウェア(BIOSやUEFI)によって最初に呼び出され、ハードディスクなどのストレージデバイスからOSのカーネルイメージをメモリに読み込み、カーネルの実行を開始させます。

  • Limine:
    今回使用したモダンなx86およびx86-64アーキテクチャ向けのブートローダーです。シンプルな設定ファイルに基づいてカーネルをロードし、起動時にカーネルが必要とする様々な情報(メモリマップ、フレームバッファ情報、ACPIテーブルなど)を提供してくれます。

  • BIOS (Basic Input/Output System):
    PC/AT互換機などで伝統的に使われてきたファームウェアです。マザーボード上のROMチップに格納されており、電源投入時にハードウェアの初期化(POST: Power-On Self Test)を行い、ブートデバイスからブートローダーを読み込んで実行します。

  • UEFI (Unified Extensible Firmware Interface):
    BIOSの後継として策定された、より高機能で柔軟なファームウェアインターフェースです。セキュアブート機能、大きなディスクのサポート、グラフィカルなブートメニューなどが特徴です。LimineはBIOSとUEFIの両方に対応しています。

  • フレームバッファ (Framebuffer):
    画面に表示される内容をピクセル単位で保持するメモリ領域のことです。このメモリ領域に直接ピクセルデータを書き込むことで、画面に画像や文字を描画できます。VGAテキストモードのようなハードウェアに依存した表示方法ではなく、より柔軟なグラフィック表示が可能です。Limineは初期化済みのフレームバッファ情報をカーネルに渡してくれます。

  • ピッチ (Pitch / Stride):
    フレームバッファにおいて、画面の一行分のデータが占めるバイト数を指します。通常は「画面の横幅ピクセル数 × 1ピクセルあたりのバイト数」と一致しますが、メモリのアライメント(特定のアドレス境界にデータを配置すること)のために、実際の横幅よりも大きな値になることがあります。描画位置を計算する際に重要になります。

  • リンカスクリプト (Linker Script):
    ソースコードをコンパイルして得られた複数のオブジェクトファイル(機械語コードやデータのかたまり)を、最終的な実行可能ファイル(今回の場合はカーネルイメージ kernel.elf)にどのように配置するかをリンカ(ldコマンドなど)に指示するための設定ファイルです。カーネルのメモリ上の開始アドレス(ベースアドレス)、各セクション(コードセクション .text、データセクション .data、BSSセクション .bss など)の配置順序やアライメントなどを定義します。

  • QEMU (Quick Emulator):
    様々なCPUアーキテクチャ(x86, ARM, MIPSなど)のマシン全体をエミュレートできるオープンソースのソフトウェアです。OS開発においては、作成中のOSを実機に書き込むことなく、PC上で手軽に起動テストやデバッグを行うために広く利用されます。

  • ISOイメージ (.iso file):
    CD-ROMやDVD-ROMなどの光学ディスクの内容を一つのファイルにまとめたアーカイブファイルフォーマットです。ブート可能なOSのイメージを配布したり、QEMUのようなエミュレータで仮想的な光学ドライブとしてマウントしたりするのによく使われます。今回は、Limineとカーネルを含んだブート可能なISOイメージを作成しました。

  • ビットマップフォント (Bitmap Font):
    文字の形を、ピクセルの集まり(ビットマップイメージ)として定義したフォントです。各文字は固定サイズのグリッド(例: 8x8ピクセル、16x16ピクセル)で表現されます。実装が比較的単純で、OSの初期段階や組み込みシステムなどでよく利用されます。今回は8x8ピクセルのASCII文字セットを使用しました。

  • GDT (Global Descriptor Table):
    x86アーキテクチャにおいて、プロテクトモードやロングモードでメモリセグメント(コードセグメント、データセグメントなど)の属性(ベースアドレス、リミット、アクセス権など)を定義するディスクリプタを格納するテーブルです。OSがメモリ空間を管理し保護するために不可欠です。

  • IDT (Interrupt Descriptor Table):
    x86アーキテクチャにおいて、割り込みや例外が発生した際にCPUがどの処理ルーチン(割り込みハンドラ)を呼び出すべきかを定義するディスクリプタを格納するテーブルです。ハードウェア割り込み(キーボード入力、タイマーなど)やソフトウェア例外(ゼロ除算、ページフォルトなど)に対応するために必要です。

3. 実行した手順

以下に、Day42でOSカーネルを起動し、画面に文字を表示させるまでに行った主要な手順を時系列で示します。

3.1. プロジェクト初期設定とブートローダーの導入

  1. 作業ディレクトリと関連ファイルの準備: OS開発のための基本的なディレクトリ構造 (kernel/, image/, scripts/) を作成しました。
  2. Limineブートローダーの取得: モダンなブートローダーであるLimineのv9.x-binaryリリースをクローンし、プロジェクト内に配置しました (day42_original_os/limine/)。これにより、自前で複雑なブートストラップコードを書く手間を省きます。
  3. Limine設定ファイルの作成 (image/limine.conf): Limineに対して、カーネルイメージの場所や起動時のオプションを指示する設定ファイルです。
    TIMEOUT=5
    DEFAULT_ENTRY=1
    
    :KERNEL(kernel)
    COMMENT=My Custom Kernel
    PROTOCOL=limine
    KERNEL_PATH=boot:///boot/kernel.elf
    # KERNEL_CMDLINE=Hello
    
    • TIMEOUT=5: 起動メニューの表示時間(秒)。
    • KERNEL_PATH=boot:///boot/kernel.elf: ISOイメージ内の /boot/kernel.elf をカーネルとしてロードするよう指定。
  4. Makefileの初期作成: カーネルのコンパイル、ISOイメージの作成、QEMUでの実行を自動化するための Makefile を作成しました。
    • カーネルのコンパイル: Cソースコード (kernel/main.c) を x86_64-elf-gcc (クロスコンパイラ) でコンパイルし、リンカスクリプト (scripts/linker.ld) を用いて kernel.elf (ELF形式の実行ファイル) を生成します。
    • ISOイメージの作成:
      • 必要なファイル (Limineのブートローダーファイル、設定ファイル、カーネルイメージ) を一時ディレクトリにコピー。
      • xorriso コマンドを使って、これらのファイルからブート可能なISOイメージ (image/os.iso) を生成します。El Torito規格に準拠し、BIOSブートとUEFIブートの両方に対応する設定を行いました。
      • limine bios-install image/os.iso コマンドを実行して、ISOイメージにBIOSブート用の情報を埋め込みます。
    • QEMUでの実行:
      • make run-bios: BIOSモードでQEMUを起動 (-machine pc)。
      • make run-uefi: UEFIモードでQEMUを起動 (-machine q35 -bios OVMF_CODE.fd)。

3.2. 最小限のカーネル作成 (エントリーポイントとフレームバッファ要求)

  1. カーネルソースファイル (kernel/main.c):
    • C言語で記述。OSの本体となるコードです。
    • Limineが提供するヘッダーファイル (limine.h) をインクルードします。これはLimineから情報を受け取るために必要です。
  2. エントリーポイント (_start): カーネルが最初に実行する関数です。Makefile 中のリンカ設定で、この _start シンボルをカーネルのエントリーポイントとして指定します。
  3. フレームバッファ要求:
    static volatile struct limine_framebuffer_request framebuffer_request = {
        .id = LIMINE_FRAMEBUFFER_REQUEST,
        .revision = 0
    };
    
    • Limineに対して、「フレームバッファの情報が欲しい」と要求するための構造体です。
    • .id = LIMINE_FRAMEBUFFER_REQUEST で要求の種類を指定します。
    • volatile キーワードは、コンパイラによる最適化でこの変数が消されたり、アクセスが省略されたりするのを防ぎます。
    • static とすることで、この変数のスコープをファイル内に限定します。
    • この構造体を特定のセクション (.requests) に配置することで、Limineが起動時にこれを見つけて処理します。
  4. リンカスクリプト (scripts/linker.ld):
    ENTRY(_start)
    SECTIONS
    {
        . = 0xffffffff80000000;
        _kernel_start = .;
    
        .text : ALIGN(4K) {
            *(.text .text.*)
        }
    
        .requests : ALIGN(4K) {
            *(.requests)
        }
        KEEP(*(.requests))
    
        .rodata : ALIGN(4K) {
            *(.rodata .rodata.*)
        }
    
        .data : ALIGN(4K) {
            *(.data .data.*)
        }
    
        .bss : ALIGN(4K) {
            *(.bss .bss.*)
            *(COMMON)
        }
    
        _kernel_end = .;
    }
    
    • ENTRY(_start): カーネルの実行開始点を _start 関数に指定。
    • . = 0xffffffff80000000;: カーネルがメモリ上にロードされるベースアドレス(開始番地)を指定。これはx86-64のHigher Half Kernel(カーネル空間をメモリの高位アドレスに置く設計)で一般的な値です。
    • .requests セクション: Limineへのリクエスト構造体を配置するための専用セクション。KEEP(*(.requests)) で、未使用でも削除されないようにします。
    • その他、.text (コード)、.rodata (読み取り専用データ)、.data (初期化済みデータ)、.bss (未初期化データ) といった標準的なセクションを定義。

3.3. フレームバッファへの描画 (画面クリアと単色塗りつぶし)

  1. フレームバッファ情報の取得: _start 関数内で、Limineからのレスポンス (framebuffer_request.response) を確認し、利用可能なフレームバッファ (framebuffers[0]) の情報を取得します。
    • address: フレームバッファのメモリアドレス。
    • width: 画面の幅 (ピクセル数)。
    • height: 画面の高さ (ピクセル数)。
    • pitch: 画面の一行分のバイト数。
    • bpp: 1ピクセルあたりのビット数 (Bits Per Pixel)。
  2. 画面クリア処理: 取得したフレームバッファ情報に基づき、画面全体を特定の色 (最初は青色 0x0000FF) で塗りつぶすループ処理を実装しました。
    // 簡易的な画面クリア
    uint32_t *fb_ptr = (uint32_t *)framebuffer->address;
    for (uint64_t y = 0; y < framebuffer->height; y++) {
        for (uint64_t x = 0; x < framebuffer->width; x++) {
            fb_ptr[y * (framebuffer->pitch / 4) + x] = color;
        }
    }
    
    • framebuffer->addressuint32_t (32ビット符号なし整数) のポインタにキャストしてピクセル操作を行います (32bppを想定)。
    • pitch はバイト単位なので、uint32_t の配列としてアクセスするために4で割っています。
  3. 無限ループとhlt: 画面描画後、CPUが停止しないように無限ループ (for (;;) {}) を配置し、その中で asm volatile ("hlt") を実行してCPUを低消費電力の停止状態にしました。これにより、QEMUが無駄にCPU資源を消費するのを抑えます。
  4. QEMUでの動作確認: make run-bios を実行し、QEMUの画面全体が指定した色で塗りつぶされることを確認しました。これが最初の画面出力成功です。

3.4. ビットマップフォントの導入と文字表示機能の実装

画面に意味のある情報を表示するため、文字描画機能を実装しました。

  1. ビットマップフォントの用意: シンプルな8x8ピクセルのモノクロームビットマップフォント (font8x8_basic.h) をWebで見つけ、プロジェクト (kernel/font8x8_basic.h) に追加しました。これは、ASCII文字コード0から127に対応する各文字のピクセルパターンを uint8_t の配列として定義したものです。
    // font8x8_basic.h の一部 (例: 'A')
    static const uint8_t font8x8_basic[128][8] = {
        // ...
        { 0x00, 0x18, 0x24, 0x42, 0x7E, 0x42, 0x42, 0x00},   // U+0041 'A'
        // ...
    };
    
    各行の uint8_t 値が、文字の1行分の8ピクセルパターンを表します (1が前景ピクセル、0が背景ピクセル)。
  2. put_char 関数の実装: 指定された文字を、指定された座標に、指定された前景色・背景色で描画する関数です。
    • 入力: フレームバッファ情報、文字、描画開始X座標、描画開始Y座標、前景色(fg)、背景色(bg)。
    • 処理:
      • 文字コードに対応するフォントデータを font8x8_basic 配列から取得。
      • フォントデータの各行 (8行) についてループ。
      • 各行のビットパターン (8ビット) を1ビットずつチェック。
      • ビットが1ならフレームバッファの対応するピクセルに前景色を、0なら背景色を描画。
  3. カーソル管理と put_string 関数の実装: 画面上に連続して文字列を描画するための仕組みです。
    • グローバル変数としてカーソル位置 (cursor_x, cursor_y)、文字色 (text_color)、背景色 (bg_color) を導入。
    • put_string 関数:
      • 内部で put_char を呼び出し、一文字ずつ描画。
      • 描画後、cursor_x をフォント幅分進める。
      • 特殊文字の処理:
        • \n (改行): cursor_x を0に戻し、cursor_y をフォント高さ分進める。
        • \r (復帰): cursor_x を0に戻す。
      • 画面端での折り返し: cursor_x が画面幅を超えたら改行と同様の処理。
      • 画面下端でのスクロール (簡易版): cursor_y が画面高さを超えたら、画面全体をクリアし、カーソルを左上 (0,0) に戻す。
  4. 数値表示ユーティリティ (put_hex, itoa_simple): デバッグ情報として数値(特に16進数)を表示するために、簡単な整数から文字列への変換関数と16進数表示関数を作成しました。
  5. _start 関数からの呼び出し: 画面クリア後、put_stringput_hex を使って、あいさつメッセージ、フレームバッファ情報、画面解像度などを表示するようにしました。

3.5. 文字サイズのスケーリング機能

8x8ピクセルのフォントは現代のディスプレイでは小さすぎるため、表示サイズを大きくする機能を実装しました。

  1. スケーリング係数の導入: FONT_SCALE というグローバル変数を導入し、文字の拡大率 (例: 2なら縦横2倍) を指定できるようにしました。
  2. 描画ロジックの変更 (put_char):
    • フォントデータの1ピクセルを描画する際に、フレームバッファ上では FONT_SCALE x FONT_SCALE の矩形領域を指定色で塗りつぶすように変更しました。
    • これにより、元のフォントデータの解像度を保ちつつ、見た目のサイズを大きくできます。
  3. 実効フォントサイズの定義: スケーリング後のフォントの幅と高さを #define マクロ (EFFECTIVE_FONT_WIDTH, EFFECTIVE_FONT_HEIGHT) で定義し、put_string でのカーソル移動や改行処理に利用するようにしました。

3.6. ビルドエラーとリンカエラーの修正

開発過程でいくつかのビルド関連の問題に遭遇し、修正を行いました。

  1. memcpy 未定義エラー:
    • 原因: -nostdlib オプション付きでカーネルをコンパイルしているため、標準Cライブラリ関数 (memcpy など) がリンクされません。
    • 対策: kernel/main.c 内に、自前の簡単な memcpy 関数を実装しました。
    void *memcpy(void *dest, const void *src, size_t n) {
        uint8_t *pdest = (uint8_t *)dest;
        const uint8_t *psrc = (const uint8_t *)src;
        for (size_t i = 0; i < n; i++) {
            pdest[i] = psrc[i];
        }
        return dest;
    }
    
  2. 初期化子のコンパイル時定数エラー:
    • 原因: static int EFFECTIVE_FONT_WIDTH = FONT_DATA_WIDTH * FONT_SCALE; のようなグローバル/静的変数の初期化において、FONT_SCALE が実行時まで値が確定しない変数として扱われたため、「初期化子がコンパイル時定数ではない」というエラーが発生しました。
    • 対策: EFFECTIVE_FONT_WIDTHEFFECTIVE_FONT_HEIGHTstatic int 変数から #define マクロに変更しました。マクロはプリプロセッサによって展開されるため、コンパイル時には定数値として扱われます。
    #define EFFECTIVE_FONT_WIDTH (FONT_DATA_WIDTH * FONT_SCALE)
    #define EFFECTIVE_FONT_HEIGHT (FONT_DATA_HEIGHT * FONT_SCALE)
    

3.7. Gitへのコミット

ここまでの変更をGitリポジトリにコミットしました。主なコミット対象ファイルは以下の通りです。

  • kernel/main.c (カーネル本体)
  • kernel/font8x8_basic.h (ビットマップフォント)
  • kernel/limine.h (Limineヘッダー)
  • Makefile (ビルドスクリプト)
  • image/limine.conf (Limine設定ファイル)
  • limine/ (Limineブートローダーファイル群)
  • scripts/linker.ld (リンカスクリプト)
  • PROGRESS.md (進捗管理ファイル)
  • README.md (プロジェクト説明ファイル)

コミットメッセージ: day42: feat: Implement basic console output with scalable 8x8 font

4. 現状と次のステップ

  • 現状: Limineブートローダーを経由して自作カーネルが起動し、フレームバッファを利用して画面上にスケーリング可能な文字を表示できる状態になりました。
  • 次のステップの案:
    • GDT (Global Descriptor Table) のセットアップ: プロテクトモード/ロングモードで必須のセグメントディスクリプタを設定します。
    • IDT (Interrupt Descriptor Table) のセットアップ: 割り込みハンドリングの基礎を築きます。まずは例外(例: Division by Zero)のハンドリングから始めることが多いです。
    • シリアルポート出力: QEMUのシリアルポート経由でデバッグメッセージを出力できるようにすると、画面表示よりも手軽にログを確認できます。
    • 物理メモリ管理 (Physical Memory Manager / PMM): Limineから受け取ったメモリマップ情報を基に、利用可能な物理メモリ領域を管理する機構を実装します。
    • ページング (Paging / 仮想メモリ管理): 物理メモリをページ単位で管理し、仮想アドレス空間を物理アドレス空間にマッピングする仕組みを有効にします。これによりメモリ保護や効率的なメモリ利用が可能になります。

以上がDay42の作業内容と関連技術の概要です。

riddle_tecriddle_tec

こういうのつくって管理してる(1コンテキストだととても終わらないので)

OS開発プロジェクト進捗

フェーズ別タスクリスト

Phase 1: "Hello, kernel!" が QEMU で起動

  • UEFI ブートローダ (MikanLoaderPkg または Limine) のセットアップ
  • カーネルエントリーポイント (C言語) の作成
  • GDT (Global Descriptor Table) の設定
  • Long mode への切り替え
  • QEMU で "Hello, kernel!" の表示確認

Phase 2: VGA/Framebuffer 文字表示 & 例外ハンドラ

  • UEFI から Framebuffer 情報取得
  • 簡易的な文字表示関数の実装 (printf ライク)
  • IDT (Interrupt Descriptor Table) の設定
  • 例外ハンドラの実装 (例: #DE, #GP)
  • 構造体定義: struct FrameBuffer, struct IDTR, struct InterruptFrame

Phase 3: 物理メモリ管理 & ページング開始

  • UEFI Memory Map の解析と利用 (Limine経由で取得しPMMで使用)
  • 物理メモリマネージャ (PMM) の実装 (スタック/フリーリスト方式)
  • alloc_page / free_page 関数の実装
  • 4レベルページテーブルの動的生成
    • HHDMオフセットの取得と確認
    • ページング関連構造体定義 (paging.h)
    • PML4テーブル初期化準備 (paging.c)
    • カーネル空間のマッピング (k_start to k_end)
    • フレームバッファのマッピング
  • カーネルの Higher Half (例: 0xFFFF8000_00000000) への再配置 (ページングにより実現)
  • CR3 レジスタ切り替えによる仮想メモリ有効化

Phase 4: タイマ割り込み & ラウンドロビン・スケジューラ

  • APIC タイマ (x2APIC推奨) の設定 (例: 100Hz)
  • タイマ割り込みハンドラの実装
  • タスク構造体 (struct Task) の定義 (レジスタ状態, スタックポインタ等)
  • タスク切り替え (task_switch()) 関数の実装
  • ラウンドロビンスケジューリングアルゴリズムの実装
  • sleep() 機能の実装 (タイマティック方式)

Phase 5: ELF 形式のユーザプロセス起動

  • ELF64 ローダーの実装
  • .text, .data セクションのユーザー空間へのマッピング
  • システムコールインターフェースの実装 (例: int 0x80)
  • 基本的なシステムコール (sys_write, sys_exit など) の実装
  • init プロセスの作成
  • forkexec (または同等の機能) の実装 (init から /bin/sh 起動目標)

Phase 6: ミニシェル + echo / ls

  • ミニシェルの実装
    • readline() (入力取得)
    • parse() (コマンド解析)
    • exec() (コマンド実行)
  • 仮想ファイルシステム (VFS) 層の設計と実装
  • RAMFS (RAMベースのファイルシステム) の実装とルートマウント
  • デバイスファイルの実装 (例: /dev/console, /proc/meminfo)
  • ls コマンドの実装 (RAMFS のディレクトリエントリ列挙)
  • echo コマンドの実装 (write(1, ...) を使用)

その他

  • ホスト環境構築 (クロスツールチェーン、QEMUなど)
  • ビルドスクリプトの整備
  • デバッグ環境の確立 (GDB/LLDBリモートデバッグ)
riddle_tecriddle_tec

全然すすまない。。
ずーっと画面が黒いまま

riddle_tecriddle_tec

1週間ぐらいやったけどとてもじゃないけど完成まで漕ぎ着けられないので諦めます

riddle_tecriddle_tec

0. 全体像

フェーズ 到達目標 主要コンポーネント
Phase 1 “Hello, kernel!” が QEMU で起動 UEFI ブートローダ, カーネルの main
Phase 2 VGA/Framebuffer 文字表示 & 例外ハンドラ コンソール, IDT, PIC/IO-APIC
Phase 3 物理メモリ管理 & ページング開始 PMM, VMM, PML4 設定
Phase 4 タイマ割り込み & ラウンドロビン・スケジューラ PIT/APIC, TSS, context switch
Phase 5 ELF 形式のユーザプロセス起動 Syscall, ユーザ空間, fork/exec
Phase 6 ミニシェル + echo / ls 仮想ファイルシステム, デバイス層

ゴール
Phase 6 までを 8〜12 週で完走すると、質問に挙げられた機能セットが一通りそろいます。


1. ホスト環境(M1 Mac)構築

1.1. パッケージ管理

brew install llvm qemu gnu-sed coreutils cdrtools

1.2. クロスツールチェーン (x86_64-elf)

  • Homebrew 公式フォーミュラではなく osdev-binutilsosdev-gcc を使うと衝突しにくい。
  • MikanOS と同じく Intel 64(x86_64) をターゲットにするのが学習コスト・情報量ともに有利。
    • aarch64 ネイティブで書きたい場合は Limine + UEFI の手順が大幅に変わるので要相談。

1.3. Python / Ninja (ビルドスクリプト用)


2. ブートチェーン

レイヤ 採用候補 理由
FW UEFI (OVMF.fd for x86_64) Apple Silicon + QEMU でも 安定/BIOS不要
ブートローダ - MikanLoaderPkg (そのまま流用)<br>- Limine (最新 UEFI 対応) 前者は教科書どおりに追従しやすい/後者は Starters が多い
カーネル C (一部 asm) ご希望に合わせ C に統一(MikanOS は C++)

MikanOS のブートローダ部は UEFI アプリとして実装されており、Apple Silicon + QEMU の組み合わせでも動作確認例が多い。


3. 推奨ツール & ライブラリ

目的 ツール メモ
GCC/Clang llvm (Clang + lld) LLD はリンクスクリプト不要で --lto が楽
アセンブラ nasm または gnu as UEFI スタブ程度なら最小で済む
デバッグ gdb-multiarc, lldb QEMU -s -S でリモート接続
イメージ生成 xorriso, mtools, objcopy fat12.img or esp.qcow2
CI GitHub Actions + brew M1 ローカルとの差分を最小化
ドキュメント mkdocs or mdBook 日々のメモを HTML 化

4. フェーズ別タスク詳細

Phase 1 — “Hello, kernel!”

[UEFI] → MikanLoaderPkg → Kernel C entry

  • UEFI アプリ (EFIAPI efi_main) で LoadKernel.efi を読み込み
  • GDT + Long mode 切り替え
  • QEMU 起動コマンド例

qemu-system-x86_64 \
  -machine q35,accel=hvf \
  -cpu host \
  -m 512M \
  -drive if=pflash,format=raw,unit=0,file=OVMF_CODE.fd,readonly=on \
  -drive if=pflash,format=raw,unit=1,file=OVMF_VARS.fd \
  -drive format=raw,file=fat:rw:./hddimg

Phase 2 — 画面出力 & 例外

  • UEFI から framebuffer 情報を受け取り、簡易 printf 実装
  • IDT を設定して #DE/#GP などを自前でハンドリング
  • 構造体:struct FrameBuffer, struct IDTR, struct InterruptFrame

Phase 3 — 物理/仮想メモリ

  • UEFI Memory Map → ビットマップ PMM (alloc_page / free_page)
  • 4 level page table を動的生成、カーネルを Higher Half (0xFFFF8000_00000000) に再配置
  • CR3 切替で仮想メモリ有効化 → fork( ) への布石

Phase 4 — スケジューラ

  • APIC タイマ (x2APIC) 100 Hz 割り込み
  • struct Task { regs_t regs; uint64_t *stack; ... }
  • 単純 ラウンドロビン で task_switch()
  • sleep() はタイマティック方式で実装

Phase 5 — ユーザプロセス & Syscall

  • ELF64 loader: .text/.data をユーザ空間 (例 0x00000000400000) にマッピング
  • int 0x80 風に sys_write, sys_exit など実装
  • init プロセスから /bin/shfork+exec

Phase 6 — シェル & コマンド

  • ミニシェル (char *readline(), parse(argv), exec)
  • 仮想 FS 層
  • RAMFS を root に
  • /dev/console, /proc/meminfo などはデバイスファイル
  • ls: RAMFS のディレクトリエントリ列挙
  • echo: write(1, argv[1], ...)

5. QEMU (Apple Silicon) Tips

課題 対応策
HVF は x86_64 ゲストでしか使えず若干遅い -accel hvf を有効にするだけで JIT 化され 3–5 倍高速
VGA 出力が崩れる OVMF の解像度を固定 (-display cocoa,gl=off)
GDB でレジスタ名が違う target remote :1234 後に set architecture i386:x86-64

6. 参考リソース

  • Deep Wiki / MikanOS — 日本語で UEFI-OS 開発を解説。図解が豊富。
  • MikanOS GitHub — 完全な C++ ソースとブートチェーン。ビルドスクリプトも公開。
  • Limine Bootloader — 最新 UEFI/BIOS 両対応の汎用ブートローダ。
  • osdev.org Wiki — ページング・割り込み・APIC の実装例が豊富。

7. 次のアクション

  1. ターゲットアーキテクチャ確認
    • 本計画は x86_64 ゲスト を前提にしています。
    • aarch64 ネイティブ で行いたい場合はブートローダから変更が必要です。
  2. フェーズ 1 の環境セットアップ
    • Homebrew でクロスツールチェーンを導入し、hello_kernel.efi が起動するまでをゴールに。
  3. 疑問点・追加要望があれば教えてください!
    • 例: 「C++ でも良い?」「シェルにパイプ機能を入れたい」など

🎉 Happy hacking!

riddle_tecriddle_tec

ここまですすんでた

OS開発プロジェクト進捗

フェーズ別タスクリスト

Phase 1: "Hello, kernel!" が QEMU で起動

  • UEFI ブートローダ (MikanLoaderPkg または Limine) のセットアップ
  • カーネルエントリーポイント (C言語) の作成
  • GDT (Global Descriptor Table) の設定
  • Long mode への切り替え
  • QEMU で "Hello, kernel!" の表示確認

Phase 2: VGA/Framebuffer 文字表示 & 例外ハンドラ

  • UEFI から Framebuffer 情報取得
  • 簡易的な文字表示関数の実装 (printf ライク)
  • IDT (Interrupt Descriptor Table) の設定
  • 例外ハンドラの実装 (例: #DE, #GP)
  • 構造体定義: struct FrameBuffer, struct IDTR, struct InterruptFrame

Phase 3: 物理メモリ管理 & ページング開始

  • UEFI Memory Map の解析と利用 (Limine経由で取得しPMMで使用)
  • 物理メモリマネージャ (PMM) の実装 (スタック/フリーリスト方式)
  • alloc_page / free_page 関数の実装
  • 4レベルページテーブルの動的生成
    • HHDMオフセットの取得と確認
    • ページング関連構造体定義 (paging.h)
    • PML4テーブル初期化準備 (paging.c)
    • カーネル空間のマッピング (k_start to k_end)
    • フレームバッファのマッピング
  • カーネルの Higher Half (例: 0xFFFF8000_00000000) への再配置 (ページングにより実現)
  • CR3 レジスタ切り替えによる仮想メモリ有効化
    • init_paging 内での各種マッピング処理 (HHDM, Kernel, Framebuffer, Stack)
    • 高位カーネルへのジャンプ成功 (switch_to_kernel_higher_half_and_run により kernel_main_after_paging が実行されることを確認)

Phase 4: タイマ割り込み & ラウンドロビン・スケジューラ

  • Sub-Task 1: APIC タイマ割り込みの実装
    • Limine から SMP 情報取得
    • apic.c/apic.h の作成
    • タイマベクタ (32) 用の IDT/ISR 更新
    • init_apic 呼び出し、割り込み有効化、ティックカウント表示
    • xAPIC フォールバック対応
    • QEMU でのビルドとテスト (カーネル起動、タイマ動作確認)
  • Sub-Task 2: 基本的なラウンドロビン・スケジューラ
    • タスク状態セグメント (TSS) 構造体の定義 (gdt.h)
    • TSS 用の GDT エントリ作成 (gdt.c, gdt.h)
    • タスク状態セグメントレジスタ (LTR) のロード (gdt.c)
    • TSS.RSP0 フィールドへのカーネルスタックポインタ設定 (gdt.c, main.c, paging.c)
    • シンプルなプロセス制御ブロック (PCB) 構造体 (task_t) の定義 (task.h)
    • シンプルなタスクキュー (task_queue_t) の作成と操作関数 (task.c, task.h)
    • タイマ割り込みハンドラ (timer_handler) 内に schedule() 関数を実装済み (基本的な選択とTSS RSP0設定)
      • コンテキスト保存の準備 (current_task->context = *regs; のような処理)
      • 次タスクの選択 (dequeue_task) と current_task 更新
      • 次タスクのRSP0設定 (tss_set_rsp0)
      • コンテキスト復元の準備 (スタック書き換え)
    • Sub-Task 2.1.4 (変更): PMMスタックページのマッピング検証とビルド修正
      • kernel/pmm.cpmm_info の実体を定義する。
      • kernel/main.cserial_putc_directwrite_serial_char に置き換える。
      • kernel/paging.hpaging_success_haltextern 宣言を追加する。
      • kernel/paging.cinit_paging 末尾で switch_to_kernel_higher_half_and_run の呼び出しをコメントアウトし、paging_success_halt() を呼び出すようにする(ビルド通過のための一時的措置)。
      • init_paging がPMMの最初のスタックページ (物理アドレス 0x200000) をHHDMに正しくマッピングしていることを確認 (kernel_main_after_paging でのR/Wテスト成功)
    • Sub-Task 2.1.5 : ダミータスクの生成とエンキューのためのPMM安定化
      • Sub-Task 2.1.5.1: PMM init_pmm 内スタックポインタ整合性検証
        • 目的: init_pmm完了時にpmm_stack_toppmm_current_stack_headがPMMスタックの正しい状態を指すことを確認。
        • 作業: init_pmmpmm_free_page呼び出し前後で主要変数のログを追加。特にスタックページ追加時の分岐を詳細化。QEMUで実行しログ分析。
        • 期待: init_pmm完了時、pmm_stack_topが最後のスタックページの空き状況を、pmm_current_stack_headがそのページを正しく指す。
      • Sub-Task 2.1.5.2: PMM map_page におけるページテーブル構造の自己マッピング安定化
        • 目的: map_page が、自身や他のページをマッピングするために必要なページテーブル構造 (PDPT, PD, PT) を pmm_alloc_page で確保し、その確保した構造自体をHHDMにマッピングする際の再帰呼び出し (is_for_table_structure = true) が、ページフォルトや無限再帰を引き起こさずに安定して動作することを確認する。
        • 作業: map_page 内の再帰呼び出し (*_STRUCT_SM タグ) 前後のログ、および is_for_table_structure = true の場合の pmm_alloc_page 呼び出しを監視。kernel/main.cのPMMスタックテスト箇所を実行し検証。
        • 期待: ページテーブル構造の確保と自己マッピングが成功し、確保したページテーブルに対するclear_pageが安全に実行できる。ページフォルトが発生しない。
      • Sub-Task 2.1.5.3: PMM pmm_alloc_page スタック枯渇時の旧スタックページへの切り替えロジック検証
        • 目的: スタックが空になった際、正しく以前のスタックページ (previous_stack_page_phys) を特定し、pmm_info の関連変数が適切に更新されることを確認する。
        • 作業: 関連するログを強化し、kernel/main.cのPMMスタックテスト(多数確保)などで動作確認。
        • 期待: ログ上で、旧スタックページへの切り替え情報が正しく表示される。
      • Sub-Task 2.1.5.4: PMM pmm_alloc_page 切り替え後の新PMMスタックページのマッピング処理検証
        • 目的: pmm_alloc_page がスタックを切り替えた後、新しいPMMスタックページをHHDMにマッピングするために map_page (タグ PMM_ALLOC_SWITCH_STACK) を呼び出す際の動作を検証する。
        • 作業: map_page 呼び出し前後と、その内部での動作ログを分析。
        • 期待: map_page が正しく呼び出され、新しいPMMスタックページがHHDMにマッピングされる。
      • Sub-Task 2.1.5.5: (上記2.1.5.2-4解決後) pmm_alloc_page スタックページ切り替え全体の動作確認
        • 目的: 複数のページを連続して割り当て・解放し、PMMスタックページの切り替えが複数回発生してもシステムが安定していることを確認する。
        • 作業: main.c などで多数のページ割り当て/解放を行うテストコードを実行。
        • 期待: ページフォルトが発生せず、PMMが正常に動作する。
      • Sub-Task 2.1.5.6: ダミータスク生成とエンキュー最終確認 (旧2.1.5.3)
        • 目的: PMM安定化後、ページフォールトなくタスク生成・エンキューログが出力されることを確認。
        • 作業: 上記修正後、kernel/main.cのタスク生成処理を実行。QEMUでページフォールトなく関連ログ出力確認。
        • 期待: 当初のSub-Task 2.1.5目標達成。
      • Sub-Task 2.1.5.7: タイマー割り込みとスケジューラ基本動作確認 (旧2.1.5.4)
        • 目的: stihltを有効化し、タイマー割り込みによるスケジューラの基本動作を確認。
        • 作業: kernel_main_after_pagingstihltを有効化。
        • 期待: カウントアップ表示またはタスクスイッチを示唆する動作が確認できる。
    • Sub-Task 2.2: 初期タスクの起動準備とコンテキストスイッチロジックの調整
      • kernel/main.c (kernel_main_after_paging 内): ready_queue から最初のタスクを dequeue_task で取得し current_task に設定。このタスクの tss_set_rsp0(current_task->kernel_stack_top) を呼び出し。もし current_task->context.cr3 が現在のCR3と異なれば load_cr3() を呼び出す(通常カーネルタスクでは同じはず)。
      • kernel/apic.c (timer_handler 内のコンテキスト保存・復元部分):
        • コンテキスト保存時: current_task (つまり old_task) が NULL でなく、old_task->has_run_once == true の場合のみ、スタック上のレジスタ情報 (regs が指す領域と、それ以降の iretq フレーム) を old_task->context へコピー。old_task->context.cr3get_current_cr3() で更新。初回(has_run_once == false)の場合は、create_task で設定された初期コンテキストが使われるように保存処理をスキップし、old_task->has_run_once = true に設定する。
        • コンテキスト復元時: schedule() によって current_task (つまり new_task) が更新された後、new_taskNULL でなく、(old_task != new_task または !new_task->has_run_once) の場合に、new_task->context からスタック上の対応する位置へレジスタ情報と iretq フレームをコピー。new_task->context.cr3 が現在のCR3と異なれば load_cr3()。復元時に new_task->has_run_once = true に設定。
      • kernel/main.c (kernel_main_after_paging 内): init_apic 呼び出しの後、asm volatile ("sti"); で割り込みを有効化し、無限 hlt ループに入り、最初のタイマー割り込みによるタスクスイッチを待つ。
    • Sub-Task 2.3: 動作検証とデバッグ
      • make clean && make && make run-bios でビルドと実行。
      • QEMU のシリアルコンソールで、各ダミータスクが出力する異なる文字が交互に表示されることを確認。
      • 問題が発生した場合、シリアルデバッグ出力を頼りに、コンテキスト構造体の内容、スタックポインタの操作、has_run_once フラグの遷移、create_task での初期コンテキスト設定、timer_handler でのスタックからの読み書きオフセットなどを中心にデバッグ。

Phase 5: ELF 形式のユーザプロセス起動

  • ELF64 ローダーの実装
  • .text, .data セクションのユーザー空間へのマッピング
  • システムコールインターフェースの実装 (例: int 0x80)
  • 基本的なシステムコール (sys_write, sys_exit など) の実装
  • init プロセスの作成
  • forkexec (または同等の機能) の実装 (init から /bin/sh 起動目標)

Phase 6: ミニシェル + echo / ls

  • ミニシェルの実装
    • readline() (入力取得)
    • parse() (コマンド解析)
    • exec() (コマンド実行)
  • 仮想ファイルシステム (VFS) 層の設計と実装
  • RAMFS (RAMベースのファイルシステム) の実装とルートマウント
  • デバイスファイルの実装 (例: /dev/console, /proc/meminfo)
  • ls コマンドの実装 (RAMFS のディレクトリエントリ列挙)
  • echo コマンドの実装 (write(1, ...) を使用)

その他

  • ホスト環境構築 (クロスツールチェーン、QEMUなど)
  • ビルドスクリプトの整備
  • デバッグ環境の確立 (GDB/LLDBリモートデバッグ)
riddle_tecriddle_tec

PMM あたりは複雑なので、C とはいえテストしながらやったほうがよかった

riddle_tecriddle_tec

振り返り

  • テスト駆動を行えるようにすべきだった
  • 知識を入れてからやるべきなのはそうなのだが、それは極力避けたい
  • 毎度立ち上がり、こっちで画面を閉じないと作業が進まないがそれだとめんどくさいので勝手に停止してくれればよかったな
riddle_tecriddle_tec

PMMの実装について、振り返りと改善案を提案させていただきます。

まず、現在の状況を確認させていただきます。

Ran tool
コードを確認した結果、以下のような問題点と改善案が見えてきました:

  1. 初期化の順序と依存関係の問題
  • 問題点:
    • PMMの初期化とページングの初期化の順序が重要ですが、依存関係が複雑
    • HHDMオフセットの取得とPMMスタックのマッピングのタイミングが重要
  • 改善案:
    1. ブートローダーからメモリマップ情報を取得
    2. HHDMオフセットを確定
    3. PMMスタック用の初期ページを確保(物理アドレス)
    4. ページング初期化
    5. PMMスタックをHHDMにマッピング
    6. 残りのメモリをPMMに登録
    
  1. PMMスタックの管理方式
  • 問題点:
    • 現在の実装では、スタックページの切り替えロジックが複雑
    • スタックページのリンク管理が不完全
  • 改善案:
    1. より単純な双方向リンクリスト方式の採用
    2. スタックページのヘッダー構造を明確に定義
    3. スタックページの切り替え時の整合性チェックの追加
    
  1. デバッグと検証の強化
  • 問題点:
    • エラー発生時の情報が不十分
    • メモリ操作の整合性チェックが不足
  • 改善案:
    1. より詳細なデバッグログの実装
    2. メモリ操作前後の整合性チェック
    3. スタックページの状態検証関数の追加
    
  1. 段階的な実装アプローチ
    以下のような順序で実装することを提案します:
Phase 1: 基本的なPMM実装
- 固定サイズのPMMスタック
- 単純なalloc/free操作
- 基本的なエラーチェック

Phase 2: スタック拡張機能
- スタックページの動的割り当て
- ページリンク管理
- スタック切り替えロジック

Phase 3: 堅牢性の向上
- 詳細なエラーチェック
- デバッグ機能の強化
- パフォーマンス最適化
  1. テスト戦略の改善
  • 問題点:
    • 現在のテストが不十分
    • エッジケースの考慮が不足
  • 改善案:
    1. 単体テストの追加
     - 基本的なalloc/free操作
     - スタックページの切り替え
     - エラーケース
    
    2. 統合テストの追加
     - ページングとの連携
     - メモリマップの解析
     - 大量のページ操作
    
    3. ストレステストの追加
     - 連続的なalloc/free
     - メモリ断片化
     - エラーリカバリ
    
  1. エラーハンドリングの改善
  • 問題点:
    • エラー発生時の回復処理が不十分
    • エラー状態の伝播が不明確
  • 改善案:
    1. エラーコードの体系化
    2. エラー回復メカニズムの実装
    3. エラー状態のログ記録
    

これらの改善を実装する際の具体的なステップとして:

  1. まず、PMMの基本構造を単純化し、固定サイズのスタックで動作確認
  2. 次に、スタック拡張機能を段階的に追加
  3. 各段階で十分なテストを実施
  4. 最後に、堅牢性とパフォーマンスの最適化

このアプローチにより、より安定したPMMの実装が可能になると考えられます。

このスクラップは2ヶ月前にクローズされました