第1章 このテキストについて

本書は、セキュリティ・キャンプ全国大会2017の集中コースX「x86 OS自作ゼミ」のために書かれたテキストに、加筆修正を加えたものである。x86アーキテクチャを採用したコンピュータ(PC/AT互換機)用のOSを作る際に役立つ情報を載せている。何かの話題に関する体系的な説明というより、役立つ資料へのリンクを紹介することを主な目的としている。

第2章「プロセス」ではx86 CPUが持つタスク管理関連の機能やOSのプロセス管理の話題を扱う。x86の保護モードでは、TSSというタスク切り替えのためのハードウェア支援機能が備わっている。「30日でできる!OS自作入門」ではもちろんその機能を利用しているが、CPUの仕様についての説明は不足感があるのでこの章で補う。OSのレイヤでもプロセス管理は必要なので「はりぼてOS」やその他のOSの事例を紹介する。

第3章「メモリ管理」ではx86 CPUが持つメモリ管理機能やOSのメモリ管理の話題を扱う。ユーザランドでのメモリ管理にも少し触れる。CPUの機能としてはセグメンテーションやページング、OSのレイヤでは「はりぼてOS」のメモリ管理手法や、Linuxで採用されているバディシステムとスラブアロケータの話題を紹介する。メモリ管理手法の有名な方法であるK&R mallocに触れる。

第4章「ドライバ」ではOS開発に欠かせないハードウェアの制御ドライバについて扱う。フロッピーとハードディスクの読み書き、ファイルシステム、UEFIなどの話を掲載した。

第5章「開発環境」では開発環境の話題を扱う。OSを書く上で必要となるツールや、知っていると便利なものを紹介する。主にC言語やC++言語での開発に主眼を置くが、その他の言語を使う場合でも役立つ内容となっている。

第2章 プロセス

この章では、x86 CPUが持つタスクスイッチに関連する機能の話題や、OSのプロセス管理の話題を扱う。

2.1 セグメントディスクリプタ

セグメントディスクリプタは保護モードの重要な機能の1つである。メモリ領域を複数のセグメントに分割し、それぞれに意味を与えて管理することができる。

  • Intel SDM Vol3, 3.4.5 "Segment Descriptors"
  • Intel SDM Vol3, 3.5 "Table 3-2. System-Segment and Gate-Descriptor Types"
  • はじめて読む486「486のセグメントディスクリプタ」p.143

1つのセグメントは8バイト(4バイトx2)のセグメントディスクリプタを使って表現する。セグメントディスクリプタの構造を表2.1に示す。

表2.1: セグメントディスクリプタ

オフセット ビット 名前 意味
0 15:0 Limit 15:00 リミット値の下位2バイト
0 16:31 Base 15:00 ベースアドレスの下位2バイト
4 7:0 Base 23:16 ベースアドレスの16ビット目から23ビット目
4 11:8 Type セグメントの種類
4 12 S ディスクリプタの種類(0:システム、1:コードまたはデータ)
4 14:13 DPL ディスクリプタの特権レベル
4 15 P セグメントが存在するか(1:存在する)
4 19:16 Limit 19:16 リミット値の16ビット目から19ビット目
4 20 AVL システムソフトウェアが自由に利用可能か
4 21 L 64ビットコードセグメントか
4 22 D/B デフォルトオペレーションサイズ(0:16ビット、1:32ビット)
4 23 G リミット値の単位(0:1バイト、1:4KB)
4 31:24 Base 31:24 ベースアドレスの24ビット目から31ビット目

セグメントディスクリプタのSビットが0ならシステムセグメント、1ならコードまたはデータセグメントを表す。

システムセグメント

S=0(システムセグメント)の場合のTypeの意味を表2.2に示す。

表2.2: システムセグメントの種類

Type 意味(32ビットモード) 意味(IA-32eモード)
0 予約 16バイトのディスクリプタの上位8バイト
1 16ビットTSS(有効) 予約
2 LDT LDT
3 16ビットTSS(ビジー) 予約
4 16ビットコールゲート 予約
5 16ビットタスクゲート 予約
6 16ビット割込ゲート 予約
7 16ビットトラップゲート 予約
8 予約 予約
9 32ビットTSS(有効) 64ビットTSS(有効)
10 予約 予約
11 32ビットTSS(ビジー) 64ビットTSS(ビジー)
12 32ビットコールゲート 64ビットコールゲート
13 予約 予約
14 32ビット割込ゲート 64ビット割込ゲート
15 32ビットトラップゲート 64ビットトラップゲート

IA-32eモードではセグメントディスクリプタは16バイトであり、上位バイトはS=0、Type=0のディスクリプタで表現する。

