Open26

WSL2で「30日でできる!OS自作入門」に取り組む

sassansassan

環境

WSL2
Ubuntu 22.04.4 LTS
CDドライブはないので手元やオンラインにあるものだけで頑張る
あんま関係ないけど
VSCode
Zsh

参考サイト

https://qiita.com/kamaboko123/items/f2f5b5511f717c3a55fb
これの中のリポジトリも参考にさせていただきました
https://zenn.dev/chorkaichan/scraps/28567f1358495b
https://github.com/zacfukuda/hariboteos

低レイヤに関する筆者のレベル

電子工作を趣味にしてて、PIC(マイコン)のアセンブリでフルスクラッチで色々(SDカードライブラリなど)書いたことがある。
CやC++の低レイヤの言語も割と出来て、C++は競プロ用のデバッグツールを作った経験がある。
ので、アセンブリが何かとか、コンパイルされたときにどんな感じにアセンブリになってメモリはどう使われるかとか、Makefileが何かとか、そういう基礎的なことはもともと知ってた。
(あと論理回路の仕組みは回路レベルで知ってるけど、それは今回関係ないと思う?)

ただ、所詮PICのアセンブリで、パソコンのCPUの命令セットは触ったことがないのと、言語は知っててもOSの仕組みはほとんど知らないって感じです!
それでも、僕が分からなかったところを書いていく感じなので、説明が端折られていたらごめんなさいmm

リポジトリ

https://github.com/philip82148/os-30days

sassansassan

1日目

sec. 1

大分前にやったのでこの投稿日時は全然違うのと、アプリのインストールなどで端折ってる手順があるかもしれない…。バイナリエディタのリンクは切れてた?気がするので窓の荘のリンクを使う。他にもいろいろ試してみたがこれが一番使いやすかった。

プロセッサエミュレータQEMU(キューエミュ(ム))をインストール。

sudo apt install qemu-kvm

この中のqemu-system-i386を使ってエミュレートする。

qemu-system-i386 -drive format=raw,if=floppy,file=helloos.img

普通にコマンドにファイル名を渡しても動くが、本と同じ環境を確実に再現するためにこの指定の仕方をする。まず、i386はiはインテルで、386は初めて32ビットを扱えるようになったCPU(インテルx86系列)(参考)。formatは指定しないとワーニングが出る。if(インターフェース)はディスクイメージを入れたことにするドライブ。この時点まででCPUの指定はなかったかもなのでなぜインテルx86系列なのかという話があるが、後々本が32ビットの話をしているのでこれでいいんじゃないかと。-cpuでCPUの型番が指定でき、公式ドキュメントによれば指定しなかった場合x86系列の全ての拡張が使えるようになるみたいだが、それは脆弱性があるのでやめた方がいいとのこと。でもこの本では恐らくどんなx86系でも動く命令しか使わないかなと思うのと、あまり本と違うことをして動かなかったら困るので、CPUの指定は一旦しないでおく。エミュレータはときどき失敗したように見えるが、ウィンドウの拡大とかリフレッシュさせると直ると思う。

とはいえqemu-system-i386じゃなくて他のCPUのエミュレータでも動くみたい。その互換性に関しては後で調べたい。

ディスクへ書き込みをしましょう、みたいなのがあるが、これはやってない。

sec.3

CDが読めずnaskが使えないのでnasmを使う。

sudo apt install nasm

NASMとはNetwide Assemblerの略でインテルx86を対象としたアセンブラ(Wiki)。アセンブリにディレクティブを加えることでCPUタイプを指定できる(公式ドキュメント)。こちらのCPUタイプも一旦指定しないでおく。デフォルトでは全ての拡張命令が使える。リポジトリのファイルとしてはhelloos2.nasmで、以下でコンパイル、実行。RESB命令はワーニングが出るのでTIMESに置き換えてあるのと、他にも変えてるとこがあるがそれは参考文献を参照。

nasm helloos2.nasm -o helloos2.img
qemu-system-i386 -drive format=raw,if=floppy,file=helloos2.img  

なお、VSCodeの拡張機能としてはNASM Language Supportと、CoolSpy3's Assembly Formatterを使っている。

レジスタの初期化にわざわざAXを使っているのは、レジスタによっては定数初期化の命令がないからみたいだ。試しにAX0とかにして保存すると、拡張機能に怒られると思う。

sassansassan

2日目

sec. 2

nasm helloos3.nasm -o helloos3.img
qemu-system-i386 -drive format=raw,if=floppy,file=helloos3.img

