※小説ではない※専門書 要約資料集 為替(換算)3.9万円でもらう 紐解集生成 専門 初入門 資料 作:{作者名}
> 学問の宇宙・統合大型巻「PC創造大全」物語脊椎 第7部(g)。ガイド役: Fable 5 監修 / Sonnet 5 執筆(脚本班)
> トーン規約: GAKUMON_UNIVERSE.md準拠。専門用語は初出で必ず説明する。
> **安全枠(§16.18・CATALOG_PC創造大全.md §3・絶対厳守)**: 本冊は教育用に縮小した「たまごOS」の実装工程を、**エミュレータ(実機を模した仮想的な計算機環境)上での動作確認を標準経路**として扱う。実機へのインストールは、使用中のパソコンに残っているデータやOSを破壊する重大なリスクを伴うため、本冊はそれを前提とした手順を書かない。実機で試したい場合は、消えてよいデータしか入っていない予備機に限る、と本冊全体を通じて明記する。実在するOS(Windows・Linux・macOSなど)への侵入・改竄・権限昇格・マルウェア作成に関する技術は一切扱わない。本冊が示すコードは、すべて教育目的の小さな断片であり、仮想マシンの中だけで完結させることを前提にしている。
> 接続先: →BOOK-0340『作る力と生きる力: OSを作るということ 第1巻』(本冊の**概念前篇**。ブート・プロセス・メモリ管理・ファイルシステム・仮想メモリ・スケジューラの「考え方」はそちらが土台になっている。本冊はその実装続篇であり、同じ用語を再定義せず「→0340第N章」で参照する)、→BOOK-0342『論理回路からCPUへ』(CPUのレジスタ・クロック・命令実行という土台)、→BOOK-0358f(第7部f「関数を正式に組む」・未執筆/予約番号のみ確定。本冊のC風コード断片の設計思想はそちらの関数契約の考え方を前提にしている)、→BOOK-0358h(第7部h「構造の読み方」・未執筆/予約番号のみ確定。本冊のリンカスクリプトや構造体レイアウトを読む力はそちらでさらに深める予定)。
> 水準: 七〜十(概念を知っている状態から、実際に手を動かして「たまごOS」を一段ずつビルドし、電源投入からシェルが動くところまでを一本道で実装する)。
---
# BOOK-0358g PC創造大全 第7部: OSを作る実践 — たまごOSを、本当に組み立てる
> 学問の宇宙・統合大型巻「PC創造大全」物語脊椎 第7部(g)。ガイド役: Fable 5 監修 / Sonnet 5 執筆(脚本班)
> トーン規約: GAKUMON_UNIVERSE.md準拠。専門用語は初出で必ず説明する。
> **安全枠(§16.18・CATALOG_PC創造大全.md §3・絶対厳守)**: 本冊は教育用に縮小した「たまごOS」の実装工程を、**エミュレータ(実機を模した仮想的な計算機環境)上での動作確認を標準経路**として扱う。実機へのインストールは、使用中のパソコンに残っているデータやOSを破壊する重大なリスクを伴うため、本冊はそれを前提とした手順を書かない。実機で試したい場合は、消えてよいデータしか入っていない予備機に限る、と本冊全体を通じて明記する。実在するOS(Windows・Linux・macOSなど)への侵入・改竄・権限昇格・マルウェア作成に関する技術は一切扱わない。本冊が示すコードは、すべて教育目的の小さな断片であり、仮想マシンの中だけで完結させることを前提にしている。
> 接続先: →BOOK-0340『作る力と生きる力: OSを作るということ 第1巻』(本冊の**概念前篇**。ブート・プロセス・メモリ管理・ファイルシステム・仮想メモリ・スケジューラの「考え方」はそちらが土台になっている。本冊はその実装続篇であり、同じ用語を再定義せず「→0340第N章」で参照する)、→BOOK-0342『論理回路からCPUへ』(CPUのレジスタ・クロック・命令実行という土台)、→BOOK-0358f(第7部f「関数を正式に組む」・未執筆/予約番号のみ確定。本冊のC風コード断片の設計思想はそちらの関数契約の考え方を前提にしている)、→BOOK-0358h(第7部h「構造の読み方」・未執筆/予約番号のみ確定。本冊のリンカスクリプトや構造体レイアウトを読む力はそちらでさらに深める予定)。
> 水準: 七〜十(概念を知っている状態から、実際に手を動かして「たまごOS」を一段ずつビルドし、電源投入からシェルが動くところまでを一本道で実装する)。
---
## 入口の物語 — 今度は、自分の手で組み立てる番だ
新人プログラマーは、計算機文明部の工房で「たまごOS」の設計図を読み終え、ブート・プロセス・メモリ管理・ファイルシステムという四つの土台をひと通り頭に入れた。先輩から手渡された次の資料には、こう書かれていた。「概念は分かった。次はこれを、自分の手でゼロから組み立ててみよう」。
机の上には、空っぽのテキストエディタと、書き込み用の空のディスクイメージ(仮想的なディスクの中身をそのままファイルにしたもの)、そしてエミュレータ(実機を模した仮想的な計算機環境)を起動するための短いコマンドが用意されていた。「実機には触らせない」と先輩は念を押した。「このパソコンにもう一台分のOSをまるごと書き込む練習をするのに、間違って自分の作業用ディスクを壊したら目もあてられない。だからここから先は、全部エミュレータの中で確かめる。もし将来どうしても実機で動かしたくなったら、そのときはデータが消えてもかまわない予備機だけを使うと約束してほしい」。
新人プログラマーはうなずき、まず何もしていない空のディスクイメージをエミュレータに渡してみた。画面には短い一行、「起動可能なディスクが見つかりません」という趣旨のメッセージだけが表示された。「これが出発点だ」と先輩は言った。「このメッセージが出るということは、少なくともエミュレータは正しく起動し、ディスクの中身を律儀に読みにいっている、ということが分かる。次にやるべきことは、このディスクの一番最初の512バイトに、何か意味のある命令を書き込むことだ」。
「概念は地図だ」と先輩は付け加えた。「地図を眺めているだけでは、目的地までの道の途中にある小さな石ころや、思いがけない曲がり角には気づけない。GDTの1バイトを書き間違えただけで画面が真っ黒のまま固まってしまうことも、PICへ処理完了の合図を1行送り忘れただけでタイマがぴたりと沈黙してしまうことも、地図の上には決して書かれていない。それは自分の手を動かして、実際にコードを書き、動かし、失敗し、原因を1つずつ探ってみて、初めて分かることだ」。
新人プログラマーは、渡された空のディスクイメージを見つめ直した。半年前、初めてたまごOSの"Hello, world."を見たときは、ただ画面に映る一行の文字を眺めるだけの立場だった。今度は逆に、自分がその一行を画面に出す側に回る番である。「電源を入れてから、シェルが自分の打ったコマンドに答えてくれるまで――その間に積み重なっている段階を、今度は一段ずつ、自分の手で組み立てていく」。先輩の言葉を胸に、新人プログラマーはエディタの空白のファイルへ、最初の1行を書き始めた。
この一言から、たまごOSを実際にビルドし、電源投入の瞬間から簡易シェルが動くところまでを一本道でたどる、本冊の作業が始まる。ブートローダ→CPUモードの切り替え→カーネルへの橋渡し→メモリ管理→割込とタイマ→簡易ファイルシステム→プロセスとスケジューラ→簡易シェル、という順番は、前篇(→BOOK-0340)で学んだ概念の並びとまったく同じである。違うのは、今度はそれぞれの段で「実際に動くコード」を書き、エミュレータの画面で結果を目で確かめながら進む、という点である。各章の終わりには、必ず「ここまでで動くもの」を確認する一節を置く。次の章に進む前に、その動作確認点を自分の手で再現できているかどうかを、必ず確かめてから読み進めてほしい。概念を知っていることと、実装できることの間にある距離を、1つずつ埋めていくのが、この第7部の仕事である。
---
## 第一章: 開発環境と安全枠 — 一本道に入る前に(水準七)
### なぜエミュレータが標準経路なのか
本冊の作業は、すべて**エミュレータ(水準七: 実機のハードウェアの挙動をソフトウェアで模倣し、実機を使わずに動作確認できるようにした仮想的な計算機環境)**の上で行う。教育用に配布されている代表的なエミュレータの一つに**QEMU(キューエミュー、水準七: さまざまなCPUアーキテクチャの実機を模倣できる、オープンソースのエミュレータ)**があり、本冊のたまごOSもQEMU上での起動を標準の確認手段とする。
実機にたまごOSを直接インストールする方法を本冊が書かない理由は単純である。実機の記憶装置には、多くの場合すでに別のOSや大切なファイルが入っている。書き込み先を一文字間違えるだけで、それらが跡形もなく消えてしまう危険がある。エミュレータは、ホスト側のパソコンの中に「仮想的なディスク」をただのファイルとして作るだけなので、間違えても被害はそのファイルの中にとどまり、実機の他の部分には一切影響しない。この安全さゆえに、たまごOSの開発と動作確認は、本冊を通じてエミュレータを標準経路とする。どうしても実機で試したくなった場合は、消えてよいデータしか入っていない予備機だけを使い、その予備機の記憶装置全体を書き換える覚悟のうえで行う、というのが本冊の唯一の実機解禁条件である。
### クロス開発という考え方
パソコンで文章を書くとき、ふつうは自分のパソコン(ホスト)の上でエディタを動かし、そのままホストの上でファイルを保存する。ところがOSを作るときは事情が違う。たまごOSは、ホストのOS(WindowsやLinuxなど)の助けを一切借りずに、電源投入直後の何もない状態から単独で動かなければならない。開発作業そのものはホストの上で行いながら、できあがるプログラムは「ホストとは別の、まっさらな環境」で動くことを前提に作る、というこの開発スタイルを**クロス開発(水準七: 開発作業を行う環境〈ホスト〉と、実際にプログラムが動く環境〈ターゲット〉が異なる開発スタイル)**と呼ぶ。たまごOSの場合、ホストはふだん使っているパソコン、ターゲットはQEMUが模倣する仮想的なPCである。
クロス開発を支える一そろいの道具を**ツールチェーン(水準七: ソースコードを実行可能な形に変換するまでの一連の道具の組。アセンブラ・コンパイラ・リンカなどをまとめて指す)**と呼ぶ。たまごOSのツールチェーンは、依存を最小限にするため、次の3種類だけを使う。
```
ツールチェーンの3点セット:
1. アセンブラ(水準七: アセンブリ言語で書かれた命令を、CPUが直接実行できる機械語に変換する道具。
たまごOSのブートローダ部分の変換に使う)
2. Cコンパイラ+リンカ(水準七: C言語で書かれたカーネル本体を機械語に変換し〈コンパイラ〉、
複数の機械語の断片を1つの実行可能な形にまとめる〈リンカ〉道具の組)
3. QEMU(動作確認用のエミュレータ)
```
### ビルドの一本道 — ソースからディスクイメージへ
たまごOSが実際に「動く」までには、書いたソースコードがいくつかの姿を経由する。この変換の道筋を**ビルド(水準七: ソースコードから、実行可能な形式のプログラムを作り上げる一連の作業)**と呼び、たまごOSのビルドは次の一本道をたどる。
```
ソースコード(アセンブリ・C言語)
→ オブジェクトファイル(各ソースを個別に機械語へ変換した断片)
→ リンク(複数のオブジェクトファイルを1つにまとめる。第四章で詳しく扱う)
→ 実行可能イメージ(まとまった機械語の塊)
→ ディスクイメージ(水準七: 仮想的なディスクの中身を、そのまま1つのファイルとして表したもの。
ブートローダ・カーネル・簡易ファイルシステムのデータを、決まった順番で並べて作る)
→ QEMUに渡して起動確認
```
**ディスクイメージ**は、たとえて言えば「まっさらなディスクの中身をそっくりコピーした写し」である。中身が空であれば、QEMUに渡しても何も起きない(あるいは「起動可能なディスクがない」という主旨のメッセージが出るだけである)。この写しの先頭512バイトに意味のある命令を書き込むところから、第二章のブートローダ実装が始まる。
### プロジェクトの部屋割り — ソースツリーの設計
家を建てるとき、配線図・配管図・構造図をひとまとめの1枚に描かず、工程ごとに図面を分けて管理するのと同じように、たまごOSのソースコードも、役割ごとにフォルダを分けて管理する。
```
たまごOSのソースツリー(概略・依存最小):
boot/ … ステージ1・ステージ2(アセンブリ、第二・三章)
kernel/ … kmain()とカーネル本体(C言語、第四章以降)
mm/ … メモリ管理(フレームアロケータ・ページング、第五章)
int/ … 割込・PIC・PIT(第六章)
fs/ … 簡易ファイルシステム(第七章)
proc/ … PCB・コンテキストスイッチ・スケジューラ(第八・九章)
shell/ … 簡易シェル(第十章)
tools/ … ディスクイメージ組み立て・ビルド自動化(第十一章)
```
この部屋割りは、コードを書く順番(第二章から第十一章までの一本道)と、フォルダの並び順がほぼ一致するように設計してある。迷ったときは「今どの章の作業をしているか」を、そのままフォルダ名として思い出せるようにしておくと、ビルドが失敗したときにも原因を切り分けやすい。
### コラム — なぜx86系を前提にするのか
本冊のコード断片は、すべてIntel/AMD系の**x86系(→BOOK-0340第三章で紹介済みのCPU系統)**を前提にしている。世の中にはARM系(スマートフォンや近年のノートパソコンに広く使われる系統)やRISC-V系(近年注目を集めている、仕様が公開された新しい系統)など、他のCPU系統も存在し、それぞれ電源投入直後の初期化手順やレジスタの構成はまったく異なる。x86系を選んだ理由は、GDT・IDT・PIC・PITのように、教育用の資料や実例が世界的に最も豊富に蓄積されている系統だからである。他の系統でOSを実装する場合も、「電源投入→最初のコードが動く→CPUの初期設定→メモリ管理→割込→ファイルシステム→プロセス管理→シェル」という一本道の骨組み自体は共通しており、本冊で身につけた考え方は、系統をまたいでも土台として役立つ。
### 小さく足して、都度動かす — 実装の進め方そのものについて
本冊の11の章は、それぞれが独立した動作確認点を持つように設計されている。これは偶然ではなく、CATALOG_PC創造大全.md §0.5が掲げる「段落独立請求項」――各章が前提リンクを明示すれば単独でも成立するという設計思想――を、コードの書き方そのものにも適用した結果である。GDTの設定、ページングの有効化、割込ハンドラの登録といった処理を、すべて一度に書いてから初めて動かそうとすると、うまく動かなかったときにどこに原因があるのか、手がかりがまったくつかめなくなる。
たまごOSの実装は、次のような「小さく足して、都度動かす」進め方を徹底することを勧める。
```
実装の基本サイクル:
1. 1つの章の中の、さらに小さな一手(たとえば「GDTのエントリを1つ作る」)だけを書く
2. すぐにビルドして、QEMUで動かす
3. その章の動作確認点(あるいはそれに至る途中の小さな確認)が取れるまで、1と2を繰り返す
4. 確認が取れたら、次の小さな一手へ進む。決して複数の一手をまとめて書かない
```
この進め方は、遠回りに見えて実は最短経路である。複数の変更を1度にまとめてから動かすと、不具合が起きたときにどの変更が原因かを切り分けるだけで多くの時間を消費してしまう。1つずつ動かして確かめていれば、不具合が起きた直後の「直前に足した、たった1つの変更」が疑わしい箇所のほぼすべてになる。第十一章のトラブルシューティング決定木も、突き詰めればこの「小さく足して、都度動かす」進め方を後押しするための道具立てである。
### 動作確認点(第一章) — ここまでで動くもの
空のディスクイメージ(たとえば1.44MBぶんのゼロで埋めたファイル)を用意し、QEMUに`-drive file=disk.img,format=raw`のような形で渡して起動する。画面に「起動可能なディスクが見つかりません」という趣旨のメッセージ、あるいは何も表示されない黒い画面が出れば、ツールチェーンとQEMUが正しく連携していることが確認できる。この時点ではたまごOS自身のコードは一切動いていないが、「エミュレータへ正しくディスクイメージを渡せる」という、以降すべての章の土台になる一歩が確認できたことになる。
> **定着量の目安(第一章)**: エミュレータ・QEMU・クロス開発・ツールチェーン・ビルド・ディスクイメージという6つの用語と、ソースコードからディスクイメージまでの一本道の順番を、図を見ずに自分の言葉で説明できるようにするには、この一本道を紙に書き写す練習を3回程度こなすと、ほぼ完全に定着すると見込まれる。
---
## 第二章: ブートローダを実際に書く — BIOSが最初のセクタを読む仕組みの実装(水準七)
→BOOK-0340第二章では、ブートセクタ・ブートローダ・マジックナンバー(`0xAA55`)という概念を扱った。本章では、その概念を実際に動くコードへ落とし込む。
### ディスクの最小単位 — セクタ
ディスクが読み書きできる最小の単位を**セクタ(水準七: ディスク装置が一度に読み書きできる、データの最小単位。伝統的には512バイトが広く使われてきた)**と呼ぶ。ファームウェア(BIOS)がブートデバイスの先頭から読み込む512バイトは、ちょうどこの1セクタぶんにあたる。
### 二段構えのブートローダ
512バイトという広さは、文字を数行表示するだけならともかく、カーネル本体を読み込んで保護モードへ切り替えるところまでの処理をすべて詰め込むには狭すぎる。そこで実務上よく使われるのが、**二段ブートローダ(にだんブートローダ、水準七: 最初の512バイト〈ステージ1〉には「もっと大きな第二のブートローダ〈ステージ2〉をディスクから読み込んで実行を移す」処理だけを書き、実際の初期化処理の大部分をステージ2に任せる方式)**という設計である。たまごOSも、この二段構えを採用する。
```
; ステージ1(先頭512バイト、教育用に簡略化・仮想マシン上でのみ実行)
org 0x7C00 ; ファームウェアがこのプログラムを読み込む決まった番地
mov [BOOT_DRIVE], dl ; ファームウェアがDLレジスタに渡してくれる起動ドライブ番号を保存
; セグメントレジスタを明示的に0へそろえる(ファームウェア実装によりCS:IPの解釈が
; わずかに異なることがあるため、決め打ちにしておくのが安全策)
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00 ; スタックをブートローダ自身の直下に置く
mov si, msg_stage1
call print_string ; "Loading kernel..." のような一行を表示
; ステージ2を、ディスクの2セクタ目から読み込む(BIOS割込0x13、機能AH=0x02)
mov ah, 0x02 ; 機能番号: セクタ読み込み
mov al, STAGE2_SECTORS ; 読み込むセクタ数
mov ch, 0 ; シリンダ番号
mov cl, 2 ; 開始セクタ番号(1セクタ目は自分自身なので2から)
mov dh, 0 ; ヘッド番号
mov dl, [BOOT_DRIVE] ; 起動ドライブ番号
mov bx, 0x8000 ; 読み込み先のメモリ番地
int 0x13
jc disk_error ; キャリーフラグが立っていればエラー
jmp 0x0000:0x8000 ; ステージ2へジャンプして実行を引き渡す
disk_error:
mov si, msg_error
call print_string
jmp $
msg_stage1 db "Loading kernel...", 0
msg_error db "Disk read error.", 0
BOOT_DRIVE db 0
; (print_string手続き・print_charなどは省略。BOOK-0340第二章の実装と同じ発想で、
; BIOS割込0x10・機能AH=0x0Eを1文字ずつ呼び出す)
times 510-($-$$) db 0
dw 0xAA55
```
### キャリーフラグという成否の合図
BIOS割込`int 0x13`が処理を終えたあと、成功したか失敗したかを、CPUの**キャリーフラグ(水準七: 演算結果の桁あふれなどを示すCPUのフラグの一つ。多くのBIOS割込では、処理が失敗したときにこのフラグが立てられる〈1になる〉という約束事で使われる)**を見て判定する、という約束事が広く使われている。上のコードで`jc disk_error`(キャリーフラグが立っていればdisk_errorへジャンプ)としているのは、この約束事に従ったエラー処理である。エラー処理を省略してしまうと、ディスク読み込みに失敗したにもかかわらず、でたらめな番地へジャンプしてしまい、原因のわかりにくい暴走につながる――ブートローダを書くときに見落としやすい罠の一つである。
### 表示手続きの中身 — print_stringとBIOSテレタイプ出力
上のコードで呼び出している`print_string`は、→BOOK-0340第二章のブートローダ例でも使われていた手続きだが、本冊では実装の中身まで踏み込む。
```
; print_string手続き(擬似アセンブリ、依存最小)
print_string:
push ax
.loop:
lodsb ; SIが指すメモリから1バイト読み、ALへ入れてSIを進める
or al, al ; 読んだバイトが0(文字列の終わり)かどうか調べる
jz .done
mov ah, 0x0E ; BIOS機能番号: 1文字をテレタイプ表示する
int 0x10
jmp .loop
.done:
pop ax
ret
```
`int 0x10`のAH=0x0E(**テレタイプ出力、水準七: BIOS割込0x10の一機能。カーソル位置を自動的に進めながら、1文字ずつ画面へ表示する機能**)は、改行やカーソル移動をBIOS側が肩代わりしてくれるため、ごく短いコードで文字列表示が実現できる。文字列の終わりを「0という値のバイト」で示す約束事を**ヌル終端(水準七: 文字列の末尾に、値が0の1バイトを置くことで終わりを示す表現方法)**と呼び、`or al, al`で0かどうかを判定しているのは、このヌル終端を検出するための定石の書き方である。
### コラム — 自分でブートローダを書かない、という選択肢
本冊は教育目的で、ステージ1・ステージ2を自分の手で書く方式を採用しているが、実務の世界では、既存の高機能なブートローダ(たとえばGRUBのような、Multibootという規格に対応したブートローダ)にカーネルの読み込みを任せてしまう方式も広く使われている。この場合、開発者はブートセクタの512バイトを手書きする代わりに、カーネルの実行イメージの先頭にMultiboot規格が定める決まった目印(マジックナンバーを含むヘッダ情報)を埋め込むだけで、GRUBが代わりにディスクからカーネルを読み込み、保護モードへの切り替えの一部まで済ませた状態で実行を引き渡してくれる。自分の手でステージ1から書く経験は、こうした既存のブートローダが「裏で何をしてくれているか」を具体的に理解するうえでも役立つ。
### org 0x7C00を書き忘れると何が起きるか
`org 0x7C00`という1行は、アセンブラに対して「このプログラムは0x7C00番地に置かれる前提でアドレスを計算せよ」と指示するものである。試しに、この1行を書き忘れた場合に何が起きるかを考えてみる。文字列`msg_stage1`の実際の番地は、ファームウェアによって変わらず0x7C00から読み込まれるにもかかわらず、アセンブラは「このプログラムは0番地から置かれる」という誤った前提で`mov si, msg_stage1`の値を計算してしまう。その結果、SIレジスタには本来より0x7C00小さい値が入り、`print_string`はまったく無関係な番地のメモリを、文字列だと思い込んで表示しようとする。実際に起きる症状は「画面に意味不明な文字化けが表示される」「何も表示されずフリーズする」など様々だが、いずれにしても正しい文字列は表示されない。`org`という、ともすれば見落としがちな1行が、プログラム全体のアドレス計算の前提を丸ごと左右する――ブートローダのようにリンカを介さず直接メモリ番地を決め打ちするコードならではの、代表的な罠である。
### 検算1 — ステージ2の大きさとセクタ数
ステージ2のプログラムが8,192バイト(8KB)にコンパイルされたとする。1セクタは512バイトなので、必要なセクタ数は次のように求められる。
```
必要セクタ数 = 8,192バイト ÷ 512バイト = 16セクタ
```
このため、上のコード中の`STAGE2_SECTORS`には16を設定する。もしステージ2がわずかでも8,192バイトを超えていた場合、切り上げが必要になる(たとえば8,300バイトなら8,300÷512=16.2…なので17セクタ分を確保しなければ、末尾が読み込まれずに欠けてしまう)。この「バイト数をセクタサイズで割って切り上げる」計算は、後の章でディスクイメージ全体のレイアウトを設計するときにも繰り返し使う考え方である。
### 罠 — ディスク読み込みは一度で成功するとは限らない
上のブートローダ例は、`int 0x13`を1回だけ呼び、失敗したら即座にエラー表示へ飛ぶ、もっとも単純な形になっている。しかし実機のディスク装置(特に古い光学ドライブなど)では、一時的な読み取り不良が起きることがあり、1回失敗しただけで即座に諦めるのは実務上やや過敏すぎる場合がある。実務のブートローダの多くは、`int 0x13`が失敗したときに数回まで読み直しを試す**リトライループ(水準七: 処理が失敗したときに、あきらめる前に一定回数まで同じ処理をやり直す仕組み)**を備えている。たまごOSは教育目的のため単純な即時エラー表示にとどめているが、「エラー処理は1回失敗しただけで即あきらめてよいとは限らない」という視点は、以降の章のディスクI/O(第七章のATA PIO)を実装するときにも通じる考え方である。
### 動作確認点(第二章) — ここまでで動くもの
ステージ1(512バイト、末尾に`0xAA55`)とステージ2(ここでは"Stage 2 loaded."とだけ表示して無限ループする、ごく短いプログラムでよい)を、この順でディスクイメージに書き込み、QEMUで起動する。画面に「Loading kernel...」に続いて「Stage 2 loaded.」の2行が表示されれば、①ファームウェアがステージ1を正しく読み込み、②ステージ1がBIOS割込でステージ2を正しくディスクから読み込み、③実行がステージ2へ正しく引き渡された、という3段階すべてが確認できたことになる。この時点でたまごOSはまだ16ビットのリアルモードで動いている。
> **定着量の目安(第二章)**: セクタ・二段ブートローダ・キャリーフラグという3つの用語と、検算1のセクタ数計算を、ステージ2のバイト数を変えて5問ほど自分で計算する練習をすると、ほぼ完全に定着すると見込まれる。
---
## 第三章: リアルモードから保護モードへ — CPUモード切替の実装(水準七〜八)
→BOOK-0340第三章では、リアルモードと保護モードという2つの動作モードの違いを概念として扱った。本章では、ステージ2の中で実際にこの切り替えを行うコードを組み立てる。
### GDT — セグメントの設計図
保護モードでは、メモリ番地の扱い方がリアルモードと変わる。CPUがどの範囲のメモリを、どんな権限で、どう扱ってよいかを定義した表を**GDT(グローバルディスクリプタテーブル、水準七: Global Descriptor Tableの略。保護モードでCPUが参照する、メモリ領域〈セグメント〉ごとの範囲と権限を定義した表)**と呼ぶ。GDTの1行1行を**セグメントディスクリプタ(水準七: GDTの1エントリ。あるセグメントの開始番地〈ベース〉・大きさ〈リミット〉・権限などをまとめて記録した情報)**と呼ぶ。
たまごOSのGDTは、もっとも単純な構成である「フラットモデル」を採用する。これは、コード用とデータ用それぞれに、メモリ全体(4GB)をまるごと1つのセグメントとして割り当ててしまう設計である。セグメントを細かく分けずに扱うことで、以降の番地計算を単純な足し算だけで済ませられる。
```
GDT(3エントリ構成):
エントリ0: ヌルディスクリプタ(すべて0。CPUの仕様上、必ず先頭に置く決まりになっている)
エントリ1: コードセグメント(ベース=0, リミット=0xFFFFF, 4KB単位×32ビット, 実行可・読み取り可)
エントリ2: データセグメント(ベース=0, リミット=0xFFFFF, 4KB単位×32ビット, 読み書き可)
```
### 検算2 — リミット値が表す実際の大きさ
コードセグメントのリミット値`0xFFFFF`(16進数)は、10進数に直すと1,048,575である。ただしこのGDTでは「1単位=4KB」という粒度(グラニュラリティ)を指定しているため、実際にカバーされる範囲は次のように計算される。
```
実際の範囲 = (リミット値 + 1) × 4,096バイト
= 1,048,576 × 4,096バイト
= 4,294,967,296バイト
= 4GB(4ギガバイト)ちょうど
```
`0xFFFFF`という一見中途半端な値が、実は4GBという非常にきりのよい大きさを表している――この「粒度をかけ合わせて実際の大きさを求める」計算は、GDTを読み書きするうえで欠かせない検算である。
### アクセスバイトの中身 — 権限と種別を1バイトに詰める
セグメントディスクリプタの中には、そのセグメントがどんな性質を持つかを1バイトにまとめた**アクセスバイト(水準七: セグメントディスクリプタの中で、そのセグメントの存在・権限・種別をまとめて表す1バイト)**が含まれている。たまごOSのフラットモデルで使う値だけに絞って、中身を要点だけ示す。
```
アクセスバイトの主なビット(コードセグメント用の例、依存最小):
プレゼントビット … このディスクリプタが有効かどうか(1=有効)
権限レベル(DPL) … 0(もっとも高い権限、カーネルが使うリング0)〜3(もっとも低い権限)
種別ビット … コードセグメントか、データセグメントか
実行可・読み取り可 … コードセグメントであれば実行可、データセグメントであれば読み書き可
```
**権限レベル(水準八: CPUがコードにどこまでの操作を許すかを、0〜3の4段階で表す仕組み。0がもっとも高い権限)**という考え方は、たまごOSでは深く踏み込まず「カーネルはすべてリング0(最高権限)で動く」という単純な前提にとどめる。実在のOSの多くは、カーネルをリング0、一般のアプリケーションをリング3で動かし、リング3のプログラムが直接ハードウェアを操作できないようにする、という権限の隔離を行っている。この隔離こそが、あるアプリケーションの不具合がOS全体やほかのアプリケーションを巻き込まずに済む、という安全性の土台になっている。たまごOSがこの隔離を持たない(すべてがリング0で動く)ことは、教育用の簡略化として本冊冒頭の安全枠でも明記したとおりであり、実在のOSに近づけるための発展課題として、→BOOK-0358h以降で扱う予定の話題である。
### A20ライン — 歴史的な制限を外す
保護モードで4GBのメモリ空間を自由に使うためには、もう一つ片付けておくべき歴史的な事情がある。**A20ライン(エーニジュうライン、水準八: 1MBを超えるメモリ番地を指定したとき、21ビット目のアドレス線が古いCPUとの互換性のために無効化されている、という歴史的な挙動、およびそれを制御する仕組み)**である。1980年代の古いCPU(Intel 8086)は物理的に1MBまでのメモリしか扱えず、それより大きい番地を指定すると自動的に折り返される(ラップアラウンドする)という挙動があった。後継のCPUは1MBを超えるメモリを扱えるようになったが、古いソフトウェアがこのラップアラウンドに依存していた可能性を考慮し、電源投入直後はA20ラインが無効化された状態(1MBを超える番地が正しく扱えない状態)で起動する、という互換性維持の仕組みが今も残っている。
```
; A20ラインを有効化する簡易な方法の一つ(高速A20法、ポート0x92を使う)
in al, 0x92
or al, 2
out 0x92, al
```
この数行を実行しておかないと、保護モードに切り替えたあとも1MBを超えるメモリへのアクセスが正しく行われず、原因の分かりにくいバグにつながる。ブートローダの初期化コードの中で、GDTの準備と並んで欠かせない一手である。
### CR0レジスタとモード切替の実行
実際にリアルモードから保護モードへ切り替える鍵は、**CR0レジスタ(水準八: CPUの動作モードなどを制御する、特別な役割を持つレジスタ〈制御レジスタ〉の一つ)**の最下位ビット(PEビット、Protection Enableビット)である。このビットを1にするだけで、CPUは保護モードの解釈規則に従うようになる。
```
; 保護モードへの切り替え(擬似アセンブリ)
cli ; 割込を一時的に禁止(初期化中に割込が来ると危険なため)
lgdt [gdt_descriptor] ; GDTの場所と大きさをCPUに教える
mov eax, cr0
or eax, 1 ; PEビット(bit0)を立てる
mov cr0, eax
jmp CODE_SEG:protected_mode_start
; ↑ 「far jump(セグメントをまたぐジャンプ)」を使うことが重要。
; CPU内部の命令先読みキューに残っているリアルモード向けの解釈を強制的に破棄し、
; 新しいCODE_SEGの規則で以降の命令を読み直させる効果がある。
[bits 32]
protected_mode_start:
mov ax, DATA_SEG
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x90000 ; 32ビット用の新しいスタックを設定
; ここまで来れば保護モード。以降はCの世界(第四章)へ引き渡す準備に入る
```
`jmp CODE_SEG:protected_mode_start`という一行のあとにセグメントレジスタをすべて`DATA_SEG`で読み直しているのは、GDTの構成を反映させ、以降のメモリアクセスがすべて新しいセグメント規則に従うようにするためである。これを忘れると、保護モードに切り替わったつもりでも、実際には古いセグメント設定の影響が残ってしまい、思わぬ番地を読み書きしてしまうことがある。
### 検算2b — コードセグメントディスクリプタを8バイトに組み立てる
セグメントディスクリプタは、実際には8バイト(64ビット)の中に、ベース・リミット・アクセスバイト・フラグをビット単位で詰め込んだ形で格納されている。たまごOSのコードセグメント(ベース=0, リミット=0xFFFFF, 4KB粒度・32ビット, 実行可・読み取り可・リング0)を例に、8バイトへの組み立て方を追ってみる。
```
8バイトの内訳(概略・依存最小):
バイト0-1: リミットの下位16ビット → 0xFFFF
バイト2-3: ベースの下位16ビット → 0x0000
バイト4: ベースの次の8ビット → 0x00
バイト5: アクセスバイト → 0x9A(存在・リング0・実行可能コード・読み取り可)
バイト6: フラグ(上位4ビット)+リミットの残り4ビット → 0xCF(4KB粒度・32ビット+リミット上位F)
バイト7: ベースの最上位8ビット → 0x00
組み立てた8バイト列: FF FF 00 00 00 9A CF 00
```
この8バイトの並びは、GDTを直接バイナリエディタで覗いたときに実際に目にする値そのものである。本冊末尾の「三つの実践解」で紹介する「ディスクイメージを目視確認する実践」を、GDTの領域に対しても応用すれば、このFF FF 00 00 00 9A CF 00という並びが、確かにディスクイメージ(あるいはカーネルの実行イメージ)の中に書き込まれているかどうかを、自分の目で検算できる。
### コラム — リアルモード・保護モード・その先のロングモード
x86系CPUの動作モードは、実は本冊で扱うリアルモードと保護モードの2つだけではない。64ビットのCPUには、保護モードよりもさらに広いメモリ空間と、64ビット幅のレジスタを扱える**ロングモード(水準八: 64ビットのx86系CPUが持つ、保護モードよりもさらに広いアドレス空間を扱える動作モード)**が用意されている。現代のパソコン向けOSの多くは、リアルモード→保護モード→ロングモードという3段階の切り替えを経て、最終的にロングモードで動作する。たまごOSは教育目的のため、32ビットの保護モードで止まる設計にしているが、ロングモードへの切り替えも「もう1段、GDTを作り直してCR4やモデル固有レジスタの設定を追加する」という、本冊で身につけた考え方の延長線上にある作業である。
### 罠 — モード切替中はスタックがまだ信用できない
第三章のコードで`cli`(割込禁止)を最初に置いているのには理由がある。GDTの設定やCR0の書き換えの途中で万一タイマ割込などが発生すると、CPUはまだ準備の整っていない状態で割込ハンドラへ飛ぼうとし、そのままシステム全体が異常終了してしまう**トリプルフォルト(水準八: CPUが例外処理そのものに失敗し、CPU自身が強制的にリセットされてしまう、もっとも重大な種類の異常)**を引き起こしやすい。「モード切替の最中は、周囲のあらゆる割込や前提を信用しない」という慎重さが、この種のコードでは特に重要になる。第十一章のトラブルシューティングで扱う「QEMUが再起動を繰り返す」という症状の多くは、実はこのトリプルフォルトが原因である。
### 動作確認点(第三章) — ここまでで動くもの
BIOS割込が使えなくなった保護モード後の世界で、画面に文字を出す最初の一手として、テキストモードの画面用メモリ領域(**0xB8000番地、水準八: VGAテキストモードで、画面に表示する文字とその色〈属性〉を直接書き込めるメモリ領域の先頭番地**)へ、1文字分のデータを直接書き込んでみる。
```
; 保護モードに入った直後、画面左上に白地に'P'の文字を直接書き込む(検証用)
mov byte [0xB8000], 'P' ; 1バイト目: 文字コード
mov byte [0xB8001], 0x0F ; 2バイト目: 属性(背景黒・文字白)
```
QEMUの画面左上に'P'という文字が表示されれば、GDTの設定・A20ラインの有効化・CR0のPEビット設定・far jumpによるセグメント再読み込みという4つの手順がすべて正しく機能し、CPUが確かに32ビット保護モードへ切り替わったことが確認できる。BIOS割込に頼らずに画面へ文字を出せた、という事実そのものが、モード切替が成功した何よりの証拠になる。
> **定着量の目安(第三章)**: GDT・セグメントディスクリプタ・A20ライン・CR0レジスタ・0xB8000という5つの用語と、検算2のリミット値からセグメントの大きさを求める計算を、値を変えて3問ほど計算する練習をすると、ほぼ完全に定着すると見込まれる。
---
## 第四章: カーネルへの橋渡し — Cコードへの遷移とリンカスクリプト(水準八)
→BOOK-0340第三章では、ブートローダが実行を引き渡す先としてカーネルという概念を扱った。本章では、アセンブリで書いたステージ2から、C言語で書いたカーネル本体へ実行を引き渡す仕組みを組み立てる。
### なぜアセンブリからCへ切り替えるのか
ここまでのコードをすべてアセンブリで書き続けることもできなくはないが、割込ハンドラやファイルシステムのような複雑な処理を、レジスタを1本ずつ手で管理しながら書くのは現実的ではない。そこで、CPUのモード切替のような「アセンブリでなければ書けない部分」だけをアセンブリに残し、それ以外の大部分をC言語に切り替える、というのがOS開発の定番の構成である。C言語の関数を呼び出す最初の一歩――関数呼び出し規約に従ってスタックを整え、`call`命令でC関数へ飛び込む――だけをアセンブリで書けばよい。
```
; アセンブリからC関数kmain()を呼び出す最小限のスタブ
[extern kmain]
call kmain
jmp $ ; kmain()が万一戻ってきても無限ループで止める
```
### コラム — 呼び出し規約という約束事
アセンブリからC関数`kmain`を呼び出す際、たまごOSでは引数を渡していないため`call kmain`の1行だけで済んでいるが、もし引数を渡すC関数を呼びたい場合には、**呼び出し規約(水準八: 関数を呼び出す側と呼び出される側が、引数をどこに置き、戻り値をどこで受け取るかについて取り決めた約束事)**を守る必要がある。古くから広く使われてきた規約の一つ(cdecl)では、引数を右から順にスタックへ積んでから`call`命令を実行する、という手順を踏む。アセンブリとCを混在させて書くときは、双方が同じ呼び出し規約を前提にしていることを確認しないと、引数の値がずれて渡ってしまう――アセンブリとCの境界をまたぐコードで起きやすい、地味だが見つけにくい不具合の典型である。たまごOSの`kmain`は引数を取らない設計にすることで、この種の食い違いをそもそも起こさないようにしている。
### フリースタンディング環境という前提
たまごOSのC言語コードは、`printf`や`malloc`のような標準ライブラリの関数を呼び出せない。なぜなら、それらの関数は本来「下にOSがある」ことを前提に作られているが、たまごOS自身がまさにこれから作ろうとしている「その下のOS」だからである。このように、標準ライブラリやOSの支援を一切当てにできない実行環境を**フリースタンディング環境(水準八: 標準ライブラリやOSのサポートなしで動くことを前提にした、プログラムの実行環境)**と呼ぶ。Cコンパイラには「標準ライブラリを使わない」「OSがある前提の初期化コードを差し込まない」という趣旨のオプションを指定し、フリースタンディングなコードとしてビルドする。
```c
/* たまごOSのカーネル最初の一歩(C風コード、依存最小) */
void kmain(void) {
const char *msg = "Kernel main() reached.";
volatile unsigned short *vga = (unsigned short *)0xB8000;
int i = 0;
while (msg[i] != '\0') {
vga[i] = (unsigned short)msg[i] | (0x0F << 8); /* 文字コード+白色属性 */
i++;
}
while (1) { } /* まだやることがないので待機 */
}
```
`printf`が使えない代わりに、第三章で確かめた0xB8000番地への直接書き込みを、この`kmain`関数の中でも同じ考え方で使っている。画面表示という一見基本的な機能さえ、フリースタンディング環境では自分で組み立てる必要がある――これがOS開発の出発点であることを、この短い関数がよく表している。
### リンカスクリプトという設計図
複数のオブジェクトファイル(アセンブリのステージ2部分、Cのkmain部分)を1つの実行イメージにまとめる作業を**リンク(水準八: 複数のオブジェクトファイルを、番地の食い違いなく1つの実行可能な形にまとめる作業)**と呼び、その作業の細かい配置ルールを指定するファイルを**リンカスクリプト(水準八: どのコードや変数を、メモリのどの番地に配置するかを指示する設計図のようなファイル)**と呼ぶ。
```
/* たまごOSのリンカスクリプト(要点のみ・依存最小の骨子) */
ENTRY(_start) /* 実行開始位置となる関数名 */
SECTIONS {
. = 0x8000; /* ステージ2が読み込まれる番地(第二章のBXレジスタと対応) */
.text : { *(.text) } /* 命令(実行される機械語)をまとめる区画 */
.data : { *(.data) } /* 初期値を持つ変数をまとめる区画 */
.bss : { *(.bss) } /* 初期値を持たない変数(0で初期化される)をまとめる区画 */
}
```
`.text`(命令)・`.data`(初期値付きの変数)・`.bss`(初期値なしの変数)という3つの区画分けは、多くのOSやアプリケーションで共通して使われる基本の構成である。リンカスクリプトの`. = 0x8000;`という一行が、ステージ1のブートローダが「0x8000番地に読み込んで実行する」と決め打ちしていた番地と、実際にリンクされる番地とを一致させる役割を果たしている。この番地がずれていると、正しくビルドできたように見えても、実行した瞬間にでたらめな番地へジャンプしてしまう――第十一章のトラブルシューティングで詳しく扱う典型的な罠の一つである。
### リンク結果をのぞいてみる
リンクが終わった実行イメージの中には、`.text`・`.data`・`.bss`の各区画が実際にどの番地に配置されたかを記録した情報が残っている。この情報を人間が読める形で表示する道具(readelf・objdumpのようなツール)を使うと、たとえば次のような一覧が得られる。
```
区画名 番地(例) 大きさ(例)
.text 0x00008000 1,536バイト (アセンブリのスタブ+Cのkmain関数の機械語)
.data 0x00008600 16バイト (初期値を持つ変数、たとえばmsg文字列)
.bss 0x00008610 4,096バイト (page_directory・first_page_tableなど、初期値なしの配列)
```
この一覧を眺めると、`.bss`区画がファイル自体には実際のデータを持たず、大きさの情報だけを記録している(実行時にゼロで埋められる)ことも確認できる。「ビルドが通ったのに動かない」という状況に出会ったとき、こうした一覧でリンカスクリプトの意図どおりに番地が割り振られているかを確認する作業は、原因の切り分けに直結する、実務的に価値の高い習慣である。
### コラム — なぜmallocがまだ使えないのか
フリースタンディング環境では`malloc`(動的にメモリを確保する標準ライブラリの関数)も使えない。`malloc`は、内部で「まだ使われていないメモリの範囲」を管理する仕組みに依存しているが、その管理の仕組み自体がOSの仕事の一部だからである――もっと言えば、たまごOS自身がこれから作ろうとしているものそのものである。次の第五章で組み立てるフレームアロケータは、いわば「たまごOS版のmallocの、さらに一段下にある土台」にあたる。標準ライブラリが当たり前のように提供してくれていた機能の1つひとつが、実は誰かが必ずどこかで実装しているのだ、という実感を持てることも、OSを実装する意義の一つである。
### 動作確認点(第四章) — ここまでで動くもの
ステージ2のアセンブリコードから、リンカスクリプトでまとめられたCの`kmain`関数を呼び出し、QEMUの画面に"Kernel main() reached."という文字列が表示されれば、①アセンブリからCへの呼び出しが正しく機能し、②リンカスクリプトの番地指定がブートローダ側の想定と一致し、③フリースタンディングなCコードから直接VGAメモリへ書き込めている、という3点が同時に確認できる。ここから先の章は、すべてこの`kmain`関数の中身を育てていく作業になる。
> **定着量の目安(第四章)**: フリースタンディング環境・リンク・リンカスクリプト・`.text`/`.data`/`.bss`という区画分けを、自分の言葉で説明できるようにするには、リンカスクリプトの3区画がそれぞれ何を格納するかを紙に書き出す練習を5例ほどこなすと、ほぼ完全に定着すると見込まれる。
---
## 第五章: メモリ管理の実装方針 — 物理メモリマップとページングの実装(水準八)
→BOOK-0340第六・七・十章では、アドレス空間・メモリアロケータ・仮想メモリとページングという概念を扱った。本章では、それらを実装方針として具体化する。
### 物理メモリマップを取得する
カーネルがメモリを管理するには、まず「実際にどれだけの物理メモリがあり、どこが使ってよい領域か」を知る必要がある。この情報は、保護モードに切り替わる前の**リアルモードのうちに**、BIOS割込(機能`INT 0x15, EAX=0xE820`)を使って取得しておく、という順序が実務上の定石になっている。保護モードに切り替えたあとはBIOS割込そのものが使えなくなるため、「メモリマップの取得は必ずステージ2の前半、保護モードへ切り替える前に済ませておく」という順序を誤ると、あとから取得し直す手段がなくなる――これも実装でつまずきやすい罠の一つである。
```
物理メモリマップの1エントリ(概念的な構造):
ベース番地(このエントリが指す領域の開始番地)
長さ(バイト数)
種別(1=使用可能、2=予約済み、それ以外=特殊用途 など)
```
たまごOSは、このメモリマップの中から「種別1(使用可能)」の領域だけを取り出し、その範囲を次に説明するフレームアロケータへ渡す。
### フレームアロケータ — ビットマップで空き地を管理する
物理メモリを、固定サイズの区画(**フレーム(水準八: 物理メモリを固定サイズ〈たまごOSでは4KB〉に区切った1区画。ページングにおける物理側の単位)**、4KBが一般的)に区切り、どのフレームが使用中でどのフレームが空いているかを管理する仕組みを**フレームアロケータ(水準八: 物理メモリをフレーム単位で確保・解放する仕組み)**と呼ぶ。たまごOSは、依存を最小限にするため、フレーム1つにつき1ビットを対応させる**ビットマップ管理(水準八: 管理対象の1区画につき1ビットを割り当て、0/1で状態〈空き/使用中〉を表す管理方式)**を採用する。
### 検算3 — ビットマップの大きさ
物理メモリが16MB(16,777,216バイト)ある場合、4KBフレームでは全部で何フレームになり、ビットマップは何バイト必要になるかを求める。
```
総フレーム数 = 16,777,216バイト ÷ 4,096バイト = 4,096フレーム
ビットマップに必要なビット数 = 4,096ビット(フレーム数と同じ)
ビットマップに必要なバイト数 = 4,096ビット ÷ 8 = 512バイト
```
16MBという、決して小さくないメモリ全体の使用状況を、たった512バイトのビットマップ1枚で管理できる――この「1ビット=1フレーム」という圧縮率の良さが、ビットマップ管理が教育用の実装でもよく採用される理由である。
### 検算3b — フレームアロケータの確保と解放をトレースする
512バイト(4,096ビット=4,096フレームぶん)のビットマップを持つたまごOSで、次の順にフレームを確保・解放する場面を考える。
```
手順:
1. alloc_frame() → 最初に見つかった空きビット(フレーム0)を1(使用中)にして返す
2. alloc_frame() → 次の空きビット(フレーム1)を1にして返す
3. free_frame(0) → フレーム0のビットを0(空き)に戻す
4. alloc_frame() → 再びビットマップの先頭から探索し、空いているフレーム0を返す
```
→BOOK-0340検算6のヒープアロケータ(フリーリスト方式)では、解放した区画の大きさが要求した大きさに満たない場合に断片化が起きた。ビットマップ方式のフレームアロケータでは、すべてのフレームが同じ大きさ(4KB)にそろっているため、この種の断片化は原理的に起こらない――「大きさをそろえてしまえば、断片化の問題自体が消える」という設計判断は、フレーム単位の粗い管理だからこそ成立する割り切りである。
### ページディレクトリとページテーブル
→BOOK-0340第十章の概念(ページング・ページ・ページテーブル)を、実際のデータ構造に落とし込む。32ビットの単純なページングでは、変換の表が2段階に分かれている。
```
2段階ページングの構造:
ページディレクトリ(水準八: 1024個のエントリを持つ、ページテーブルを指し示す上位の表)
└ 各エントリが1つの「ページテーブル」を指す
ページテーブル(1024個のエントリを持つ、物理フレームを指し示す下位の表)
└ 各エントリ(PTE)が1つの物理フレームの番地と、
プレゼントビット(そのページが実際にメモリ上にあるか)・
読み書き許可ビットなどの属性を保持する
```
たまごOSの最初のページテーブル群は、**恒等マッピング(こうとうマッピング、水準八: 仮想アドレスと物理アドレスを同じ値に対応させる、もっとも単純なページング設定)**で構築する。カーネル自身が読み込まれている番地(たとえば0x8000から数MB程度)を、仮想アドレスと物理アドレスが完全に一致するように対応づけておくことで、ページングを有効にした瞬間にカーネル自身が「自分の置かれている番地を見失ってフォールトする」という事故を避けられる。
```c
/* ページディレクトリ・ページテーブルの確保とページング有効化(C風コード、依存最小) */
static unsigned int page_directory[1024] __attribute__((aligned(4096)));
static unsigned int first_page_table[1024] __attribute__((aligned(4096)));
void init_paging(void) {
for (int i = 0; i < 1024; i++) {
/* 恒等マッピング: 仮想ページiは物理フレームiにそのまま対応させる */
first_page_table[i] = (i * 0x1000) | 3; /* 3 = プレゼント+読み書き許可 */
}
page_directory[0] = ((unsigned int)first_page_table) | 3;
for (int i = 1; i < 1024; i++) {
page_directory[i] = 0 | 2; /* 未使用エントリ(プレゼントビットは立てない) */
}
asm volatile("mov %0, %%cr3" :: "r"(page_directory));
unsigned int cr0;
asm volatile("mov %%cr0, %0" : "=r"(cr0));
cr0 |= 0x80000000; /* PGビット(bit31)を立てる */
asm volatile("mov %0, %%cr0" :: "r"(cr0));
}
```
**CR3レジスタ(水準八: 現在使用中のページディレクトリの物理番地を保持する制御レジスタ)**にページディレクトリの番地を設定し、CR0レジスタの最上位ビット(PGビット)を立てることで、ページングが有効になる。恒等マッピングを済ませたうえでこの手順を踏めば、ページング有効化の瞬間にカーネルが動き続けたまま、仮想アドレスの世界へ滑らかに移行できる。
### コラム — フレームアロケータの先にあるヒープアロケータ
→BOOK-0340第七章では、ヒープからバイト単位で必要な大きさを切り出す**メモリアロケータ**(フリーリストを使うもの)を概念として扱った。本章のフレームアロケータは、それとは扱う粒度が違う点に注意したい。フレームアロケータは「4KB単位のかたまり」を貸し出す粗い管理であるのに対し、→BOOK-0340のヒープアロケータは「100バイト」「200バイト」のような細かい単位を貸し出す。実務のカーネルでは、まずフレームアロケータで大きな4KB単位の土地をいくつか確保し、その土地の中を、→BOOK-0340のフリーリスト方式のような、より細かい粒度のアロケータで切り分けて使う、という2段構えがよく採られる。たまごOSでは依存を最小限にするため、このヒープアロケータの実装までは踏み込まず、フレーム単位の粗い管理にとどめる。
### 検算4 — 仮想アドレスからページディレクトリ・テーブルの添字を求める
仮想アドレス`0x00401000`(10進数で4,198,400)にアクセスが発生した場面を考える。2段階ページングでは、この番地を「ページディレクトリの添字(上位10ビット)」「ページテーブルの添字(次の10ビット)」「ページ内オフセット(下位12ビット)」の3つに分解する。
```
0x00401000 を2進数で見ると: 0000 0000 0100 0000 0001 0000 0000 0000
上位10ビット(ページディレクトリ添字) = 1(10進数)
次の10ビット(ページテーブル添字) = 1(10進数)
下位12ビット(オフセット) = 0
検算: ページディレクトリ添字1 × (1024×4096) + ページテーブル添字1 × 4096 + オフセット0
= 4,194,304 + 4,096 + 0 = 4,198,400 = 0x00401000 (元の番地と一致)
```
3つに分解した値を逆向きに掛け算・足し算し直すと、元の仮想アドレスにきちんと戻る――この確認は、ページディレクトリ・ページテーブルの実装にバグがないかを検算する、地味だが確実な方法である。
### コラム — ページテーブルは1段階では終わらない
本章で扱った2段階のページング(ページディレクトリ→ページテーブル)は、32ビットCPUの標準的な構成である。64ビットのロングモード(第三章コラムで触れた発展的な動作モード)では、扱えるアドレス空間が大幅に広がる代わりに、ページディレクトリのそのまた上に、さらに上位の表を2段重ねた、合計4段階の変換表が使われる。段数が増えるほど、1回のアドレス変換にたどる表の数は増えるが、その分、広大なアドレス空間の大部分を「まだ使っていない」として簡潔に表現できる、という利点がある。たまごOSの2段階構成は、この考え方の入り口にあたる、もっとも単純な形である。
### 動作確認点(第五章) — ここまでで動くもの
`init_paging()`を呼び出したあとも、カーネルがクラッシュ(リブートループやフリーズ)せずに、第四章までに実装した画面表示処理を続けられることを確認する。加えて、恒等マッピングの範囲外の仮想アドレス(たとえばプレゼントビットを立てていないページディレクトリエントリが指す番地)へわざと書き込みを試み、**ページフォールト(→BOOK-0340第十一章で定義済みの概念)**が発生して例外ハンドラ(第六章で実装するIDTの仕組み)に処理が移ることを確認できれば、ページングの有効化と保護の両方が正しく機能している証拠になる。
> **定着量の目安(第五章)**: フレーム・フレームアロケータ・ビットマップ管理・ページディレクトリ・CR3レジスタ・恒等マッピングという6つの用語と、検算3・検算4の計算を、数値を変えてそれぞれ3問ずつ解く練習をすると、ほぼ完全に定着すると見込まれる。
---
## 第六章: 割込とタイマの実装 — IDTとPICの初期化(水準八〜九)
→BOOK-0340第三章では、割込・割込ベクタテーブルという概念を扱った。本章では、実際に割込ハンドラを組み立て、時間の経過をたまごOSに教えるタイマを動かす。
### IDT — 割込ごとの行き先を記録する表
保護モードで割込が発生したときに、どの処理へ飛ぶべきかを記録した表を**IDT(割込記述子テーブル、水準八: Interrupt Descriptor Tableの略。割込番号ごとに、対応する処理〈割込ハンドラ〉の場所を記録した表)**と呼ぶ。GDTと同じように、IDTもCPUに`lidt`命令で場所を教えてから使う。
```c
/* IDTエントリの構造(C風コード、依存最小) */
struct idt_entry {
unsigned short offset_low; /* ハンドラの番地(下位16ビット) */
unsigned short selector; /* GDT上のコードセグメント選択子 */
unsigned char zero; /* 常に0 */
unsigned char type_attr; /* ゲートの種別と権限 */
unsigned short offset_high; /* ハンドラの番地(上位16ビット) */
};
struct idt_entry idt[256]; /* 256個の割込番号ぶんを確保 */
```
### PIC — 複数の割込要求を1本にまとめる調停役
キーボードやタイマなど、複数のハードウェアが同時に割込を要求してくる場合がある。これらの要求を受け取り、優先順位をつけてCPUの1本の割込線に伝える部品を**PIC(ピック、水準八: Programmable Interrupt Controllerの略。複数の割込要求信号を調停し、CPUへ順番に伝える部品。教育用の実装では8259という型番の系統がよく参照される)**と呼ぶ。PICの初期設定を怠ると重大な問題が起きる――PICの工場出荷時の初期設定では、キーボードなどのハードウェア割込の番号が、CPU自身が使う例外(ゼロ除算など)の番号(0〜31)と衝突してしまうのである。この衝突を避けるため、たまごOSはPICを次のように再設定(リマップ)する。
```
PICリマップの要点(4つの初期化コマンド、ICW1〜ICW4を送る):
1. ICW1: 初期化を開始する合図をPICへ送る
2. ICW2: ハードウェア割込の番号を、CPU例外と重ならない範囲(たまごOSでは0x20〜0x2F)へずらす
3. ICW3: マスタPICとスレーブPICの接続関係を伝える
4. ICW4: 動作モード(8086互換モード)を指定する
```
このリマップにより、キーボードの割込は例外0x00〜0x1Fと重ならない0x21番、タイマの割込は0x20番として届くようになる。
### コラム — 時刻を覚えているもう一つの部品、RTC
PITが「一定間隔でタイマ割込を刻む」役割を担うのに対し、パソコンには電源を切っても電池でバックアップされ、現在の年月日・時刻を覚え続ける**RTC(アールティーシー、水準八: Real Time Clockの略。電源が切れていても電池でバックアップされ、現在の日時を保持し続ける部品)**という別の部品が搭載されている。PITが「経過時間(何ティック進んだか)」を数えるのに対し、RTCは「今何年何月何日の何時何分か」という絶対的な日時を提供する、役割の異なる部品である。たまごOSは時刻表示の機能を持たないためRTCを扱わないが、`ls`コマンドでファイルの作成日時を表示するような発展を加える場合には、このRTCから日時を読み出す処理が新たに必要になる。
### IRQ番号の割り当て
PICに接続されているハードウェアの割込要求を、**IRQ(水準八: Interrupt ReQuestの略。ハードウェアがCPUに割込を要求するための、装置ごとに割り当てられた番号)**と呼ぶ。たまごOSが扱う主なIRQは次のとおりである。
```
主なIRQの割り当て(マスタPIC側、教育用の代表例):
IRQ0 … タイマ(PIT)。第九章のスケジューラが利用する
IRQ1 … キーボード。第十章のシェルが利用する
IRQ2 … スレーブPICとの接続専用(マスタ・スレーブのカスケード接続のために予約されている)
IRQ6 … フロッピーディスクコントローラ(たまごOSでは未使用)
IRQ14/15 … プライマリ/セカンダリのATAコントローラ(第七章で利用)
```
IRQ番号は、PICリマップ後のIDT番号(たとえばIRQ0はリマップ後0x20番)に対応づけられる。「IRQ番号」と「IDT番号(割込ベクタ番号)」は別の数え方であり、リマップの設定(ICW2)がこの2つを橋渡ししている、という対応関係を混同しないことが実装上の要点になる。
### PIT — 一定間隔でタイマ割込を発生させる
一定の間隔でタイマ割込を発生させる部品を**PIT(ピット、水準八: Programmable Interval Timerの略。指定した周波数で規則的にタイマ割込を発生させる部品。教育用の実装では8253/8254という型番の系統がよく参照される)**と呼ぶ。PITの基準周波数(約1,193,182Hz)を、望む周波数で割った値(リロード値)を設定することで、望みどおりの間隔でタイマ割込を発生させられる。
### 検算5 — 100Hzのタイマを設定するリロード値
たまごOSのタイマ割込を、1秒間に100回(100Hz)発生させたい場合のリロード値を求める。
```
リロード値 = 1,193,182 ÷ 100 = 11,931.82 …
≈ 11,931(整数に切り捨てて設定)
実際に発生する頻度 = 1,193,182 ÷ 11,931 ≈ 100.0009回/秒
```
割り切れない端数はわずかな誤差として残るが、実用上は無視できるほど小さい。この100Hz(1秒間に100回、つまり1回あたり10ミリ秒間隔)というタイマ割込の刻みが、第九章のスケジューラがタイムスライスを数える基準の単位になる。
### 検算5b — リマップ前後のIDT番号の食い違い
PICのリマップを行わなかった場合、IRQ0(タイマ)は工場出荷時の設定でIDT番号0x08に、IRQ1(キーボード)はIDT番号0x09に割り込みを送ってくる。ところがCPU自身の例外は0x00〜0x1F(たとえば0x08は「ダブルフォルト」という重大な例外)を使っており、ここにタイマの割込が重なってしまう。
```
リマップ前: IRQ0 → IDT番号0x08(CPU例外「ダブルフォルト」と衝突)
リマップ後: IRQ0 → IDT番号0x20(CPU例外と重ならない、たまごOS専用の範囲)
差分 : 0x20 − 0x08 = 0x18(このオフセット分だけ、ICW2でずらしている)
```
この衝突を放置すると、タイマ割込が発生するたびに、CPUは「ダブルフォルトが起きた」と誤解して異常系の処理に迷い込み、最終的にトリプルフォルト(第三章コラム参照)を引き起こすことがある。IDT・PICの初期化のうち、リマップの手順だけは順序も含めて特に注意深く扱うべき理由が、この番号の衝突にある。
### 割込ハンドラの骨組み
```
; タイマ割込ハンドラの骨組み(擬似アセンブリ、依存最小)
timer_handler_stub:
pushad ; すべての汎用レジスタを保存
call timer_handler_c ; C言語側の処理本体を呼ぶ
mov al, 0x20
out 0x20, al ; PICへ「処理を受け取った(EOI)」と伝える
popad ; レジスタを復元
iret ; 割込元の処理へ復帰
```
```c
/* C言語側のタイマハンドラ(依存最小) */
volatile unsigned int tick_count = 0;
void timer_handler_c(void) {
tick_count++;
/* 第九章でここにスケジューラの呼び出しを追加する */
}
```
`out 0x20, al`でPICに送る**EOI(水準八: End Of Interruptの略。割込処理が終わったことをPICへ伝える合図)**を書き忘れると、PICは「まだ処理中」と誤解し続け、以降の割込を一切通知しなくなってしまう。タイマが1回しか動かずに以降ぴたりと止まってしまう、という不具合の多くは、このEOI送信忘れが原因である――割込実装で特につまずきやすい罠の一つとして覚えておきたい。
### コラム — PICの後継、APIC
現代のパソコンの多くは、本章で扱った8259 PICの代わりに、より高機能な**APIC(エーピック、水準九: Advanced Programmable Interrupt Controllerの略。PICの後継にあたる割込コントローラで、複数のCPUコアそれぞれへ個別に割込を振り分けられる)**を搭載している。1個のCPUコアしか相手にしないPICに対し、APICは複数コアのどれに割込を届けるかを指定できるため、複数コア(マルチコア)環境での割込処理に向いている。たまごOSは単一コア・単一の実行の流れだけを前提にしているため、より単純で、QEMUを含む多くのエミュレータで確実に動作するPICを採用している。複数コアを扱うカーネルへ発展させる場合、APICの初期化が次に取り組むべき課題になる。
### 動作確認点(第六章) — ここまでで動くもの
`tick_count`の値を画面の隅に表示し続けるようにカーネルを書き換え、QEMUを起動する。数字が一定のリズムで(検算5の設定なら1秒間におよそ100ずつ)増え続けていれば、IDTの登録・PICのリマップ・PITのリロード値設定・EOIの送信という、割込まわりの初期化がひととおり正しく機能していることが確認できる。この`tick_count`は、次章以降でも「時間が経過している」ことを示す土台として使い続ける。
> **定着量の目安(第六章)**: IDT・PIC・PIT・EOIという4つの用語と、検算5のリロード値計算を、目標周波数を50Hz・200Hzなどに変えて3問ほど計算する練習をすると、ほぼ完全に定着すると見込まれる。
---
## 第七章: 簡易ファイルシステムの実装 — ディスクイメージ上の簡易FAT風構造(水準九)
→BOOK-0340第八・九章では、ブロック・inode・ディレクトリ・FATという概念を扱った。本章では、たまごOS専用の簡易FAT風ファイルシステムを、ディスクイメージの中に実際に組み立てる。
### 保護モードに入ると、もうBIOSは使えない
第二章のブートローダはBIOS割込`int 0x13`でディスクを読んでいたが、これはリアルモード専用の仕組みである。保護モードに切り替わったあとのカーネルは、BIOS割込を一切呼び出せない。そこで、カーネルは自分自身でディスクコントローラのポートを直接操作する必要がある。この方式を**ATA PIO(エーティーエー・ピーアイオー、水準九: CPUが専用の入出力ポートへ直接命令やデータを送り、ハードディスクを操作する古典的な方式)**と呼ぶ。「BIOS割込はリアルモードでしか使えない」という事実は、実装を始めて初めて痛感する典型的な罠であり、本冊があえて章をまたいで強調している理由もそこにある。
```c
/* ATA PIOでの1セクタ読み込み(C風コード、依存最小・LBA28方式) */
#define ATA_DATA 0x1F0
#define ATA_SECCOUNT 0x1F2
#define ATA_LBA_LOW 0x1F3
#define ATA_LBA_MID 0x1F4
#define ATA_LBA_HIGH 0x1F5
#define ATA_DRIVE_HEAD 0x1F6
#define ATA_COMMAND 0x1F7
void read_sector(unsigned int lba, unsigned short *buffer) {
outb(ATA_DRIVE_HEAD, 0xE0 | ((lba >> 24) & 0x0F));
outb(ATA_SECCOUNT, 1);
outb(ATA_LBA_LOW, lba & 0xFF);
outb(ATA_LBA_MID, (lba >> 8) & 0xFF);
outb(ATA_LBA_HIGH, (lba >> 16) & 0xFF);
outb(ATA_COMMAND, 0x20); /* READ SECTORSコマンド */
while ((inb(ATA_COMMAND) & 0x08) == 0) { } /* データ準備完了を待つ */
for (int i = 0; i < 256; i++) {
buffer[i] = inw(ATA_DATA); /* 1セクタ=512バイト=256ワード */
}
}
```
`(lba >> 24) & 0x0F`のような書き方で28ビット幅の番地(**LBA(水準九: Logical Block Addressingの略。ディスクの1セクタを、シリンダ・ヘッド・セクタの組ではなく、0から始まる通し番号1つで指定する方式)**)をレジスタへ分割して渡している点が、ATA PIOを読むうえでの要点である。
### ステータスレジスタの意味
先ほどのコードにあった`while ((inb(ATA_COMMAND) & 0x08) == 0) { }`は、ATAコントローラの**ステータスレジスタ(水準九: ディスクコントローラの現在の状態を、1バイトのビット列として表すレジスタ)**を繰り返し読み、特定のビットが立つのを待つ**ポーリング(水準九: 一定の条件が満たされるまで、同じ場所を繰り返し確認し続ける待ち方)**という手法である。
```
ステータスレジスタの主なビット(教育用の抜粋・依存最小):
bit7 (BSY) … コントローラが動作中(1の間は他のレジスタへ触れてはいけない)
bit3 (DRQ) … データ転送の準備ができている(1になったらデータレジスタから読み出せる)
bit0 (ERR) … 直前のコマンドでエラーが発生した
```
`0x08`は2進数で`00001000`、ちょうどDRQビット(bit3)だけを取り出すための**ビットマスク(水準九: 特定のビットだけを取り出したり判定したりするために使う、AND演算などと組み合わせる値)**である。BSYビットを確認せずにいきなりDRQを見てしまうと、コントローラがまだ準備中の生煮えのデータを読んでしまう恐れがあるため、実務のドライバではBSYが下がるのを待ってからDRQを確認する、という2段階のポーリングを行うのが定石である。たまごOSは依存を最小限にするため、この2段階を簡略化しているが、「レジスタの意味を1ビットずつ正しく読み解く」という姿勢そのものは、ハードウェアを直接操作するコードすべてに共通する土台になる。
### 簡易FAT風ディスクレイアウト
たまごOSの簡易ファイルシステムは、→BOOK-0340第九章のFAT(File Allocation Table)の考え方をそのまま採用し、ディスクイメージを次の4領域に区切る。
```
たまごOSのディスクレイアウト(先頭からの区画):
[0] ブートセクタ(第二・三章のステージ1)
[1..N] ステージ2+カーネル本体
[FAT領域] 各クラスタの「次のクラスタ」を記録する管理表(→BOOK-0340検算7と同じ発想)
[ルートディレクトリ領域] ファイル名・先頭クラスタ番号・サイズを記録する固定長エントリの並び
[データ領域] ファイルの実際の中身が、クラスタ単位で格納される
```
```c
/* ディレクトリエントリの構造(C風コード、依存最小) */
struct dir_entry {
char name[12]; /* ファイル名(末尾はヌル終端) */
unsigned int start_cluster; /* データ領域内での先頭クラスタ番号 */
unsigned int size; /* ファイルサイズ(バイト) */
};
```
### 検算6 — ファイルサイズからクラスタ数を求める
たまごOSの1クラスタを2セクタ=1,024バイトとする。3,000バイトのファイルを保存する場合、必要なクラスタ数は次のように求める。
```
必要クラスタ数 = 3,000バイト ÷ 1,024バイト = 2.93 …
→ 切り上げて3クラスタ
```
3クラスタぶんの領域を確保し、FAT風の管理表には「クラスタA→クラスタB→クラスタC→EOF(ファイルの終わりの印)」という鎖をつなぐ。最後のクラスタは1,024バイトぶん確保されているが、実際に使われるのは3,000−1,024×2=952バイトだけであり、残りの72バイトは無駄になる。この「切り上げによる無駄」は、クラスタサイズを大きくするほど目立ちやすくなる、というトレードオフも実装上覚えておきたい点である。
### ファイル読み出し関数
```c
/* ファイル名からデータを読み出す(C風コード、依存最小) */
int read_file(const char *name, unsigned char *out_buffer) {
struct dir_entry *entry = find_dir_entry(name); /* ルートディレクトリを線形探索 */
if (entry == 0) return -1; /* 見つからなければ失敗 */
unsigned int cluster = entry->start_cluster;
int offset = 0;
while (cluster != FAT_EOF) {
read_cluster(cluster, out_buffer + offset); /* クラスタの中身をコピー */
offset += CLUSTER_SIZE;
cluster = fat_table[cluster]; /* 次のクラスタへ、鎖をたどる */
}
return entry->size;
}
```
`fat_table[cluster]`を1つずつたどっていく処理は、→BOOK-0340検算7で紙の上で追いかけた「表[5]=8, 表[8]=2, 表[2]=EOF」という手順そのものを、そのままC言語のwhileループに置き換えたものである。概念で理解した鎖のたどり方が、実装では単なる配列アクセスの繰り返しになる――この対応関係を確かめることが、本章の一番の狙いである。
### ファイルの書き込み — read_fileの逆手順
第十章のシェルで`ls`・`cat`を実装するには読み出しだけで足りるが、ファイルシステムの完全な姿を理解するために、書き込み側の骨組みも押さえておく。
```c
/* ファイルを新規に書き込む(C風コード、依存最小) */
int write_file(const char *name, const unsigned char *data, unsigned int size) {
unsigned int clusters_needed = (size + CLUSTER_SIZE - 1) / CLUSTER_SIZE; /* 切り上げ除算 */
unsigned int first = alloc_free_clusters(clusters_needed); /* FAT表から空きを探す */
if (first == FAT_NONE) return -1; /* 空き容量不足 */
unsigned int cluster = first;
for (unsigned int i = 0; i < clusters_needed; i++) {
write_cluster(cluster, data + i * CLUSTER_SIZE);
unsigned int next = (i + 1 < clusters_needed) ? alloc_next_free_cluster() : FAT_EOF;
fat_table[cluster] = next;
cluster = next;
}
add_dir_entry(name, first, size); /* ルートディレクトリへ新しいエントリを追加 */
return 0;
}
```
`(size + CLUSTER_SIZE - 1) / CLUSTER_SIZE`という式は、検算6で手計算した「切り上げ」を、整数演算だけでその場で行うための定石の書き方である(割り切れない端数がある限り、分子にクラスタサイズ−1を足してから割ることで、自動的に1クラスタぶん繰り上がる)。
### コラム — たまごOSのファイルシステムに欠けているもの
→BOOK-0340第九章のコラムで触れたジャーナリング(変更操作を先にログとして記録しておき、異常終了からの復旧を容易にする仕組み)を、たまごOSの簡易ファイルシステムは実装していない。もし`write_file`の途中(たとえばFAT表を書き換えた直後、ディレクトリエントリを追加する前)でエミュレータの電源が落ちてしまったら、データ領域には中身が書き込まれているのに、ディレクトリからはそのファイルが見えない、あるいは逆にFAT表の鎖が矛盾した状態のまま残ってしまう、という不整合が起こり得る。実在のファイルシステム(NTFS・ext4・APFSなど)は、こうした「処理の途中で中断された場合の矛盾」を防ぐための工夫を、何段階も積み重ねて備えている。たまごOSがこの安全策を持たないことは、教育用の簡略化として正直に明記しておく――「動くところまでは実装したが、壊れにくくする工夫はまだ先にある」という誠実な線引きも、実装力の一部である。
### 検算6b — 大きめのファイルでのクラスタ計算
同じ1,024バイトクラスタのたまごOSで、10,500バイトのファイルを保存する場合を考える。
```
必要クラスタ数 = 10,500バイト ÷ 1,024バイト = 10.25…
→ 切り上げて11クラスタ
最後のクラスタの使用量 = 10,500 − 1,024×10 = 260バイト(残り764バイトは無駄になる)
```
ファイルが大きくなるほど、切り上げによる無駄が全体に占める割合は小さくなっていく(検算6の3,000バイトのケースでは無駄72バイトが全体の2.4%だったのに対し、このケースでは764バイトが全体の7.3%を占める――クラスタサイズと無駄の関係は必ずしも単調ではなく、端数の出方次第であることが分かる)。
### コラム — ATAのその後、AHCIとNVMe
本章で扱ったATA PIOは、ディスクとのやりとりを1バイトずつCPU自身が手を動かして行う、もっとも古典的な方式である。実在の現代的なパソコンでは、CPUを介さずディスクコントローラが直接メモリへデータを転送する**DMA(水準九: Direct Memory Accessの略。CPUを介さずに周辺機器とメモリの間で直接データを転送する仕組み)**を活用した**AHCI(水準九: Advanced Host Controller Interfaceの略。SATA接続のディスク装置を制御するための、DMAを活用した規格)**、さらに近年ではSSD向けに設計され、はるかに高い並列度でコマンドを処理できる**NVMe(水準九: Non-Volatile Memory Expressの略。フラッシュメモリ〈SSD〉の性能を引き出すために設計された、比較的新しいディスク制御規格)**が広く使われるようになっている。ATA PIOはCPUがデータの転送そのものにも付き合わなければならないため速度面では見劣りするが、レジスタの構成が単純で教育用の実装に向いている、という理由から、本冊ではあえてこの古典的な方式を選んでいる。「まず単純な方式で仕組み全体を理解し、その後で高速化の工夫を積み重ねる」という順序は、ATA PIOからAHCI・NVMeへの歴史的な発展そのものをなぞる学び方でもある。
### 動作確認点(第七章) — ここまでで動くもの
ディスクイメージのデータ領域に、ホスト側であらかじめ"welcome.txt"という名前のテキストファイル(たとえば"Hello from the disk.")を書き込んでおき、たまごOSのカーネルが起動時に`read_file("welcome.txt", buffer)`を呼び出して、その中身を画面に表示できれば、ATA PIOによるディスクアクセスとFAT風のクラスタ管理の両方が実際に機能していることが確認できる。この「ディスクから読んだ文字列を画面に表示する」という一手は、次章のプロセス管理とあわせて、第十章のシェルの`cat`コマンドの土台になる。
> **定着量の目安(第七章)**: ATA PIO・LBA・簡易FAT風ディスクレイアウトという用語と、検算6のクラスタ数計算を、ファイルサイズを変えて5問ほど解く練習をすると、ほぼ完全に定着すると見込まれる。
---
## 第八章: プロセスとPCB、コンテキストスイッチの実装(水準九)
→BOOK-0340第四・五章では、プログラムとプロセスの違い、PCB(プロセス制御ブロック)、コンテキストスイッチという概念を扱った。本章では、それらをC言語の構造体とアセンブリの手続きとして実装する。
### PCBを構造体として書く
```c
/* プロセス制御ブロック(PCB)の実装(C風コード、依存最小) */
enum proc_state { READY, RUNNING, BLOCKED };
struct pcb {
unsigned int pid; /* プロセス番号 */
unsigned int esp; /* このプロセスのスタックポインタの保存値 */
enum proc_state state; /* 実行可能・実行中・待機のいずれか */
struct pcb *next; /* レディキュー(第九章)をつなぐポインタ */
};
#define MAX_PROCS 4
struct pcb proc_table[MAX_PROCS];
struct pcb *current_proc = 0;
```
→BOOK-0340のPCBの説明では「プログラムカウンタやレジスタの値」とまとめて表現していたが、実装上は、レジスタ全体を毎回PCB構造体のフィールドとして持たせる代わりに、**そのプロセス専用のスタックへレジスタをまとめて退避し、そのスタックの先頭番地(ESP)だけをPCBに保存する**、という省力化された方法がよく使われる。たまごOSもこの方式を採用する。
### コンテキストスイッチの実装 — スタックを使った早業
```
; コンテキストスイッチの骨組み(擬似アセンブリ、依存最小)
switch_to:
; 現在のプロセスのレジスタをすべて、現在のスタックへ積む
pushad
; 現在のESPを、現在のプロセスのPCBへ保存する
mov eax, [current_proc]
mov [eax + PCB_ESP_OFFSET], esp
; 次に実行するプロセスのPCBからESPを読み込み、スタックを切り替える
mov eax, [next_proc]
mov esp, [eax + PCB_ESP_OFFSET]
mov [current_proc], eax
; 切り替え後のスタックからレジスタをすべて復元する
popad
ret ; ここでのretは、切り替え先のスタックに積まれていた
; 「戻り先番地」へジャンプすることになる
```
この`ret`命令が、コンテキストスイッチの実装でもっとも巧妙な部分である。`switch_to`が呼ばれる直前まで、CPUはあるプロセスのスタックを見ていた。ところが関数の途中でESPを書き換えてしまうため、最後の`ret`は「呼ばれたときのスタック」ではなく「切り替えた先のスタック」に積まれている番地へ戻る。つまり、`switch_to`という1つの関数が、呼ばれたときとは別のプロセスの中で終わる――この一見不思議な挙動こそが、コンテキストスイッチの正体である。
### プロセスの生成 — 最初のretのための仕込み
新しいプロセスを作るとき、まだ一度も動いたことのないプロセスであっても、上記の`switch_to`が期待する「スタックの上に戻り先番地とレジスタ一式が積まれている」という状態を、あらかじめ人工的に作っておく必要がある。
```c
/* プロセス生成(C風コード、依存最小) */
void create_process(unsigned int pid, void (*entry_point)(void), unsigned char *stack_top) {
unsigned int *sp = (unsigned int *)stack_top;
*(--sp) = (unsigned int)entry_point; /* switch_toの最後のretで飛ぶ先を仕込んでおく */
for (int i = 0; i < 8; i++) *(--sp) = 0; /* pushadぶんのダミーレジスタ値(8個) */
proc_table[pid].pid = pid;
proc_table[pid].esp = (unsigned int)sp;
proc_table[pid].state = READY;
}
```
スタックの一番上に、あらかじめ`entry_point`(プロセスの最初に実行したい関数の番地)を仕込んでおくことで、初めてこのプロセスへ`switch_to`されたときの`ret`が、まさにその`entry_point`へジャンプすることになる。まだ一度も動いていないプロセスにも「あたかも一度コンテキストスイッチされて中断していたかのような」スタックの姿を人工的に用意しておく、というこの発想は、OS実装で繰り返し出てくる巧妙な仕込みの典型例である。
### pushad/popadが保存する中身
`pushad`命令は、EAX・ECX・EDX・EBX・ESP(元の値)・EBP・ESI・EDIという8つの汎用レジスタを、一括してスタックへ積む命令である。`popad`はその逆に、スタックから8つのレジスタへ一括で復元する。1本ずつ`push eax` `push ecx`…と書く代わりにこの1命令で済ませられるため、コンテキストスイッチのようにレジスタ全体を丸ごと退避・復元したい場面で好んで使われる。ただし、EFLAGS(演算結果のフラグをまとめて保持するレジスタ)やセグメントレジスタは`pushad`の対象に含まれないため、それらも保存したい場合は別途`pushf`などを組み合わせる必要がある――たまごOSの単純な実装では割込ハンドラ側の`iret`がEFLAGSの保存・復元を自動的に行ってくれるため、`switch_to`の中では意識せずに済んでいる。
### コラム — たまごOSの「プロセス」は、実は軽いもの
本章で実装したprocess_a・process_bは、→BOOK-0340第四章の定義(プログラムが実行のためにメモリへ読み込まれ、CPU時間や独自のメモリ空間を割り当てられて実際に動いている状態)のうち、「独自のメモリ空間」の部分をまだ満たしていない。第五章で構築したページディレクトリは、たまごOS全体で1つしかなく、process_aとprocess_bはどちらも同じ恒等マッピングのアドレス空間を共有している。実在のOSでよく言う「プロセス」は、多くの場合プロセスごとに別々のページディレクトリを持ち、互いのメモリを直接のぞけないように隔離されている。本章のprocess_a・process_bは、その隔離を持たない、独立したスタックとPCBだけを持つ軽量な実行の流れ――実在のOS用語で言えば**カーネルスレッド(水準九: 1つのアドレス空間を共有しながら、独立した実行の流れとPCB相当の情報を持つ、軽量な実行単位)**に近い性質のものである、と正直に位置づけておく。プロセスごとに独立したページディレクトリを持たせる発展は、第五章のページング実装と第八章のPCB実装を組み合わせれば手が届く範囲にあり、→BOOK-0358h以降の発展課題として位置づけられる。
### 検算7c — 2回のコンテキストスイッチをトレースする
process_a(PCB.esp=0x9000)が実行中に、`switch_to`でprocess_b(PCB.esp=0x9800)へ切り替わり、次のタイマ割込でふたたびprocess_aへ戻ってくる場面を、ESPレジスタの動きだけに絞って追ってみる。
```
時点0: 実行中はprocess_a。ESPは現在のスタック上の値(0x8F40など)を指している
時点1: switch_to呼び出し。pushadで現在のレジスタをスタックへ積む → ESPが0x8F20まで下がる
時点2: current_proc(process_a)のPCB.espへ、この0x8F20を保存する
時点3: next_proc(process_b)のPCB.esp(0x9800)を読み込み、ESPへ書き込む → スタックが切り替わる
時点4: current_proc変数をprocess_bに更新する
時点5: popadでprocess_bのスタックからレジスタを復元し、retでprocess_bの続きへ戻る
(この後、process_bが50ミリ秒動いたあと、同じ手順が逆向きに実行され、
process_aのPCB.espに保存しておいた0x8F20が読み戻され、process_aは
まさにswitch_toを呼び出した直後の状態から、何事もなかったかのように実行を再開する)
```
この一連の流れの中で、CPUそのものは「自分が今どのプロセスを実行しているか」を一切意識していない、という点が興味深い。CPUはただ、ESPが指すスタックの中身を律儀に読み書きしているだけであり、「どのスタックがどのプロセスのものか」という意味づけは、すべてPCB構造体とソフトウェア側の管理によって成り立っている。
### 動作確認点(第八章) — ここまでで動くもの
画面に'A'を表示し続けるだけの無限ループ関数`process_a`と、'B'を表示し続けるだけの無限ループ関数`process_b`を、それぞれ`create_process`で登録する。カーネルの初期化処理の最後に、手動で1回だけ`switch_to`を呼び出し、実行がカーネルの初期化コードから`process_a`へ確かに移ることを確認する。この時点ではまだ自動的な切り替え(スケジューラ)はできていないが、「コンテキストスイッチという仕組みそのものが正しく機能する」ことを、1回の手動切り替えで検証できたことになる。
> **定着量の目安(第八章)**: PCB・コンテキストスイッチ・スタックポインタの退避と復元という考え方を、`switch_to`の最後の`ret`がなぜ別のプロセスへ飛ぶのかを、自分の言葉で誰かに説明できるようにする練習を3回ほど行うと、ほぼ完全に定着すると見込まれる。
---
## 第九章: スケジューラの実装 — ラウンドロビン(水準九〜十)
→BOOK-0340第五章では、たまごOSのスケジューラとして、待機中でないプロセスを順番に選ぶもっとも単純な方式を扱った。本章では、これを第六章のタイマ割込と組み合わせ、自動的にプロセスを切り替える実装へ発展させる。
### レディキュー — 順番を待つ行列
実行可能な状態にあるプロセスのPCBを、順番につないだ行列を**レディキュー(水準九: 実行可能〈READY〉状態にあるプロセスのPCBを、実行される順番につないだ行列)**と呼ぶ。たまごOSは、第八章のPCB構造体が持つ`next`ポインタを使い、単純な循環リンクリストとしてレディキューを実装する。
```c
/* レディキューへの追加と、次のプロセスの取り出し(C風コード、依存最小) */
struct pcb *ready_queue_head = 0;
void enqueue_ready(struct pcb *p) {
p->state = READY;
p->next = ready_queue_head;
ready_queue_head = p; /* 先頭に追加する簡易実装 */
}
struct pcb *dequeue_ready(void) {
struct pcb *p = ready_queue_head;
if (p != 0) ready_queue_head = p->next;
return p;
}
```
### タイムスライスをタイマ割込に接続する
→BOOK-0340第十二章の概念(ラウンドロビン・タイムスライス)を、第六章で実装した`tick_count`に接続する。
```c
/* スケジューラの本体(C風コード、依存最小) */
#define TIME_SLICE_TICKS 5 /* 5ティック=検算5の設定で約50ミリ秒 */
unsigned int slice_counter = 0;
void timer_handler_c(void) {
tick_count++;
slice_counter++;
if (slice_counter >= TIME_SLICE_TICKS) {
slice_counter = 0;
schedule(); /* タイムスライスを使い切ったので切り替える */
}
}
void schedule(void) {
struct pcb *prev = current_proc;
enqueue_ready(prev); /* 今動いていたプロセスを列の後ろへ戻す */
struct pcb *next = dequeue_ready();
if (next != prev) {
switch_to(prev, next); /* 第八章で実装したコンテキストスイッチを呼ぶ */
}
}
```
### 検算7 — タイムスライスを実時間に変換する
第六章の検算5で、たまごOSのタイマは100Hz(1ティック=10ミリ秒)に設定されていた。`TIME_SLICE_TICKS`を5に設定した場合、1回のタイムスライスが実時間で何ミリ秒に相当するかを求める。
```
1タイムスライスの実時間 = 5ティック × 10ミリ秒/ティック = 50ミリ秒
```
つまり、たまごOS上の2つのトイプロセス(process_aとprocess_b)は、およそ50ミリ秒ごとに交互に実行される。もし`TIME_SLICE_TICKS`を50に変えれば、1回のタイムスライスは500ミリ秒(0.5秒)になり、画面上でAとBの切り替わりが人間の目にもはっきり分かるほどゆっくりになる――この値を変えて再ビルドし、切り替わる速さの変化を実際に目で確かめる実践は、第十二章末の実践解の一つとしても取り上げる。
### 検算7b — レディキューが3プロセスのとき
process_a・process_bに加えて、画面に'C'を表示し続けるprocess_cを3つ目のプロセスとしてレディキューに登録した場合、`TIME_SLICE_TICKS=5`(1回50ミリ秒)の設定で、A→B→C→A→B→C→…と切り替わっていく。1周(AとBとCが1回ずつ実行される)にかかる実時間は次のように求められる。
```
1周の実時間 = 3プロセス × 50ミリ秒 = 150ミリ秒
```
レディキューに登録するプロセスの数を増やすほど、1つのプロセスに次の順番が回ってくるまでの間隔(応答性)は伸びていく――これは、たまごOSに限らず、ラウンドロビン方式全般に共通する基本的なトレードオフである。
### コラム — 実在OSのスケジューラは「公平さ」をどう測るか
現在広く使われているLinuxカーネルは、CFS(Completely Fair Scheduler、完全公平スケジューラ)と呼ばれる方式を採用している。CFSは、たまごOSのようにタイムスライスを固定の長さで区切るのではなく、それぞれのプロセスが「これまでにどれだけCPU時間を使ったか」を仮想的な時間の物差し(仮想実行時間)で管理し、常に「もっとも損をしている(仮想実行時間が短い)プロセス」を優先的に選ぶ、という発想で公平さを実現している。ラウンドロビンが「順番」という単純な物差しで公平さを表現するのに対し、CFSは「これまでの取り分」という、より精密な物差しで公平さを表現している、と言い換えられる。たまごOSのラウンドロビンは、この精密な物差しを持たない代わりに、実装がはるかに単純であり、教育目的の見通しの良さを優先した設計判断である。
### コラム — たまごOSが実装していない、もう一歩先のスケジューリング
→BOOK-0340第十二章で扱った優先度スケジューリングやマルチレベルフィードバックキューを、たまごOSのスケジューラは実装していない。本章の`schedule()`は、すべてのプロセスを完全に平等に扱う、もっとも単純なラウンドロビンにとどめてある。優先度を導入するなら、PCB構造体に優先度フィールドを追加し、`dequeue_ready()`が優先度の高いプロセスを優先して選ぶように変更する、という拡張が考えられる。ただし→BOOK-0340第十二章で触れたとおり、優先度の高いプロセスばかりが選ばれ続けると、優先度の低いプロセスが**飢餓状態**に陥る恐れがあるため、優先度を上げ下げする仕組み(エージング)もあわせて必要になる――本冊はこの発展を、あえて「やらないことの一覧」として明記し、一本道の範囲を実装だけに絞り込む判断をしている。
### 動作確認点(第九章) — ここまでで動くもの
第八章のprocess_aとprocess_bをともに`enqueue_ready`でレディキューへ登録し、タイマ割込のたびに`schedule()`が自動的に呼ばれる状態でQEMUを起動する。画面に"AAAAA"と"BBBBB"がひとかたまりずつ交互に表示され続ければ、タイマ割込からスケジューラが呼ばれ、レディキューを介して自動的にコンテキストスイッチが発生している、というラウンドロビンスケジューラの一連の流れが正しく機能していることが確認できる。これは、たまごOSが初めて「人手を介さずに複数の処理を並行して進める」ことに成功した瞬間である。
> **定着量の目安(第九章)**: レディキュー・タイムスライスの実装(タイマ割込との接続)という考え方と、検算7の実時間変換を、`TIME_SLICE_TICKS`の値を変えて3問ほど計算する練習をすると、ほぼ完全に定着すると見込まれる。
---
## 第十章: 簡易シェルの実装 — 一本道の総仕上げ(水準十)
ここまでの章で、ブートローダ・保護モード・メモリ管理・割込とタイマ・簡易ファイルシステム・スケジューラという一本道の部品がすべてそろった。本章では、それらを1つにまとめ上げ、キーボードから打ち込んだコマンドに応答する**シェル(水準十: ユーザーが打ち込んだコマンドを受け取り、解釈して実行する、対話的なプログラム)**を実装する。
### キーボード入力を受け取る循環バッファ
第六章で扱った割込の仕組みを、キーボード(IRQ1)にも適用する。キーボードが押されるたびに発生する割込ハンドラは、押されたキーに対応する**スキャンコード(水準九: キーボードのキーが押されたり離されたりしたときに、キーボードコントローラが送ってくる、キーごとに割り当てられた符号)**を、いったんバッファへためておく。
```c
/* キーボード入力用の循環バッファ(C風コード、依存最小) */
#define KBD_BUF_SIZE 64
char kbd_buffer[KBD_BUF_SIZE];
int kbd_head = 0, kbd_tail = 0;
void keyboard_handler_c(void) {
unsigned char scancode = inb(0x60);
char c = scancode_to_ascii(scancode); /* 対応表を引いて文字へ変換(押下時のみ) */
if (c != 0) {
kbd_buffer[kbd_head] = c;
kbd_head = (kbd_head + 1) % KBD_BUF_SIZE; /* 末尾まで来たら先頭へ折り返す */
}
}
```
先頭(head)と末尾(tail)の2つの添字を、配列の大きさで割った余りを使って管理し、末尾まで来たら自動的に先頭へ戻る、という**循環バッファ(水準十: 配列の末尾まで達したら、先頭に戻ってデータを詰め直す形で使い回す、リング状のバッファ)**の実装は、キーボード入力のように「いつ届くか分からないデータを、いったんためておいて後で読み出す」場面で広く使われる基本の型である。
### スキャンコードから文字への対応表
`scancode_to_ascii`が参照する対応表の一部を、具体例として示す。
```
スキャンコード(押下時)と対応する文字の例(教育用の抜粋・依存最小):
0x1E → 'a' 0x30 → 'b' 0x2E → 'c'
0x1C → Enter(改行として扱う特別な符号)
0x0E → Backspace(直前の1文字を取り消す特別な符号)
0x39 → Space(空白文字)
```
キーボードは、キーを「押した」ときと「離した」ときの両方でスキャンコードを送ってくる。離したときのスキャンコードは、押したときの値に0x80を加えた値になる、という約束事が広く使われている。`keyboard_handler_c`が押下時の符号だけを拾い、離したときの符号(0x80以上の値)を無視しているのは、この約束事を利用した単純化である。Shiftキーによる大文字・記号の切り替えのような、複数のキーの組み合わせを扱う処理は、対応表をさらに複雑にする必要があるため、たまごOSでは扱わない。
### コマンドループ(REPL)
```c
/* シェルのコマンドループ(C風コード、依存最小) */
struct command {
const char *name;
void (*handler)(int argc, char **argv);
};
void cmd_ls(int argc, char **argv) { list_dir_entries(); } /* 第七章のFSを利用 */
void cmd_cat(int argc, char **argv) { if (argc > 1) print_file(argv[1]); } /* 第七章のread_file */
void cmd_ps(int argc, char **argv) { print_proc_table(); } /* 第八・九章のPCBを利用 */
void cmd_help(int argc, char **argv) { print_command_list(); }
struct command command_table[] = {
{ "ls", cmd_ls },
{ "cat", cmd_cat },
{ "ps", cmd_ps },
{ "help", cmd_help },
};
void shell_loop(void) {
char line[128];
print_string("tamago> ");
while (1) {
read_line(line, sizeof(line)); /* kbd_bufferから1行ぶん読み取る */
char *argv[8];
int argc = split_by_space(line, argv, 8);
if (argc == 0) { print_string("tamago> "); continue; }
int found = 0;
for (int i = 0; i < sizeof(command_table) / sizeof(command_table[0]); i++) {
if (str_equal(argv[0], command_table[i].name)) {
command_table[i].handler(argc, argv);
found = 1;
break;
}
}
if (!found) print_string("unknown command\n");
print_string("tamago> ");
}
}
```
このコマンドループは、「1行読み取る→意味を解釈する→実行する→また読み取る」という繰り返しの形をしており、この構造を**REPL(レプル、水準十: Read-Eval-Print Loopの略。入力を読み〈Read〉・評価し〈Eval〉・結果を表示し〈Print〉、それを繰り返す〈Loop〉、対話的プログラムの基本構造)**と呼ぶ。`command_table`という配列に名前と処理関数の組を並べているのは、`if`文を延々と書き連ねる代わりに、コマンドを増やしたいときには配列に1行足すだけで済むようにするための工夫である。
### ここまでの全部品が、シェルの中で合流する
`cmd_ls`は第七章のディレクトリ探索を、`cmd_cat`は第七章のファイル読み出しを、`cmd_ps`は第八・九章のPCB管理とスケジューラの状態をそれぞれ呼び出している。シェル自体は目新しい理論を必要としない、これまでの一本道の部品を1つの対話的な窓口へまとめ上げる役割だけを担っている、という点が本章の一番の要点である。
### コラム — シェルという発想の来歴
コマンドを打ち込んで応答を受け取るという対話の形式は、1960年代から70年代にかけてのタイムシェアリングシステム(1台の計算機を複数人が同時に使う仕組み)の時代に、コマンドラインインタプリタとして育まれてきた。UNIX系OSで使われてきたsh(シェル)やその後継、Windowsのコマンドプロンプトやその後継であるPowerShellも、たどれば「1行読み取る→意味を解釈する→実行する→結果を返す」という、本章のREPLとまったく同じ骨組みの上に成り立っている。もちろん実在のシェルは、変数展開・パイプ(あるコマンドの出力を別のコマンドの入力に直結する仕組み)・スクリプトファイルの実行など、たまごOSには到底ない機能を何十年もかけて積み重ねてきているが、その最初の1歩――名前を1つ受け取り、対応する処理を呼ぶ――は、本章の`command_table`と本質的に同じ発想である。
### たまごOSのシェルが扱えない範囲
`split_by_space`は、単純に空白文字で区切るだけの実装であるため、たとえば`cat "long name.txt"`のように空白を含むファイル名を引用符でくくって1つの引数として扱う、という**クォーティング(水準十: コマンドライン上で、空白などの特別な意味を持つ文字を、通常の文字として扱わせるための引用符による指定)**には対応していない。同様に、あるコマンドの出力を別のコマンドへつなぐパイプや、`>`によるファイルへの出力先変更(リダイレクト)も、たまごOSのシェルには実装されていない。これらは実在のシェルが持つ代表的な発展機能であり、「動くところまでは実装したが、便利にする工夫はまだ先にある」という、第七章のファイルシステムのコラムと同じ誠実な線引きをここでも明記しておく。
### たまごOSシェルの操作例
ここまでの部品がすべてそろった状態で、実際にキーボードから打ち込む一連のやりとりを書き出してみる。
```
tamago> ls
welcome.txt 120 bytes
notes.txt 3000 bytes
tamago> cat welcome.txt
Hello from the disk.
tamago> ps
PID 0 process_a RUNNING
PID 1 process_b READY
tamago> nosuchcommand
unknown command
tamago> help
available commands: ls, cat, ps, help
```
`ls`は第七章のディレクトリ探索、`cat`は同じく第七章のファイル読み出し、`ps`は第八・九章のPCB一覧を、それぞれ`command_table`経由で呼び出しているだけであることが、この操作例からも見て取れる。存在しないコマンド`nosuchcommand`を打ち込んだときに"unknown command"とだけ返して停止せずに次のプロンプトへ戻る、というふるまいも、`shell_loop`の`found`フラグの分岐がそのまま反映された結果である。
### 動作確認点(第十章) — ここまでで動くもの
QEMUを起動し、"tamago>"というプロンプトが表示された状態で、キーボードから`ls`と打ち込むと第七章のディスクイメージに書き込んだファイル一覧が、`cat welcome.txt`と打ち込むとその中身が、`ps`と打ち込むと第九章のprocess_a・process_bの状態(READY/RUNNING)がそれぞれ画面に表示されれば、電源投入から始まったこの一本道が、ついに人間との対話が可能な「動くたまごOS」として完成したことになる。
> **定着量の目安(第十章)**: シェル・循環バッファ・REPLという3つの用語と、`command_table`の仕組みを、新しいコマンド(たとえば`clear`)を1つ自分で設計してみる練習を1回行うと、ほぼ完全に定着すると見込まれる。
---
## 第十一章: 統合ビルドと起動チェックリスト — 安全に確かめて締める(水準十)
### ビルドの全体像
第一章で示した一本道を、実際のビルド手順としてまとめ直す。
```
たまごOSの統合ビルド手順(概略・依存最小):
1. ステージ1(アセンブリ)をアセンブラで機械語へ変換 → 512バイトのバイナリ
2. ステージ2+カーネル(アセンブリ+C)をアセンブラ・Cコンパイラでそれぞれ機械語へ変換し、
第四章のリンカスクリプトに従ってリンク → 1つの実行イメージ
3. ステージ1バイナリ + ステージ2実行イメージ + 第七章の簡易ファイルシステム領域を、
決まった順序で連結し、1つのディスクイメージファイルにまとめる
4. QEMUに、そのディスクイメージを渡して起動する
```
この手順を、毎回手作業で繰り返す代わりに、一連のコマンドをまとめて自動実行する仕組み(Makefileなどのビルド自動化の仕組み)にまとめておくと、コードを書き換えるたびに素早く動作確認できるようになる。ビルド自動化そのものの詳しい書き方は、依存を増やしすぎないという本冊の方針から、要点の紹介にとどめる。
```
Makefile的なビルド規則の骨組み(概略・依存最小):
boot/stage1.bin: boot/stage1.asm … アセンブラでステージ1を512バイトへ変換
boot/stage2.bin: kernel/*.c, mm/*.c,
int/*.c, fs/*.c,
proc/*.c, shell/*.c,
boot/stage2.asm,
tools/linker.ld … Cとアセンブリをコンパイル・リンクしてステージ2を作る
disk.img: boot/stage1.bin,
boot/stage2.bin,
fs/image_data/* … 3つの材料を決まった順に連結する
run: disk.img … QEMUへdisk.imgを渡して起動する
```
それぞれの規則が「材料(左辺の依存先)」と「作り方(その行の右側の変換)」の組になっている、という考え方は、第一章のソースツリーの部屋割りとも対応している。ある章のコードだけを書き換えたときに、その章に関係する材料だけを作り直せば済むように依存関係を整理しておくと、ビルドのたびに全体をゼロから作り直す無駄を省ける。
### コラム — 家の総仕上げ検査と、たまごOSの統合ビルド
CATALOG_PC創造大全.md第d部(自作PCの設計と組立)では、家の建て方とPCの組立を同型に対応させる考え方を扱っている。この対応は、実は本章の統合ビルドにもそのまま当てはまる。家を建てるとき、基礎工事・骨組み・配線・断熱をそれぞれ個別に検査したあとで、最後に電気を通し水を流して「実際に住める状態か」を通しで確認する総仕上げ検査を行う。たまごOSの統合ビルドも同じで、第二章から第十章までの各章末で1つずつ動作確認点をクリアしてきたが、それらを別々に確認しただけでは「実際に一本道が最初から最後まで通るか」は保証されない。本章の統合ビルドは、いわばたまごOSの総仕上げ検査であり、ここで初めて「電源投入からシェルの応答まで、一度も切れ目なくつながっている」ことが確認できる。
### シリアルポート出力によるデバッグ技法
VGAテキストモードへの直接書き込みは、第三章以来ずっと使ってきた表示手段だが、画面がフリーズしてしまった不具合を調べるときには、画面表示そのものが頼りにならないことがある。そこで実務のOS開発では、**シリアルポート(水準十: 1ビットずつ順番にデータを送受信する、古くからある通信用のハードウェアインタフェース。ポート0x3F8番台がよく使われる)**を使い、ホスト側の別ウィンドウやログファイルへデバッグ情報を送り出す、という技法が広く使われている。
```c
/* シリアルポートへ1文字出力する(C風コード、依存最小・COM1ポート想定) */
#define SERIAL_PORT 0x3F8
void serial_putc(char c) {
while ((inb(SERIAL_PORT + 5) & 0x20) == 0) { } /* 送信バッファが空くまで待つ */
outb(SERIAL_PORT, c);
}
void debug_log(const char *msg) {
for (int i = 0; msg[i] != '\0'; i++) serial_putc(msg[i]);
}
```
`debug_log("paging: enabled\n")`のような1行を、第五章のページング有効化の直前・直後、第六章の割込ハンドラの入口など、疑わしい箇所へ差し込んでおくと、QEMUがフリーズする直前にどこまで処理が進んでいたかを、ホスト側のログとして時系列に残せる。画面表示に頼らないこの記録方法は、「症状が起きた瞬間には、もう画面が固まっていて何も見えない」というOS開発特有のもどかしさを解決する、実務でも定番の一手である。
### トラブルシューティングの決定木
実装を進めていると、原因がすぐには分からない不具合に必ず出会う。次の決定木は、たまごOSでよく起きる不具合の切り分けに使える。
```
症状: QEMUの画面が真っ黒のまま何も表示されない
→ ステージ1が0xAA55で終わっているか確認(第二章)
→ org 0x7C00の指定を忘れていないか確認(第二章)
→ 見つからなければ: ディスクイメージへの書き込み順序を再確認
症状: QEMUが再起動を繰り返す(リブートループ)、または「トリプルフォルト」と表示される
→ GDTの設定順序(lgdtの前にセグメントの内容が正しいか)を確認(第三章)
→ IDTが未設定のまま割込が発生していないか確認(第六章)
→ リンカスクリプトの番地(0x8000)とブートローダの読み込み先番地が一致しているか確認(第四章)
症状: ディスク読み込みで止まったまま先に進まない
→ BIOS割込を保護モード後に呼んでいないか確認(第七章の冒頭で述べた罠)
→ ATA PIOのポート番号・待機ループの条件を再確認(第七章)
症状: タイマが1回しか表示されず、以降ぴたりと止まる
→ PICへのEOI送信を忘れていないか確認(第六章)
症状: ページングを有効化した直後に画面が止まる、または文字化けする
→ 恒等マッピングの範囲がカーネル自身の置かれている番地を覆っているか確認(第五章)
→ ページディレクトリ・ページテーブルが4KB境界に正しく配置されているか確認(第五章)
症状: シェルにコマンドを打っても反応がない、または文字が表示されない
→ キーボード割込(IRQ1)がPICのマスクで無効化されたままになっていないか確認(第六・十章)
→ 循環バッファのhead/tailの初期値がずれていないか確認(第十章)
```
### 安全枠の最終確認
本冊全体を通じて、ここまでの動作確認はすべてQEMUという仮想的な環境の中だけで完結している。実機に興味を持った読者のために、あらためて条件を明記しておく。**実機でたまごOSを試す場合は、消えてよいデータしか入っていない予備機に限り、USBメモリなどの外部メディアから起動して確かめる**。ふだん使っているメインのパソコンの内蔵ディスクに直接書き込む手順は、本冊では一切扱わない。また、本冊が扱う技術はあくまで教育用トイOSの実装であり、実在するOS(Windows・Linux・macOSなど)の内部やほかの人が管理する計算機に対して、侵入・改竄・権限昇格といった操作を行う技術は一切含まれていない。
具体的に避けるべき典型例も挙げておく。ディスクイメージを実機に書き込む道具(ホスト側OSのディスク書き込みコマンド)は、指定先を1文字間違えるだけで、ふだん使っているメインの内蔵ディスクを丸ごと上書きしてしまう危険を持つ。書き込み先のデバイス名や識別子は、必ず複数回確認し、少しでも自信が持てない場合は実行しない、という慎重さを本冊は強く勧める。エミュレータの中で何度失敗しても、ホスト側のファイル1つが影響を受けるだけで済むのに対し、実機への書き込みの失敗は取り返しがつかない――この非対称性こそが、本冊がエミュレータを標準経路とする最大の理由である。
### 動作確認点(第十一章) — 一本道のゴール
第一章から第十章までのすべての部品を組み込んだディスクイメージをビルドし、QEMUで起動する。電源投入の合図から始まり、ブートローダのメッセージ、保護モードへの切り替え、"Kernel main() reached."の表示、ページングの有効化、`tick_count`の増加、ディスクからの"Hello from the disk."の読み出し、process_a/process_bの交互実行、そして最後に"tamago>"のプロンプトが現れて`ls`・`cat`・`ps`のいずれのコマンドにも正しく応答する――この一連の流れが、途中で止まることなく最後まで通ることを確認できれば、本冊の一本道は完了である。
> **定着量の目安(第十一章)**: トラブルシューティングの決定木を、実際に自分のビルドで意図的に1か所だけ間違えて(たとえばリンカスクリプトの番地をわざとずらして)みて、決定木のとおりに症状が再現するかを確かめる実践を1回行うと、実装力としての定着がもっとも確実になると見込まれる。
---
## 発展ノート — 水準十から、その先へ
本冊はたまごOSを水準七〜十の範囲で「電源投入からシェルまで動く」ところまで一本道で実装したが、実在のOSとの間には、まだいくつもの段差が残っている。ここでは、あえて実装しなかった発展課題を、正直に一覧しておく。いずれも実在OSへの侵入・改竄技術ではなく、たまごOS自身をさらに育てるための、教育的な発展方向である。
**ユーザーモードへの分離**: 第三章コラムで触れた権限レベル(リング0〜3)を使い、シェルや今後追加するプログラムをリング3(もっとも低い権限)で動かし、ハードウェアへの直接アクセスやページテーブルの書き換えをリング0のカーネルだけに許す、という隔離を導入する発展である。これにより、シェル側のプログラムに不具合があっても、カーネル全体が巻き添えで停止しにくくなる。実装するには、GDTにリング3用のコードセグメント・データセグメントを追加し、`iret`で権限レベルを切り替える仕組み、およびユーザーモードからカーネルの機能を呼び出すためのシステムコール(→BOOK-0340第三章で触れた概念)の実装が必要になる。
**ELF形式の実行ファイルローダ**: 本冊のカーネルはリンカが生成した実行イメージをディスクの決まった番地へ直接読み込む単純な方式を採ったが、実在のOSの多くは、ELF(Executable and Linkable Format)という共通の形式で保存されたプログラムを、実行時に解析して適切な番地へ配置する**ローダ(水準十: 実行ファイルを読み込み、適切なメモリ番地へ配置して実行の準備を整えるプログラム)**を備えている。ELFローダを実装できれば、たまごOSの上で複数の異なるプログラムを、あらかじめ番地を決め打ちせずに動かせるようになる。
**プロセスごとの独立したアドレス空間**: 第八章コラムで触れたとおり、たまごOSのprocess_a・process_bは1つのページディレクトリを共有している。プロセスごとに別々のページディレクトリを割り当て、`switch_to`の中でCR3レジスタも切り替えるように拡張すれば、それぞれのプロセスが互いのメモリを直接のぞけない、より実在のOSに近いプロセス分離が実現できる。
**マルチコア対応**: 第六章コラムで触れたAPICを初期化し、複数のCPUコアそれぞれで別々のプロセスを同時に実行できるようにする発展である。レディキューへの同時アクセスを守るための排他制御(複数のコアが同時に同じデータを書き換えてしまう競合を防ぐ仕組み)が新たに必要になる、油断のならない発展課題でもある。
**ネットワークスタックの入り口**: NIC(ネットワークインタフェースカード)のドライバを実装し、パケットの送受信ができるようになれば、たまごOSは他の計算機と通信できるようになる。→BOOK-0070(情報派生_ネットワーク)で扱われているOSI参照モデルの各層を、たまごOS自身の中に実装していく作業に相当する。
これらの発展課題は、いずれも本冊で組み立てた土台(GDT・IDT・ページング・PCB・ファイルシステム)の延長線上にあり、ゼロから作り直す必要はない。「動くところまでは実装したが、その先には何が待っているか」を見晴らしておくことも、一本道を読み切った読者への道しるべになる。
## 三つの実践解(§16.21) — 安全に手を動かして確かめる方法
理論と実装を、実際に手を動かして確かめる方法を三つ紹介する。いずれも、実在のOSへの侵入・改造や、他人の管理する計算機を対象にした操作を一切含まない、エミュレータの中だけで完結する安全な実践解である。
1. **一行ずつ実行して確かめる実践**: QEMUには、デバッガ(gdbなど)を接続して、CPUの実行を1命令ずつ止めながら観察できる機能がある。第三章の保護モード切替の直前・直後でCPUを止め、CR0レジスタの値やセグメントレジスタの値が確かに変化していることを、レジスタ表示機能で1つずつ確認する。理論として読んだ「PEビットが立つ」という一文が、実際のレジスタの数値としてこの目で確認できる、もっとも直接的な実践である。
2. **ディスクイメージを目視確認する実践**: ホスト側のパソコンで、ディスクイメージファイルをバイナリエディタ(hexdumpのようなツール)で開き、第二章で書き込んだ`0xAA55`の位置、第七章で構築したFAT風の管理表の値が、想定どおりのバイト列として並んでいるかを目で確認する。プログラムを実行しなくても、ファイルの中身を直接読むだけで、実装の正しさをある程度検証できることを体験する実践である。
3. **タイムスライスを変えて実測する実践**: 第九章の`TIME_SLICE_TICKS`の値を、5(約50ミリ秒)から50(約500ミリ秒)、500(約5秒)へと段階的に変えて再ビルドし、process_aとprocess_bの切り替わる速さが、検算7の計算どおりに変化することを、実際にストップウォッチなどで実測して確かめる。数式の上での計算が、実際の体感時間として一致することを確認する実践である。
---
## 章末: 統合ビルド階段図とまとめ
まず、本冊全体でたどってきた一本道を、電源投入からシェルまでの流れとしてASCII図にまとめる。
```
[電源ON] → [ステージ1がBIOSから読み込まれる(第二章)]
→ [ステージ2をディスクから読み込み、実行を引き渡す(第二章)]
→ [GDT設定・A20有効化・CR0のPEビットで保護モードへ(第三章)]
→ [Cのkmain()へ橋渡し(第四章)]
→ [ページディレクトリ構築・恒等マッピング・CR3/CR0のPGビットでページング開始(第五章)]
→ [IDT登録・PICリマップ・PIT設定で割込とタイマが動く(第六章)]
→ [ATA PIOでディスクを読み、FAT風構造でファイルを取り出す(第七章)]
→ [PCB構造体とswitch_toでコンテキストスイッチを実装(第八章)]
→ [レディキューとタイマ割込でラウンドロビンが自動的に回る(第九章)]
→ [キーボード循環バッファとコマンドループでシェルが動く(第十章)]
→ [統合ビルドし、QEMUで最初から最後まで通しで確認する(第十一章)]
```
続いて、本冊全体の歩みを、水準の階段図としてまとめる。
```
[水準十] 簡易シェルと統合ビルド
REPL・循環バッファ・全部品の合流・トラブルシューティング決定木
▲
│ ここまでの部品を1つの対話的な窓口へまとめる
[水準九] プロセス・スケジューラ・ファイルシステム
PCB構造体・switch_toのret技法・レディキュー・ATA PIO・FAT風管理表
▲
│ ディスクとプロセスという「もう一段上の資源」を扱えるようにする
[水準八] メモリ管理・割込とタイマ
ページディレクトリ・CR3/CR0のPGビット・IDT・PICリマップ・PITリロード値
▲
│ CPUの土台(第五・六章)を安全に使える形へ整える
[水準七] ブートローダ・CPUモード切替
二段ブートローダ・GDT・A20ライン・CR0のPEビット・far jump
▲
│ 何もない状態から、最初の一歩を踏み出す
安全枠: 教育用の縮小トイOS実装に限定。実機は消えてよい予備機のみ、
標準経路はエミュレータ。実在OSへの侵入・権限昇格・マルウェア作成技術は一切扱わない。
```
## まとめ — たまごOSが、ついに自分の足で立った
本冊では、→BOOK-0340で学んだ概念を土台に、空のディスクイメージにブートローダを書き込むところから、電源投入の瞬間に始まりシェルのプロンプトにたどりつくまでの一本道を、実際に動くコードを積み重ねながらたどってきた。開発環境と安全枠の確認(第一章)、ブートローダの実装(第二章)、リアルモードから保護モードへのCPUモード切替(第三章)、Cコードへの橋渡し(第四章)、物理メモリマップとページングの実装(第五章)、IDT・PIC・PITによる割込とタイマの実装(第六章)、ATA PIOと簡易FAT風構造によるファイルシステムの実装(第七章)、PCBとコンテキストスイッチの実装(第八章)、ラウンドロビンスケジューラの実装(第九章)、キーボード循環バッファとコマンドループによる簡易シェルの実装(第十章)、そして統合ビルドと起動チェックリスト(第十一章)――この十一の段を、各章末の動作確認点で1つずつ検証しながら登ってきた。
概念を理解することと、実際に動くコードを書けることの間には、決して小さくない距離がある。GDTの1バイトの並び順を間違えるだけで画面は真っ黒のままになり、PICへのEOI送信を1行忘れるだけでタイマは沈黙する。しかし、その1つひとつの罠を、動作確認点というチェックポイントで確かめながら乗り越えていけば、たった数千行のコードの中に、現在広く使われているOSの中核にある考え方――ブート・保護・メモリの区画整理・時間の管理・記録の保存・並行の実現・対話の窓口――のすべてが、確かな形で息づいていることが見えてくる。本冊で組み立てたたまごOSは、実運用のOSと比べればごく小さな存在にすぎないが、電源を入れてからシェルが応答するまでを、読者自身の手で最初から最後まで組み立てきった、という経験そのものが、次の水準(→BOOK-0358h、構造の読み方・未執筆/予約番号のみ確定)へ進むための、何より確かな土台になる。
---
## 参照文献(定番教科書・一般公開資料)
1. オペレーティングシステムの標準的な教科書群(大学初年次・情報工学系で広く使われている、プロセス・メモリ管理・ファイルシステムの実装を扱う定番の教科書)。
2. マサチューセッツ工科大学 6.828/6.S081 教材、教育用OS「xv6」のソースコードと解説資料(→BOOK-0340で紹介済み)。
3. Intel/AMD系CPU(x86系)アーキテクチャの一般公開されている技術資料(リアルモード・保護モード・GDT・IDT・ページングの仕様に関する部分)。
4. 8259 PIC・8253/8254 PITなど、周辺コントローラ類の一般公開されている技術資料。
5. QEMUプロジェクトの公式ドキュメント(エミュレータの一般的な使用方法に関する部分)。
6. OS自作を教育目的で解説する、一般に公開されている入門的な技術資料群(ブートローダ・保護モード切替・簡易ファイルシステムの実装パターンに関する部分)。
---
(本冊子は現代学問宇宙図鑑シリーズ BOOK-0358g。統合大型巻「PC創造大全」物語脊椎 第7部(OSを作る実践)。CATALOG_PC創造大全.md §2部構成表のg行に対応。→BOOK-0340(概念前篇)・→BOOK-0342(CPUの土台)・→BOOK-0358f(前部・未執筆/予約)・→BOOK-0358h(次部・未執筆/予約)。GAKUMON_UNIVERSE.md 進捗台帳を参照。)
# BOOK-0358g PC創造大全 第7部: OSを作る実践 — たまごOSを、本当に組み立てる