コードまたはデータセグメント

S=1(コードまたはデータセグメント)の場合、Typeのビット11の値に応じてデータセグメントまたはコードセグメントを表す。データセグメントの種類を表2.3に、コードセグメントの種類を表2.4に示す。

表2.3: データセグメントの種類

11 10(E) 9(W) 8(A) 意味
0 0 0 0 読み取り専用
0 0 0 1 読み取り専用、アクセスされた
0 0 1 0 読み書き可能
0 0 1 1 読み書き可能、アクセスされた
0 1 0 0 読み取り専用、エクスパンド・ダウン
0 1 0 1 読み取り専用、エクスパンド・ダウン、アクセスされた
0 1 1 0 読み書き可能、エクスパンド・ダウン
0 1 1 1 読み書き可能、エクスパンド・ダウン、アクセスされた

S=1、Typeのビット11=0の場合、そのセグメントはデータセグメントである。データセグメントの場合、Typeフィールドの残り3ビットはそれぞれエクスパンド方向(E)、書き込み可能(W)、アクセスされたかどうか(A)という意味になる。データセグメントの種類を表2.3に示す。

E=0は普通のセグメント(エクスパンド・アップ)で、リミット値はベースアドレスに加算されてアドレス範囲の上限値を決める。E=1はエクスパンド・ダウンなセグメントで、リミット値はベースアドレスから減算されて、アドレス範囲の下限値を決める。スタックセグメントのように、サイズがアドレス空間の下位に向かって成長するようなセグメントの場合、エクスパンド・ダウンにしておくことでセグメント範囲の動的な拡大が可能となる。

表2.4: コードセグメントの種類

11 10(C) 9(R) 8(A) 意味
1 0 0 0 実行専用
1 0 0 1 実行専用、アクセスされた
1 0 1 0 実行・読み取り可能
1 0 1 1 実行・読み取り可能、アクセスされた
1 1 0 0 実行専用、コンフォーミング
1 1 0 1 実行専用、コンフォーミング、アクセスされた
1 1 1 0 実行・読み取り可能、コンフォーミング
1 1 1 1 実行・読み取り可能、コンフォーミング、アクセスされた

S=1、Typeのビット11=1の場合、そのセグメントはコードセグメントである。コードセグメントの場合、Typeフィールドの残り3ビットはそれぞれコンフォーミング(C)、読み込み可能(R)、アクセスされたかどうか(A)という意味になる。コードセグメントの種類を表2.4に示す。

C=1の場合コンフォーミングなコードセグメントとなる。特権レベルがより高いコードセグメントへ実行が移るとき、そのセグメントがコンフォーミングであればそのままの特権レベルで実行が継続する。コンフォーミングでない場合は一般保護例外が発生する(コールゲートおよびタスクゲート経由であれば例外は発生しない)。

2.2 保護モードにおけるタスクスイッチ

  • 30日でできる!OS自作入門「タスクスイッチに挑戦」p.294
  • Intel SDM Vol3, "Tsak-State Segment (TSS)"

1タスクに1つのTSSを生成。TSSを指し示すセグメントディスクリプタを、1つのTSSにつき1つディスクリプタテーブルに登録する。(ベースアドレスをそのTSSが配置されているメモリアドレスとする)

  • Intel SDM Vol2, "JMP"
  • Intel SDM Vol3, 7.3 "Task Switching"
  • 30日でできる!OS自作入門「タスクスイッチに挑戦」p.295

far JMPのセグメント値がTSS(を指すセグメントディスクリプタ)であれば、そのTSSからコードセグメント、スタックセグメント、EIPなどがロードされ、タスクが切り替わる。切り替わる際、現在のタスクのセグメント値やEIPが、TRレジスタに設定されたTSSに保存される。

2.3 セグメントレジスタの表と裏

  • Intel SDM Vol3, 2.4 "Memory-Management Registers"

GDTRとIDTRは、グローバルなディスクリプタテーブルの場所を示すレジスタ。32(64)ビットベースアドレスと16ビットリミットを設定する。

LDTR(Local Descriptor Table Register)とTR(Task Register)はどちらも同じ構造で、16ビット幅のレジスタでセグメントセレクタを設定する。LDTRとTRはともに、GDT内のセグメントディスクリプタを指す。

LDTRとTRには、レジスタへ値を読み込んだり(Load)、レジスタの値をメモリに書き出す(Store)専用の命令がある。

表2.5: LDTRとTRの読み書き命令

レジスタ 読み込み 書き出し
LDTR LLDT SLDT
TR LTR STR
  • Intel SDM Vol3, 2.4.2 "Local Descriptor Table Register (LDTR)"
  • Intel SDM Vol3, 2.4.4 "Task Register (TR)"

