$ make
$ ./vm dos-1.25/floppy
するとソースからビルドした MS-DOS v.1.25 が動きます。mcopy などが無い場合は各自でmtoolsをインストールしてください。
(内容と少し関係ない宣伝) Binary Hacks Rebooted という本が出ました。
18年前の記録 : https://w0.hatenablog.com/entry/20061106/p5
前回は、Hacks 的なノリで書いてた気がするけど、今回はみんな論文かよというくらいのレビューをしていて、品質は Hacks 的なノリを超えてると思いますね。(私は色々忙しくてあまり参加できなかったけど…)
私も少し書きました。私が書いたのは、
このへんの時に理解したファームウェアプログラミングの話 (結局 FireBox EXPLODED はなんもしてないです) と、それの準備的な話x2、あとそれとは関係なく本書いてる時期に知った huge page の話の計4つです。
書いたり調べたりしてると、ファームウェアプログラミングの気持ちがちょっと高まってきたので、何か目標を置いてファームウェアを動かしたい、となると、やっぱ OS 動くレベルのファームウェアにはしたいよねぇ、どうせならBIOSの上で動いていた昔の MS-DOS を動かしたいのでは?せっかく MS-DOS はソース公開されてんだしさぁ!という気持ちのルートを辿って、
「自作ファームウェアの上で MS-DOS を動かしてみよう」
という目標が立った。
(実際にはこれは難しい、なぜかというと、DRAMコントローラの初期化方法はどこにも書かれてないので…corebootのソース読めば分かるかと思ってたけど、読んでも分からなかった。まあソースそのまま持ってくればいいといえばいいが…これはどうするかは特に何も決まってない)
で、その準備段階として、MS-DOS がどう動いているのが理解しよう、というのが、今回の話。
MS-DOS のソースコード見たことある人は分かると思うが、MS-DOS のソースが公開されていると言っても、
親切な説明は一切なく、ビルド方法も不明な ASM (と何に使ったらいいかわからないバイナリ(とBASIC。なお当時のBASIC実装は一部の処理をROM BASICに移譲していて、ROM BASICが入ってないと動かないらしい…どうやって使えと…))が置いてあるだけで、これをどうしたら動くようになるかはさっぱり分からないという状態である。
これは詳しい人が解説してくれている。 https://www.os2museum.com/wp/pc-dos-1-1-from-scratch/ が、これ、ソースとして公開されていないバイナリを含んでいて、若干グレーな感じがある。今更MS-DOSのバイナリの一部をグレーな方法で使ったぐらいで即座に訴えられるとは思わないけど、できれば公開されているソースだけからビルドしたいですよね。
(厳密には私はこれの中身を見てしまってるので、以下の解説はクリーンルームから生まれた解説ではないです。そのあたり気になる人は読まないように)
以下、ソースからビルドした MS-DOS を KVM 上で動かす方法について、解説していく。
(KVM って何?あ! Binary Hacks Rebooted には https://www.oreilly.co.jp/books/9784814400850/ KVM で自分でハイパーバイザを作る方法も載ってるね。みんなも読もう(ダイマ))
MS-DOS v1.25 のソース
MS-DOSが動いた、と主張できる状態にするのに必要なASMは
- https://github.com/microsoft/MS-DOS/blob/main/v1.25/source/COMMAND.ASM
- https://github.com/microsoft/MS-DOS/blob/main/v1.25/source/MSDOS.ASM (これは実際には https://github.com/microsoft/MS-DOS/blob/main/v1.25/source/STDDOS.ASM からincludeされていて、STDDOS.ASM からアセンブルする)
- https://github.com/microsoft/MS-DOS/blob/main/v1.25/source/IO.ASM
の、みっつだ。MSDOS.ASM がいわゆるカーネル、COMMAND.ASM がシェルになる。
IBM PC では、MSDOS.ASM が IBMDOS.COM、IO.ASM が IBMBIO.COM という名前にアセンブルされる。それ以外のマシンでは、MSDOS.SYS、IO.SYS という名前になる。初見だと、「MS-DOSのソース公開」と聞けば、これがビルドできるソースだと思ってしまうだろう。
// MS-DOS のソースと聞いて多くの人が想像するもの
+------------------------------+
| DOS カーネル (IBMDOS.COM) |
+------------------------------+
| IBM PC用ドライバ(IBMBIO.COM) |
+------------------------------+---+
| IBM PC BIOS |
+----------------------------------+-+
| IBM PC HW |
+------------------------------------+
だが、このソースからは、これは再現できない。
問題は、IO.ASM である。MS-DOS では、カーネルには(x86のasmで書いてあることを除いて)ハードウェア依存部分はなく、ハードウェア依存部分は、IO.ASM に閉じ込められている。つまり、ハードウェアと対応したIO.ASMを用意する必要がある。
残念ながら、この公開されているソースコードに含まれている IO.ASM は、DOSのオリジナル開発元のSCP社が販売していたコンピュータ用(多分)の IO.ASM のみとなっており、IBM PC 用の IO.ASM が含まれていない。
// 公開されているMS-DOS v1.25 のソース (筆者も全く知らない時代なのでどういう状況なのか説明できないです)
+------------------------------+
| DOS カーネル (MSDOS.SYS) |
+------------------------------+
| SCP PC用ドライバ(IO.SYS) |
+------------------------------+-----+
| SCP PC HW |
+------------------------------------+
つまり、この公開されているソースコードからは、IBM PC で動く MS-DOS をビルドすることはできないのだ。もちろん IBM PCをエミュレートするQEMUで動かすこともできない。
悲しいね…
なんだが、当初の目標は、
「自作ファームウェアの上で MS-DOS を動かしてみよう」
であったので、私個人の目標としては、そんなに障害ではなかった。IBM PC 用の IO.ASM は、IBM PC BIOS 向けに作られているが、そもそも今の目標としては、IBM PC BIOS は存在してないわけで、無理して IBM PC 用の IO.ASM を用意する必要もない。
ここでの将来の最終目標としては、自作ファームウェア向けの IO.ASM を直接作る、という目標にする。
// 私の最終目標 (今作ってるわけではない)
+------------------------------+
| DOS カーネル (MSDOS.SYS) |
+------------------------------+
| MY FW用ドライバ (IO.SYS) |
+------------------------------+---+
| MY FW |
+----------------------------------+--+
| x86 PC |
+-------------------------------------+
で、その前段階として、(MS-DOS v1.25 の動作の理解を深めるために)
- 自分で作った VM 上で、MS-DOS v1.25 を動かす
- IO.ASM の処理に相当するものは全てハイパーバイザコールとして、仮想マシンモニタ側に投げる
という実装をして、ソースからビルドした MS-DOS v1.25 を動かす。
// 今日の話
+------------------------------+
| DOS カーネル (MSDOS.SYS) |
+------------------------------+
| MY VM用ドライバ (IO.SYS) |
| (全てをハイパバイザに丸投げ) |
+------------------------------+---+
| x86 MY VM |
+----------------------------------+
前置きが長くなりました。今日やることは、
「MS-DOS v1.25 を動かすだけのハイパーバイザを作る」
という話です。
MS-DOS v1.25 のメモリマップ
x86では、0x0_0000-0x0_0400 は、割り込みベクタが埋まっている。IBM PC の場合は、この後ろの 0x0_0400-0x0_0600 に、BIOS が使うメモリ領域(BDA)が予約されている。このあとに、IO.ASM をビルドしてできた IO.SYS が配置される。
(STDDOS.ASMのIBM EQU TRUE,MSVER EQU FALSEになってると、このメモリマップになる。もとのソースではMSVER EQU TRUEになってるけどこれは手で修正してます)
IO.SYS の先頭には、アドレスと対応するjmp命令が埋まっている。(https://github.com/microsoft/MS-DOS/blob/main/v1.25/source/IO.ASM#L112-L126)
ここにlong jmpすることで、HW抽象化レイヤであるIO.SYSに実装されている処理を呼び出すことができる。(long jmpが何かわからない平成生まれ達はお父さんお母さんに聞いてみよう)
つまり、
CALL 0060h:0000
と、すれば、IO.SYS に含まれるハードウェア依存処理を呼び出せる、というようになっている。
この 0060h セグメントの位置はカーネル(MSDOS.SYS)にハードコードされていて、IO.SYS は必ずこのアドレスにロードされていなければならない。
IO.SYSのあとに、MSDOS.SYS がロードされる。MSDOS.SYS がロードされるセグメントは固定されていない。IO.SYS のサイズが変わっても、そのまま同じバイナリが使える (はず)。
MSDOS.SYS は初期化中に、ディスクのフォーマット状況を調べて、必要なメモリを確保する(これ嘘かも)。それ確保したあとの領域に、COMMAND.COM がロードされる。
というわけで、起動直後のメモリマップは↓こんなふうになる。
+-------------+----0x0_0000
| |
| x86 IVT |
| | 0x0_03ff
+-------------+----0x0_0400
| |
| BIOS BDA |
| | 0x0_05ff
+-------------+----0x0_0600
| jmp table | <-- ここへlong jmpするとIO.SYSを呼び出せる
| |
| |
| IO.SYS |
| |
+-------------+--- (どこでもいい)
| |
| MSDOS.SYS |
| |
+-------------+--- (どこでもいい)
| |
| COMMAND.COM |
| |
+-------------+----
| |
| free area |
| |
| |
| |
こうなるように配置して、IO.SYS を自分に必要なようにすればいい。
vmm for MS-DOS
これらを踏まえて、MS-DOS v1.25 を動かす vmm を考えよう。
vm 内からハイパバイザを呼ぶ方法は hlt 命令とする。これは hlt 命令は1byteでデバッグに便利だったので…
hlt 命令で、ハイパバイザが呼ばれたら、そのときのプログラムカウンタを見て、どの hlt に来たのかを判断する。
+-------------+----0x0_0000
| |
| x86 IVT |
| | 0x0_03ff
+-------------+----0x0_0400
| |
| BIOS BDA |
| | 0x0_05ff
+-------------+----0x0_0600
| hlt | <-- MSDOS.SYSはドライバが必要になるとここへlong jumpしてくる。ここにhltを埋めてハイパバイザへ処理を渡す
| hlt |
| hlt |
| ... |
| IO.SYS |
| |
+-------------+--- (どこでもいい)
| |
| MSDOS.SYS |
| |
このときのプログラムカウンタによって、対応する処理を実行する。
今の実装はカスなので時間処理が適当(全部1980年で返してる)。
あとターミナルから一文字読む処理はttyをraw modeにしてttyのechoなしにして、ちゃんとスキャンコードとか返さないといけないけど、これをやってない。なので、ttyのechoとDOSのecho両方出てしまってるし、Ctrl-C を押すとvmmごと終了する。(これをちゃんとするのは読者の夏休みの宿題とする)
原理的にやってることはこれだけで、そんな難しくなかった。
初期化
MS-DOS v1.25 ではIO.SYSから初期化処理がはじまる。IO.SYSは、自分の初期化が終わったら、MSDOS.SYS カーネルの初期化を呼び出し、それが終わったらCOMMAND.COMをメモリ上にロードし、そこへジャンプして、IO.SYSの初期化の役割は終わる。
ドライバがシェルをロードする責任を持ってるのが現代のOSとちょっと違って面白い。
あと、現代のファイルシステムは、フォーマットに関するメタ情報をディスクに保存しておくのが普通だが、MS-DOS v1.25 の頃は、フロッピーにフォーマット情報は記録されておらず、IO.SYS がハードウェアにあわせて、ディスクのフォーマット情報をMSDOS.SYS カーネル初期化時に渡さないといけない。
これのレイアウトは、現代ではMBRに書かれている https://wiki.osdev.org/FAT#BPB_(BIOS_Parameter_Block) BPB の11byte目以降と同じである。この BPB をIO.SYSのセグメント中に置いて、そのテーブルへのポインタをSIレジスタ経由で MSDOS.SYS の初期化時に渡す必要がある。(正直これを理解するのが一番時間かかった)
ビルドツール
MS-DOS v1.25 のソースには、バイナリとしてリンカ(LINK.EXE)が含まれているが、アセンブラ(MASM.EXE)がなく、厳しい。
幸い、MS-DOS v2.0 のバイナリにはMASM.EXEが入っていたので、それを使っている。https://github.com/microsoft/MS-DOS/tree/main/v2.0/bin
微妙に互換性がなく、COMMAND.COM が、セグメントごとにZEROラベルを定義しているが、これはバージョンのあってないMASMでは多重定義になってしまう。
これだけ fix する python を通している https://github.com/tanakamura/run_dos125/blob/main/dos-1.25/fixzero.py
まとめ
これで動きました。
https://x.com/tanakmura/status/1286976515570163712 今ならこれできるだけの知識ある。割り込みはいらない(が、もうハードウェアないのでできない)
https://x.com/hdk_2/status/1573562183765880833 あとこれはMS-DOSのビルド方法調べていて、参考にならないか見てた時は意味わからなかったけど、自分でkvm_runを書いてるときに、「あ、これ大体同じことやってるわね!」と気付いたりした。