CD読み取れんのでブートセクタ以降のコードがどうアセンブリになってるかが分からん。と思ったけど、逆アセンブルしてもアセンブリにならんのでそもそも命令じゃないっぽい。まあ後でわかると信じよう。
ORG疑似命令を若干勘違いしていたのだが、この疑似命令により0x7c00に配置されるのではなく、0x7c00に配置されるのは決まっているからそれをアセンブラに教えるための命令だとわかった。このおかげで$による相対位置で指定ができる。

sec. 4

make helloos4

何やってるかはMakefile参照。ddコマンドはブロック単位でファイルをコピーするコマンド(参照)で、一つ目のddはただのコピー、二つ目はファイルサイズを1474560にしている。なお、空いた部分は0フィルされる(スパースファイル)。これを参考にstraceしてlseek関数について調べればなぜ0フィルされるかわかると思う(参考)。ただ、lseek関数ではファイルサイズが変更できないとのことだが、変更できてしまっている(statdiffを使って調べた)。これはバグなのか?もしバグだったら以下のように置き換えるといい。

# 2つ目のddはこうも書ける
dd if=/dev/zero of=$@ bs=1 count=1 seek=1474559
sassansassan

3日目

sec. 1-4

cd harib00*
make run

4つのフォルダはMakefile共通である。

sec. 5

sudo apt install mtools
cd harib00e
make run

Makefileが変更有、haribote.nasmが新規追加。
MakefileのmformatはMS-DOSフォーマット(参考1参考2)を行うものらしい。オプションは-f 1440がファイルシステムのサイズを"1440K, double-sided, 18 sectors per track, 80 cylinders"(man mformat参照) にする、-Bがブートセクタの指定、-CがMS-DOSファイルシステムの載ったディスクイメージファイルを作る、-i $@がフォーマットするファイルである。mcopyの方は最後に::がついているが、これはディスクイメージ内MS-DOSファイルシステム内のルートのパスだと思う。理由はman mtools上で::ファイル名みたいな例があるのと、mdirmmdを使って調べてみたから。他のサイトだとmformatの方でも::をつけているが、mformatの方はなくても動くので省略可能と判断した。mcopyの方で省略するとバグる。ここら辺の説明がマニュアルで乏しいのはなぜなのか😭。意味としては、Linuxのファイルシステムと、フロッピーディスクのファイルシステム(MS-DOS)が違うから、mtoolsを使って読み書きしないといけないということだろう。

sec. 6

cd harib00f
make run

ipl.nasmとharibote.nasmに変更有。

sec. 7

cd harib00g
make run

haribote.nasmに変更有。真っ黒な画面が出てくると思う。なんか最初調子悪かったが、ALの他のモードを試したりしていたら直った。

sec. 8

cd harib00h
make run

haribote.nasmに変更有。

sec. 9

cd harib00i
make run

ipl.nasm以外は変更有。
gccのコマンドはこの記事の中のリポジトリを参考にした。-nostdlibはシステムライブラリのリンクをしない、-m32は32ビット環境用のコードを生成する、-fno-pieはPIE(Position Independent Executable)にしないというオプションである。-Tはリンカスクリプトの指定で、このページを参考にしているみたい。このスクリプトの仕様は今度調べることにしよう。

sec. 10

cd harib00j
make run

nasmfunc.nasm追加、Makefile、bootpack.cに変更有。nasm-f elf32はLinux向けi386のコードを生成するオプションである。どう違うのかわからないが、Windows向けにしても動いた。MacOS向けは動かない。ここに関しても後で調べたい。

sassansassan

4日目

sec. 1

cd harib01a
make run

bootpack.cとnasmfunc.nasmに変更有。

sec. 6

cd harib01f
make run

bootpack.cとnasmfunc.nasmに変更有。

sec. 7-8

cd harib01*
make run

bootpack.cに変更有。

sassansassan

5日目

sec. 4

以降コマンドは省略する。いつも通りmake runでよい。また、今回からフォーマッタclang-formatを導入したのと、僕好みの書き方にちょいちょい変えてある。あと、unsigned charcharが混在していたが、文字('a'等)や文字列(const char *)を表す場合を除きunsigned charに統一した。なお、clang-formatはVSCodeの拡張機能があるのと、ルートの.clang-formatというファイルが設定ファイルである。ちなみにランゲージサーバーはclangdが僕のおすすめ(VSCodeの拡張機能にあるので入れてみよう)。
(ちょいちょい思うのだが時代のせい(そもそも言語自体やmake等が高機能でなかったか、ノウハウが蓄積されてなかった)なのか何なのかわからないが、可読性や書き方などこの本のコードの書き方には思うことがある。ただ、いちいち修正してたら時間がかかるので気になったところだけ変えることにする。)