実は、LDTRとTRは16ビット幅のレジスタに加えて、32(64)ビットベースアドレスと16ビットリミット、属性値を保持するレジスタを持つ。それらのレジスタはプログラマが直接読み書きすることはできないので「不可視部」(hidden part)と言う。

読み込み命令(LLDTやLTR)を実行すると、16ビット幅のレジスタにセグメントセレクタが設定されると同時に、そのセレクタが指すGDT内のセグメントディスクリプタからベースアドレス、リミット、セグメント属性値が読み取られ、それらの「裏レジスタ」にコピーされる。

  • Intel SDM Vol3, 3.4.3 "Segment Registers"
  • はじめて読む486「セグメントディスクリプタキャッシュ」p.146

裏レジスタはCSやDSなどの一般のセグメントレジスタにも備わっている。Intel SDMでは、表と裏をそれぞれ「可視部」(visible part)と「不可視部」(hidden part)と表記している。不可視部は「ディスクリプタキャッシュ」とか「シャドウレジスタ」とか「裏レジスタ」と呼ばれることもある。

セグメントセレクタがセグメントレジスタの可視部に読み込まれたとき、そのセグメントセレクタで指定されたセグメントディスクリプタから、ベースアドレス、リミット、アクセス制御情報が不可視部へ自動的に読み込まれる。

第4章 ドライバ

この章では、ハードウェアを制御するドライバの話題を扱う。

4.1 フロッピー読み書き

フロッピーの読み書きには主に2つの方法がある。BIOSの機能を使う方法とFDCを自分で制御する方法である。

  • 30日でできる!OS自作入門「さあ本当のIPLを作ろう」p.48

「30日でできる!OS自作入門」ではBIOSによるフロッピーの読み込みを解説している。

BIOSで読み込むには、CPU動作モードが実アドレスモードでなければならない。しかし、この本が題材とする「はりぼてOS」は保護モードのOSであるので、基本的にはBIOSは使えない(仮想8086モードにすれば使うことができるが)。保護モードでフロッピーの読み書きをするには、FDCを制御して自力で読み書きするのが基本である。「はりぼてOS」は、起動時にOSの動作に必要な分をすべて読み込んでしまうことで、保護モードに移行した後でのフロッピー読み書きを不要としている。

FDCを制御するには、FDCやDMACをI/O命令により制御する必要がある。このページには、制御に必要な最低限のレジスタの解説や制御方法が紹介されている。

Intelが開発したIntel 82078というフロッピーディスクコントローラの仕様書へのリンクがある。現在、Intel公式サイトからは仕様書がダウンロードできなくなっているが、QEMU wikiにコピーが置いてあるので読むことはできる。単体のチップであるIntel 82078の仕様書だが、ほぼ同じ機能が現代のIntelチップセットに搭載されていると思われる。各種の制御コマンドの値などはこの仕様書がとても参考になる。

haribote/fdc.cにFDCの制御コードがある。コードを簡単に説明する。

elf_hariboteのFDC制御コードの説明

まず全体的な構成として、FDC制御用のタスク(fdc_task)とFIFOキューが2つある。

1つ目のキュー(fdc_task->fifo)は、外部のタスクからFDC制御タスクへ要求を送るためのものである。外部のタスクはfdc_push_request関数を通じてフロッピーを読み書きする。外部のタスクはFDCを直接制御してはならず、このキューを通じて間接的に制御する。

2つ目のキュー(fdc_interrupt_notifier)は、FDCからの割込みを待つためのキューである。FDCの制御コマンドは時間がかかるものが多く、基本的には制御コマンドの結果は割込みで受け付けるように作るのが一般的だ。

fdc.cで定義されているfdc_task_main関数がタスク本体の関数である。この関数は永久ループを行い、fdc_task->fifoに要求オブジェクトがpushされるのを待つ。要求オブジェクトがあれば受信し、fdc_process_req関数に処理させる。

fdc_process_req関数では要求タイプ(読み込み・書き込み)に応じて、適切なコマンドをFDCに送り、割込みを待ち、結果を要求オブジェクトに書き戻す。処理が終わった後、要求オブジェクトにFIFOキューが設定されていれば、そのキューに要求オブジェクトをpushする。そのキューがタスクに関連付いている(taskフィールドがNULLでない)場合、キューにデータをpushするとタスクがsleepから目覚め、再び実行可能状態となる。フロッピー読み書きの要求をFDC制御タスクに送信した後、要求が完了するまで送信者はsleepしていても良いということである。

試し読みはここまでです。
この続きは、製品版でお楽しみください。