30日でできる! OS自作入門の1~18日目

この記事は圧倒的令和ッ!!ぴょこりんクラスタ Advent Calendar 2019のために書いたものです。 ちなみにこのAdvent Calendarが何なのかについては、主催者による紹介記事を見てください。

はじめに

コンピュータをかじったことがある人間であれば、誰しも1度位はOSとかコンパイラを作ってみたいと 思ったことがあるだろう。ただ、実際に作る人はそんなに多くない。 自分自身はと言うと、時折発作のように思い出しては今更かなと思って何もしないクズ系ワナビーである。 そんな折、たまたまAmazonで"30日でできる! OS自作入門"が半額セールで買えたので、 これをひととおりやってみることにした。つまり、本記事はこれの作業記録である。

環境用意

あまりがんばらなくても用意できるものでやるという方針のもと、以下とした。 基本的な作業は、Virtualbox上のManjaro Linuxでやる。

以下、x日目は、 本の上での日数であり、実際の作業時間とは一致しない。

また、リポジトリは以下である。 github.com

1日目

Vimバイナリエディタとして使おうとして拾った設定がうまく動かずじたばたした。 拾ったやつはBufWritePreの%!xxd -r後に| endifが書いてあったんだけど、 それだと!の引数として解釈されるようで正しく動かなかった。 全部書いて:wqしたら、Command Not Foundだけ書いてあるファイルが出てきてつらくなった。

以下は正しく動く版のもの。

"バイナリ編集(xxd)モード(vim -b での起動、もしくは *.binファイルを開くと発動)
augroup BinaryXXD
    autocmd!
    autocmd BufReadPre  *.bin let &binary =1
    autocmd BufReadPost * if &binary | silent %!xxd -g 1
    autocmd BufReadPost * set ft=xxd | endif
    autocmd BufWritePre * if &binary | %!xxd -r
    autocmd BufWritePre * endif
    autocmd BufWritePost * if &binary | silent %!xxd -g 1
    autocmd BufWritePost * set nomod | endif
augroup END

バイナリファイルはとりあえず全部書いてみた。 バイナリファイルを作成するアセンブリコードについては、 バイナリファイルを読んでアセンブリコードを吐き出すスクリプトを作り、 写経したことにした。本に従って、アセンブラIntel形式で書き出した。

作ったイメージファイルは、Virtualboxに仮想フロッピーディスクとして認識させたら普通に動いた。 もっと苦労すると思ったけど、案外あっけない、hello world

f:id:nbisco:20191203223039p:plain
1日目

2~3日目

イメージの作り方がわからない。しかし、同じことをやっている人はたくさんいて、 その中でもきちんとログを残してくれた偉大な先人Makefile等を参考になんとか実施。 本では、スクリプトの中身の解説はまだなので、理解する前に写した。 画面はまっくらだけど、hlt命令を実行しているだけなので、これで正しい。

f:id:nbisco:20191203223138p:plain
2~3日目

4日目

Win98みたいなツールバーが出せるようになった。結構あっという間に画面の大枠が出来てしまった。

f:id:nbisco:20191203223219p:plain
4日目

ところで、なぜ第1引数がESP + 4なのか?それは単純でx86のCalling Conventionによるものであった。 32bitに関して言えば、ESP + 0は戻り先アドレス、ESP + 4は最左の引数、ESP + 8は次・・・みたいな感じで積まれるようである。 ここを参考にした。

5日目

フォントを手作りして、何か表示する回。 途中、sprintfがなくて困る。gcc-multilibを入れてもstaticにリンクできなかったので、 ここからsprintfを拝借した。 最初は小さいlibc(muslとか)をリンクすればいいかなと思ったけど、 ほしいのはsprintfだけだったので止めた。

また、手作りフォントについては、ここを参考に、pythonスクリプトをえいやで作ってなんとかした。

真ん中の数値は、緑画面の中央点の座標を表している。

f:id:nbisco:20191203223330p:plain
5日目

6日目

キーボードからの割り込みを拾ったよ、ということを表示する回。内容ではまるというよりは、Makefileではまる。 haribote.sysをddする際、countが正しく指定されていないことが問題で、原因はワンライナーの計算方法だった。 Makefileの変数内の$マークをエスケープ($を$$に置き換え)して、0.5足して、小数点以下切り捨てることで対応。

f:id:nbisco:20191203224035p:plain
6日目

7日目

キーボード入力とマウス入力を拾って、単にそのまま生の値を表示する回。 矢印は動かないし、自由に文字を打てないが、入力を拾えるようになったのは嬉しい。

定数をマクロに置き換えた際に写経ミス(8で割るのを忘れた)発生により、動作せずにしばらくはまった。 デバッガがないせいで原因がよくわからず、発見まで時間がかかった。デバッガがないのは本当につらい。 これ以外は特に問題なかった。

f:id:nbisco:20191203224532p:plain
7日目

8日目

マウス入力を拾って、どのボタンが押されたか、どちら向きに移動したか、矢印が今どこに いるかを表示する回。