sec. 5

hankakuフォルダ追加、bootpack.c、Makefileに変更有。
本ではhankakuの配列のオブジェクトをつくってリンクしているが、ここではhankaku.cというCファイルを作ってリンクさせている。

sec. 7

libフォルダ追加、bootpack.c、Makefileに変更有。
lib/my_spintf.cはこちらを参考にさせてもらった。

sec. 9

bootpack.c、nasmfunc.nasmに変更有。
なお、勘違いしてしまうがこの時点では画面上の変化はない。

sassansassan

6日目

sec. 3

ただソースファイルを分割しただけ。cフォルダの中身とMakefileのみ変更有。

sec. 6

bootpack.c、bootpack.h、dsctbl.c、int.c、nasmfunc.nasm、Makefileに変更有。

学んだこと

今度からコミットログ貼るようにしようかな。
C言語自体は元から知ってるし説明することがなくなってきた。だったら学んだことを書こうかな。
割り込みはPIC(Programable interrupt Controller)という別チップで監視されてる。割り込みがCPUに通知されるとIN,OUTの信号線でどの割り込みかを通知する。PICはマスタとスレーブの二つを使って15個の割り込みを監視する。それぞれの割り込みで呼び出される関数の番地はIDT(Interrupt Descriptor table)に登録しておく。IDTはメモリに配置し先頭アドレスを専用のレジスタに登録しておけばよい。そしてIDTの設定のためにはセグメントの設定が必要。これはなぜかというとIDTの一つ一つのデータは関数の番地(正確にはオフセット、32bit)、セグメント番号(16bit)、割り込みを表すトークン?(16bit)で構成されているから(8バイト)。セグメント番号もGDT(Global (Segment) Descriptor Table)にIDTと同じように登録する。GDTの一つのデータはセグメントの大きさ(20bit)、先頭アドレス(32bit)、セグメントの属性(書き込み・実行禁止、システム専用など、12bit)で構成され、8バイト。セグメントの数は8192個で、GDTのメモリ上のサイズは8*8192=64kB。セグメントの大きさを表すビットは20ビットしかないので、1MB以上の場合は属性のGフラグを立て、ページの数として解釈させる。その場合×4kBのサイズになる。よく言うSegmentation Faultとか、ページとかってこれのこと言ってるんだろうな。なるほどねぇ。

sassansassan

IDTの一つ一つのデータはGATEとプログラム上ではなっていたが、これは後で説明があるのか。それともただの割り込みのGATEという意味だろうか。GATEの構造体の詳しい説明がなかったが後で説明があるのか読み落としたのか。
メモリ容量の確認は今回は書き込みを行って本当に覚えているかでチェックした。その際にキャッシュがバッファにならないようにオフにした。キャッシュの管理ってOSがやってるんじゃなくてハードウェア由来なのか。

割り込み時のレジスタの内容はスタックに記録する。そのとき、PUSHADといった命令があるが、これのクロック数を調べたい。あと、これは全てのレジスタを記録できているのか?

sassansassan

7日目

sec. 1

コミットログ
ちなみにこの後編集していたりするのでフォルダのその後のログを確認すると良い。
なお、これのついでにmy_sprintf.cをリッチにしておいた。

sec. 5

コミットログ
今後言わないが、今後のコミットもこの後編集している可能性がある。

sec. 7

コミットログ

学んだこと

マウスもキーボードも複数バイトのデータを送ってくるのでFIFOにためて処理する。なお、それぞれのbyteで毎回割り込みが発生する。マウスは3byteそれぞれで押しているボタンの情報、x、yの移動量を送る。押しているボタンに関しては押したときと離したときに割り込みを発生させているようだ。送るデータには特に離したという情報はつけず、今押しているボタンの情報を送る(割り込みの発生した回数を表示させて調べた)。キーボードは2byteでキーを押しているか離したかの情報とキーコードを送る(p142)。押している間ずっと割り込みが発生するみたい(割り込みの発生した回数を表示させて調べた)。確かに何も押してないのに押したままと勘違いするみたいな誤動作を考えるとこれがいいかもだ。と思ったら、キーによって違うようだ。Ctrlキーは押したときと離したときにしか反応しない。キーコードは確かにCtrlは2byteだが、他のキーは同じByteを重ねるときもある。そうか、これで長押ししてるということを示してるのかな。また、長押しフェーズに入るまで割り込みは発生しないから、長押しの時間とかもハードウェア側で処理しているっぽい。

