KDOC 202: 『30日でできる! OS自作入門』
この文書のステータス
- 作成
- 2024-09-08 貴島
- レビュー
- 2024-10-28 貴島
概要
30日でできる! OS自作入門は、OS自作を解説する本。付録コードはhide27k/haribote-osにある。
メモ
- 難しいから失敗したのではなく、簡単だよといって説明してくれる人がいなかったから失敗した。OSの作り方に限らないが、難しいと考えている人から説明してもらってもやさしくて分かりやすい説明は期待できない(まえがき)
- 1からOSを作る場合のコツ。いきなり最初からOSを作ろうと考えないこと、だという。なんとなくOSっぽく見えるものを作ればよい。OSを作ろうと考えてしまうと、あれもやらないといけない、これもやらないといけないなど、先々のことで頭がいっぱいになってしまって、うんざりしてやる気がなくなる(p4)
- OSまわりの詳しいことは、入門者には不要である。勉強しているだけで時間が過ぎ、さらに世間のOSは細かいことまであれこれよく考えられているなと思い知らされる。自分はなんと浅い考えで作ろうとしていたのだろうかと打ちのめされる。やる気がなくなる。もしくは先人の考えに圧倒されて、それらの技術を組み合わせるだけの作業になると、面白くない(p5)
- 専門用語や理屈だけ知っていても面白くない。どんなにおんぼろでも、楽しんで作れればよい。とにかく一回作ってみて、それで問題点を把握して、その問題点を克服するためにどういう方法が世間では提案されているかを知るほうが、複雑な理論を深く理解できる(p5)
- 壁にぶつかったら、そのときに必要なことだけ勉強する、でよい(p5)
- OS屋にとっては、ソースプログラムなんて結局は機械語を得るための「手段」であって目的ではない(p9)
- 本文を読むだけでなくてちゃんとプログラムを読む必要がある。本の主役は本文ではなく、付録のプログラムである、という(p10)
- メモリの0番地はBIOSがさまざまな用途で使っている。ここを勝手に使うともれなくBIOSとのけんかになり、BIOSは誤作動する。また、メモリの0xf0000番地付近はBIOSそのものがいて、使えない(p42)
- BIOSが設計された時代のCPUは、32ビットレジスタを使うことができなかった。そのため補助的な役目をするセグメントレジスタというのを作った。そしてメモリの番地を指定するときにこのセグメントレジスタも使えるようにした(p52)
MOV CX,[1234]
だと考えていたのは、じつはMOV CX[DS:1234]
という意味だった。セグメントレジスタを省略するとほとんどの場合DSを指定したものとみなされる(p53)- EQUは、C言語でいうところの
#define
のようなもので、定数を宣言するのに使う(p57) - 空の状態のディスクに対してファイルを保存したとき(p59)
- ファイル名は0x002600以降に入る
- ファイルの中身は0x004200以降に入る
- BIOSは16ビット用の機械語で書いてある(p61)
- VRAMというのはビデオメモリ、画面用のメモリのこと。それぞれの番地が画面上の画素に対応していて、画面に絵を出せる(p62)
- オブジェクトファイルは、ほかのオブジェクトファイルとリンクするための特別な機械語ファイル。C言語だけではプログラム全体を書けない限界がある。一部はアセンブラで書かなければならない。アセンブラで書いてある部分とくっつけなければならない。そのため、オブジェクトファイルは他のオブジェクトファイルとくっつけるための「のりしろ」に相当する情報を余分に持っていて、単独では機械語としては一部未完成になっている(p65)
char *p; int i = 0; *p = i & 0x0f;
[ Babel evaluation exited with code "Segmentation fault" ]
- ポインタは番地にすぎない(p83)
p[i]
は、*(p + i)
と完全に同じ意味p[i]
は、i[p]
と完全に同じ意味
- CPUにはメモリだけでなく装置(device)もつながっている。装置へ電気信号を送るのがIN命令やOUT命令。送り先の装置を区別するために、装置番号(ポート)を使う(p88)
- CLI命令は、割り込みフラグを0にする命令(p89)
- STI命令は、割り込みフラグを1にする命令(p89)
- FLAGSはキャリーフラグや割り込みフラグが詰まったレジスタ。EFLAGSはFLAGSを32ビットに拡張(Extend)したレジスタ(p89)
- ポインタ参照でカッコがある場合とない場合の違い
xsize = *binfo.scrnx;
はbinfoが指す構造体のメンバscrnxがさらにポインタであり、そのポインタを参照するxsize = (*binfo).scrnx;
はbinfoが指す構造体のメンバscrnxにアクセスする
- カッコを使わずに矢印表記で
xsize = binfo->scrnx;
と書ける - 文字列というのは、文字コードを順番にメモリに並べて、最後に
0x00
をつけたもののこと(p107) - セグメンテーションというのは、メモリを好きなように切り分けて、それぞれのブロックの最初の番地を0として扱える機能である、という。したがって、どのプログラムも
ORG 0
とすればよい。また、セグメンテーションでなくページングでも開始アドレスの問題を解決できる(p112) - セグメント1つの情報には8バイト必要である。セグメント番号は1~8191が使える。つまり8*8191=65536(64KB)が必要になる。こんなにたくさんのデータはCPUが覚えられないのでメモリに保存する。この64KBの領域をGDT(Global Descriptor table)という。メモリのどこかにセグメントの設定を並べて、その先頭の番地と有効設定個数をCPUの
GDTR
という特別なレジスタに保存する(p113) - 割り込み処理を管理するIDT(Interrupt Descriptor Table)もある。IDTは割り込み番号0から255に対して、割り込み番号123が発生したら〜関数を呼び出してね、みたいな設定の表。設定方法はGDTとよく似ている(p113)
- IDTの設定の際には、セグメントの設定が先に完了している必要がある。なのでGDTを先に設定する(p114)
- C言語ではポインタに足し算するときには、掛け算をしている。例えば8バイトの構造体へのポインタの変数に1を足すと、番地が8増える
#include "example.h"
- 割り込みを使うにはGDT、IDT、PICを設定しておく必要がある(p127)
- CPUは単独では1つしか割り込みを扱えない設計になっている。補助チップとしてPIC(programmable interrupt controller)がついている。PICは8個の割り込み信号を1つの割り込み信号にまとめる装置。入力ピンの8個の信号を監視してどれか1つでも割り込み信号が来たら出力ピンがオンになってお知らせするというもの(p127)
- PICはCPUから見ると外部装置なので、CPUからはOUT命令を使って操作する
INT 0x00~0x1f
はIRQ用に使ってはいけない。INT 0x00~0x1f
はアプリケーションがOSに対して悪さをしようとしたときにCPUが内部で自動的に発生させて、それを通じてOSに通報するためのものだから。IRQをこれに重なるように割り当てると、IRQからなのか、それともCPU保護の通知なのか、わからなくなってしまう(p130)- 割り込み処理は基本的に本来の仕事に割り込んで実行されるものだから、すぐに終わらなければならない。ほかの割り込みが受け付けられなくなる(p139)
- 2バイトのキーコードの場合、キーボード回路は一度には1バイトしか送れない。なので一度のキーの押し下げに対して2回のキーボード割り込みが発生する(p142)
- キーボード割り込みのバッファを実装する。遅いコピーを避け、使い回せるような構造を持ったFIFOキュー(p148)
- 機械語の流れを観察すると、9割以上の時間がループで消費されている。その番地のメモリは何度も読まれることになるが、キャッシュに入っているので時間のかかるメモリの読み出し命令はなくなって、機械語の実行速度が早くなる(p177)
- 486以降にしかセットされないビットがあり、そこに書き込んでみて反映するかで判定する。使えるメモリがどこまであるかを、書き込んで反映されるかで調べる(p178)
- 切り上げや切り下げの計算はANDでビット演算することで行える。割り算ではないので、計算は早い。メモリ処理を1024バイト、4096バイト単位にしておくと切り上げの計算でANDを使うことができ、メモリ確保の処理が速くなる。いっぽう、1000バイトや4000バイト単位にしていると割り算を使う必要がある。割り算はCPUの命令の中で遅く、全体としての速度に悪影響がある(p198)
- returnというのは、突き詰めて考えると呼び出し元に戻るJMP命令である(p305)
#include <stdio.h> struct a { int x, y, z; }; struct b { unsigned char *buf; }; printf("a size: %d\nb size: %d", sizeof(struct a), sizeof(struct b));
a size: 12 b size: 8
- キーボードのランプを点けたり消したりする方法(p353)
- C言語では、gotoと関数の呼び出しはまったく違う扱いであるが、アセンブラでは、CALL命令とJMP命令はほぼ同じである。そのわずかな違いは、CALL命令の場合、あとでRET命令を実行したときに戻ってこられるようにするために、スタックの戻り先の番地をPUSHしてくれること(p402)
- セグメント定義のところでアクセス権に0x60を足すと、アプリ用のセグメントという設定になる。OS用のセグメントを代入しようとするとすぐに例外を起こすようになる(p437)
- 1000で割るのは早くないので1024で割ることにする(p480)
- GDTは全部のタスクから共通に使えるセグメント設定で、LDTはあるタスクでしか有効でないセグメント設定である(p577)
- OSをうまく作るコツ。最初からOSを作ろうと考えないことに加えて、気に入らないところはあとで直せばいい、と考えること。なんならあとで作り直してもよい(p684)
- OSに明確で説明しやすい目的を持たせるのも重要である。「何を優先して何をあきらめていいか」がはっきりしていると、OSをうまく作りやすくなる(p684)
- 最初は目的なんて気にしなくてよい。むしろ作っていくうちに目的が見えてくる。目的を気にするのは、作り直し3回めからでいい(p685)
- この本で一貫しているのは、「失敗から学ぶ」ことである、という。最初は適当に作り、後になってから最初作ったものに欠陥があるから改良しようという流れになっている(p694)
- 本当は難しいことをどれだけ簡単に見せるかも本書のテーマであった、という。簡単そうに見えれば、とりあえずわかったつもりになって先を読んでくれる。先を読んでくれさえすれば、その難しいことがどれだけ役に立つかを実感できるだろう。実感できないようなことであれば、重要ではないことなので、わかったつもりのままでよい(p695)
用語集
- IDT(Interrupt Descriptor Table)
- 割り込み処理が発生したとき、対応する命令を設定する機能。キーボードの入力処理などをやる
- GDT(Global Descriptor Table)
- システムで唯一のセグメントディスクリプタテーブル
- LGDT命令
- GDTRレジスタをロードする命令
- セグメントディスクリプタ
- 論理アドレスと対になった物理アドレスとのマッピング
- CLI命令
- ハードウェア割り込みを禁止する
- STI命令
- ハードウェア割り込みを許可する
- RESB命令
- 指定したバイト数だけ0x00を書き込む
- PIC
- プログラマブル割り込みコントローラ。外部デバイスからの割り込みを管理するためのハードウェア
- OCW2レジスタ
- PICに対して割り込みの処理完了を通知するために使用されるレジスタ。特定のビットフィールドを設定することでどの割り込みラインの処理が完了したかをPICに伝える
- 例えば、
0x61
が設定されたとき、0x60
はOCW2レジスタの基本値で、割り込み受付完了を通知するコマンドを示す。0x01
はIRQライン1(キーボード割り込み)を表す
- 例えば、
- KBC
- キーボード制御回路
- IPL
- Initial Program Loader
- TRレジスタ
- 現在どのタスクを実行しているのかCPUが覚えておくためのレジスタ。Task Register
- セグメントレジスタ
- セグメントレジスタはセグメントアドレスを指定するのに使う。メモリにアクセスするとき、コンピュータはセグメントレジスタを参照する。セグメントレジスタは4つ(CS, DS, ES, SS)存在し、専用の役割がある。
- CSレジスタ
- コードセグメントレジスタ。自分自身のセグメント。このレジスタが指すセグメントアドレスは、実行中の機械語プログラムが格納されているアドレス。CPUが機械語を実行するために必要で、自動的に使用される
- DSレジスタ
- データを格納する専用のセグメントを表す。レジスタとメモリのデータ転送で参照されるセグメント
- ESレジスタ
- 基本的な役割はDSレジスタと同じ。データセグメント以外にセグメントが必要な場合に使用する
- SSレジスタ
- スタック操作に使われるセグメントアドレスを格納する
- EIPレジスタ
- 次に実行する命令がメモリのどの番地にあるのかCPUが記憶しておくためのレジスタ
- TSS
- レジスタの内容をメモリに書き込む先となるセグメント。Task Status Segment
- ESPレジスタ
- スタック領域の一番上のアドレスを示す。ESPレジスタに格納されている値(スタックポインタ)はインテルプロセッサの仕様により、「pop」命令や「push」命令を使用すると自動的に加減算される
関連
- KDOC 167: 『CPUの創り方』。低レイヤーつながり
- KDOC 140: 『自作エミュレータで学ぶx86アーキテクチャ』。低レイヤーつながり