マウスの矢印が動き始めたので感慨深い。拝借したsprintf関数が負の数に対応していなかったので修正した。 ふと思ったんだけど、BIOSでUSBキーボードが使えるということは、BIOSそのものがUSBドライバとキーボードドライバを持ってるってことなんだよな。OSのサポートなしでドライバ作るなんてBIOSメーカも大変だなあ・・・

f:id:nbisco:20191203224858p:plain
8日目

9日目

メモリ管理ができるようにして、メモリ総容量と現使用量を表示する回。 ただ、矢印がメモリ容量の文字のところに重なると、文字が消えてしまう。。。

memtest_subについては、Cでもvolatileをつければ大丈夫。ただし、今使っている gcc 7.4.0では、xor命令ではなくnot命令として出力されている様子。

f:id:nbisco:20191203225200p:plain
9日目

10日目

矢印が文字の上を通っても消えなくなるように、描画方法を工夫する回。

描画の重ね合わせ処理でメモリ管理関数を使ったが、特に問題なし(写経しているだけなので当然といえば当然なんだけど)。 マウスの矢印を動かしても、下の文字が消えないというのは結構感動する。何より、 動いているものをグラフィカルに見られるというのはよいもので、やったかいがあると思える。

f:id:nbisco:20191203225615p:plain
10日目

11日目

Windows98を思い出すウインドウを作り、その中でカウンタを表示する回。

カウンタにリーディング0がほしかったので、sprintf関数を修正した。 難しいところはなかったけど、しょうもない写しミスで苦労した。 手書きで写しているから、こういうミスがあると大変なんだよなあ・・・ ウインドウといっても、まだ動かしたり閉じたりはできないが、ずいぶん形になってきた。

f:id:nbisco:20191203225818p:plain
11日目

12日目

タイマー割り込みを使って、カーソルの点滅や、時間カウンタを実装する回。

8254タイマは初めて使ったけど、シンプルでわかりやすい。 今はもっぱらLocal APIC + TSCだから使う機会がない。HPETですら使わないんだもんね。 タイマーが使えるようになって、より一層OS感が出てきた気がする。

f:id:nbisco:20191203230022p:plain
12日目

13日目

割り込み周りの性能を改善をする回。 具体的には、割り込みハンドラごとに用意していたバッファを1つにまとめたこと、 バッファのデータ長を1バイトから4バイトに変えたこと。特に問題なし。

f:id:nbisco:20191203230351p:plain
13日目

14日目

解像度を大きくし、テキストボックスを作ってみる回。

解像度を変えられる(動的にではないけど)とぐっとOSらしくなってくる。 今まで320x200でやってた分、1024x768になるとめちゃ広く感じる。 左クリックすると、矢印の位置にウィンドウが移動する。左クリックをしたままだと、 ウィンドウがそのままついてくる。

ウィンドウを動かすこともできるようになったし、いよいよOSらしくなってきて 盛り上がってきた。

f:id:nbisco:20191203230754p:plain
14日目

15日目

マルチタスクにする回。

ついにマルチタスク!と言っても、まだスケジューラを用意したわけではなく、 一定周期で2つのタスクを切り替えるというもの。シンプル。

f:id:nbisco:20191203231117p:plain

16日目

複数のタスクの切り替え、タスクごとの優先度付けをできるようにする回。

優先度の付け方はLinuxでいうところのSCHED_FIFOに近い。 優先レベルごとにグループを作成し、動作可能なプログラムが存在するグループのうち、 優先レベルの最も高いグループ内で 優先度に従ってスケジューリングを行う。 優先レベルの低いグループに属するプログラムは動作させない。

今回は構造が変わるところが多く、複雑なので、こまめにコミットするようにした。

図では、4タスク(タスクA、タスクB0~B2)動作させているところ。 タスクB0への割当時間を1とすると、タスクB1への割当時間は2、 タスクB2への割当時間は4となるようにしている。 カウンタもだいたい合ってそうだよね。

f:id:nbisco:20191203231533p:plain
16日目

17日目

コンソール画面を作って、キーボード入力をできるようにする回。 tabを押すことで、タスクの切り替えもできるようになった。

これまでアルファベット大文字と数字だけだったのが、 アルファベット小文字、記号も入力できる。小文字と記号を入力できるように、 shiftキーをサポートし、ついでに*Lockをサポートした。 まだEnterキーは未サポート。

f:id:nbisco:20191203231857p:plain
17日目

18日目

Enterキー、画面スクロールのサポートと、コンソール用コマンドを作成した回。

Enterキーをサポートし、コンソール画面もスクロールができるようになった。 文字を打つだけでなく、 3つのコマンドも定義したので、 コンソールもそれっぽくなってきた。

  • mem: 全メモリ量と使用可能メモリ量の表示
  • cls: 画面のクリア
  • dir: FAT32メタデータを読んでファイル名リストの表示

コマンド入力するのに、strcmpが欲しくなったので自作。また、dirコマンドはFAT32を直接読むので、 これまでみたいに適当にやるのはダメである。ということで、 再度偉大な先人の記事に習って、 ディスクイメージ作成にddではなく、mformatコマンドとmcopyコマンドを使うようにした。

f:id:nbisco:20191203232304p:plain
18日目

19日目以降

そのうちやるつもり。

おわりに

手を動かして物事を理解する、というのは大変楽しいですね。