sassansassan

マウスやキーボードで1byteずつ割り込みが発生しているのは最新のCPUでもそうなのか?
長押しの時間とかキーのリピート速度って確かOS側で設定可能だった気がするがそれはBIOSの設定か何かか?

sassansassan

8日目

sec. 4

コミットログ

学んだこと

32ビットモードへの道はデバイスの調整や割り込みの禁止、メモリのコピー(転送)などのbootpackを実行できるだけの作業をしている。

sassansassan

メモリの転送についての理解が浅いが後でまた説明があるっぽいので放置する。

sassansassan

9日目

sec. 1

コミットログ
リファクタリングしただけ。

sec. 2

コミットログ
僕の環境(gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0)ではコンパイラの最適化は起こらなかった。qemuのメモリのデフォルト値はマニュアルに書いてある。

sec. 3

コミットログ

sec. 4

コミットログ

学んだこと

動的メモリ管理用の(空き領域の)表を配列(free[])で作った。解放時はそのメモリが前後の空き領域の間に入るようにO(N)のswap動作をするようにして入れ込む。もし前後がつながるようならつなげる。

sassansassan

確か普通はメモリはリンクリストで管理してるんじゃなかったっけ。
でもその場合順番に並べて管理するみたいなのは難しいな。そこはどうしてるんだろ。
free[]の4000個で十分らしいが本当だろうか。最近のOSはどうしてるんだろう。

sassansassan

10日目

sec. 1

コミットログ
毎回リファクタリングがだるいが、今回はリファクタリングだけじゃなく新しい関数の追加もある。
ちなみにもしかしたら次から最初からファイル分割してかけるかも。

sec. 2

コミットログ

sec. 4

コミットログ
コードが読みずらい。全部終わったら大リファクタリング祭りでもしようかしら。
CPUが早すぎで分かりづらかったが、確かに少しちらつきがなくなった気がする。

学んだこと

グラフィックはレイヤー化(この本ではSheet)して変更部分だけ四角形に更新。更新は全レイヤをイテレートする。

sassansassan

全レイヤのイテレートって遅くないのかな?そこの計算時間を後で見積もりたい。

sassansassan

11日目

sec. 2

コミットログ
Window外サポートって些細だけど興味深いな。シートで管理してるからWindow外を無限遠まで考えなくて良いという。

sec. 3

コミットログ

sec. 4

コミットログ

sec. 6

コミットログ

sec. 7

コミットログ

sec. 8

コミットログ
変更部分についてmapを更新してから書き込む。mapの計算量は変わらないが、mapを作り替えなくて良い場面(各シートのスライドがなく、不透明が透明になったりしていないとき。一枚のシートにバックグラウンドを書いてそのあと文字を書いたりしている場合など)はシート一枚分の書き込み量になる。mapを作り替える場面でも追加の書き込み量は書き換えなくて良いシート(変更があったシートより上のシート)が分かるので少ない。

学んだこと

やったこととしてはWindowを表示させてみたのと、mapなどを使い一回当たりの画面更新量を減らすかつレイヤが重なっている部分の再描画回数を減らしちらつきを抑える工夫。基本的に前回のグラフィックの続き。

sassansassan

計算量解析的に速度が怪しい。実際のPCもこうなっているのか確認。

sassansassan

12日目

sec. 2

コミットログ

sec. 4

コミットログ

sec. 7

コミットログ

学んだこと

PIT(Programmable Interval Timer)を使ってタイマーを作った。タイマーは複数同時に動かせるようになっていて、OSとはFIFOでやり取りしている。複数のタイマーは設定時に時間順にソートするようにして、先頭のものだけ見張っていればよいようにしている。

sassansassan

13日目

sec. 2

コミットログ

sec. 3

harib10cのコミットログ harib10dのコミットログ
一つ目はフォルダ名がharib10fとなっているが間違い。
恐らくエミュレータのせいだと思うが、カウントのたびに画面更新をしないと実行速度が遅くなるという問題が発生した。毎ループで割り込みの禁止・許可をしているから、その周期があまりに短すぎることが問題ではないかと疑い、空forループを入れることで実行速度を回復させることができた。

sec. 4

コミットログ バグ修正
本ほど早くならなかった。まあforループで1万も回しているから当たり前である。

sec. 5

コミットログ

sec. 6

コミットログ

学んだこと

今までのアルゴリズムの改善を行った。マウスやキーボード、タイマのFIFOを一つに統一した。その際、それぞれのコードをシフトすることでそれぞれのデータを表していて、そのためにFIFOを1データ4byteに拡張した。タイマはリンクリストに変更し、ずらし処理をなくした。(あと番兵を使ってコードを短くした。)最近リファクタリングばかりだな。

sassansassan

STIやCLI命令はCPU上で割り込みを禁止して、PICは監視を続けるイメージ。割り込みがスタックした場合、STI命令が実行された瞬間に割り込みされるものと予想しているが違うのか。でなければQEMUが割り込み禁止・許可の周期でPITのカウントが遅くなる理由(QEMU上のPITはクロック数のカウントで動いてるという前提だが)が分からない。

sassansassan

15日目

sec. 2

コミットログ バグ修正

sec. 4

コミットログ バグ修正

sec. 5

コミットログ
例の問題が発生するので空for文を追加している。

sec. 6

コミットログ

sec. 7

コミットログ

学んだこと

JMP命令で飛んだ先のセグメントが実行可能セグメントではなくTSS(Task Status Segment)だった場合、CPUは全レジスタを今行っているタスクのTSSに書き込む。また、JMP先のTSSセグメントが保存しているレジスタの情報を全てロードし、(拡張)命令ポインタ(EIP)もその保存された値になる。TR(Task Register)というレジスタがあり、今行っているTSSを覚えるレジスタだそうだ。これはタスクスイッチング時に自動的に変更されるようで、マルチタスクの初期化時にだけ明示的にセットしていた。スタックも新しく用意してESPに入れる。自動で定期的なタスクスイッチングをするためには、定期的に割り込んで、割り込みハンドラの中でJMP命令を行う(次タスクスイッチングをして戻ってくる場所は割り込みハンドラ内のその次の命令である)。なお、ここで使うJMP命令はfar-JMPといって、CS(コードセグメントレジスタ)とEIPの両方に書き込む命令である。それと、今更気づいたがこのOSやC上ではアドレスが32bitで表される。なんかおかしいとずっと思っていたがそういうことか。

sassansassan

セグメントレジスタはコード用(CS)とスタック用(SS)とデータ用(DS)があるらしい(他にもあるがおまけらしい)。p134の記述によると「C言語では『DS(データセグメント)もES(エクストラセグメント)もSS(スタックセグメント)も同じセグメントを指している』という思い込み」があって、だからSSをDSとESに代入するそうだ。セグメントが違うデータにはアクセスしない前提なのか。
また、もしそうならSS=DS=ESが保たれているC言語の途中で呼び出された割り込み処理内でわざわざ代入する必要はないと思うがどうなのか。まあこれに関してはタスクスイッチング中などは異なる可能性あると思う。

と思ったが、どうやらセグメントの意味を完全に誤解していた。セグメントは4GBにアクセスするための手段だと思っていたが、32bitモードになった今セグメントはその手段ではなく、プログラムのアドレスを0からにリセットして使えるようにするためのものらしい。なんじゃそりゃ。だったらDS=ES=SSも納得できるな。(もちろんCSは一致していない。)
今回の場合、CS=2<<3、DS=ES=SS=1<<3で、どちらのタスク(マルチタスクの)もこれらのセグメントを使っている。1<<3は4GB全体を示すセグメント、2<<3はbootpack.sysがあるセグメントである。ここら辺の整理をしなければ。読み飛ばした8日目の32bitモードへの道の記述の中にヒントがあると思われる。

タスクスイッチング中のJMP命令によるレジスタの保存のクロック数が知りたい。

TSSの登録の際、limitを103としていたが、これはなぜなのか。104byteあるのにどういう意味があるのか。

ちなみにJMP命令の正体はEIPへのMOV命令らしい。ここら辺も整理したり、使えるレジスタについても学びなおしたい。

sassansassan

16日目

sec. 1

コミットログ

sec. 2

コミットログ

sec. 3

コミットログ

sec. 4

コミットログ

sec. 5

コミットログ

学んだこと

タスクのスリープや優先順位を付けた。タスクのスリープやウェイクアップはタスクスイッチのリストからタスクを外したり追加したりすることで行う。タスクの優先順位は一回当たりの起動時間と、レベル付けによって行う。高レベルのタスクが実行されている間は低レベルのタスクはブロックされる。もし早く処理が終わったらタスクはスリープされるから、低レベルタスクも実行されるということだろう。

sassansassan

レベルの高いタスクが永遠に終わらない場合、それより下のタスクが全部停止してしまうなら、結構不都合な気がするけど、実際のOSもこうなっているのだろうか。