Golangの8086実装でとりあえずMINIXのyesコマンドが動いた
細かいことはおいといて、8086 向けのバイナリとして MINIX という OS が持っている yes コマンドをとりあえず動かすことができました。OS とかは省略して、ともかくこのバイナリを解釈して 8086 に命令を飛ばして動かしただけですが、自分としては大きな一歩です。文字を書き出すところの write system call はそのまま実行元の OS の system call を呼び出しています。
https://showterm.io/6ddbd0ae8018f9ba7434d#fast
なお、@7shi さんのこちらの記事とコードを丸パクリしたようなものですので、@7shi さんには頭が上がりません。大感謝。
type/interface 設計
逆アセンブラを作るだけなら、あまり凝らなければすぐできてしまいました。そこで、すぐに VM の実装を始めました。それにあたって、一度作った逆アセンブラは捨てて、一から type や interface の設計をしてみました。
Operand を interface で表現して、実装として即値やレジスタやメモリを定義しています。Opcode は最大 2 つの Operand を持つ struct として定義して、逆アセンブラ同様にバイナリを解析してはデータシートの通りに Opcode を生成できるようになりました。あとは VM を struct で定義して、レジスタやメモリの実際のデータを保持させている感じです。
interface は初めて使ったので苦労しましたが、呼び出せるメソッドを制限できるという点で隠蔽っぽいこともできるなーと思いました。まだまだ慣れが必要。
苦労した点
設計は結構時間を使いましたが、あとは愚直に実装していくだけだったので CPU としての 8086 の実装はそんなに苦労してない感じです。delegate 的なことやってる(例えば逆アセンブルは Operand 自体に実装を寄せている)ので、実装を 1 つ 1 つ片付けていくと一気に動くようになる感じは設計ちゃんとやってよかったなーと思いました。
8086 の命令のバイナリが汚すぎてマジで発狂しそうでしたが、結果的には 1 バイト目を愚直にすべて switch/case に書いたらそこそこスッキリしました。変にロジックを入れるより見やすくて良くなったと思います。
あとは、MINIX のコマンドライン引数の渡し方がよくわからなくて苦労しました。バイナリを何度も実行しながら、execve のコードをみたりしながら、とりあえず引数の文字列へのポインタを並べておけば動きはしました。32bit な C の情報とか見てたらもうちょっと複雑に組み立ててたのでそれで実装してたら、どうみてもバイナリと合わなかったので混乱しました。これでいいのかはこれから考えます。。。
write system call のところはソース眺めはしたものの、とりあえず@7shi さんの実装をパクっただけです。もうちょっといろんな system call 実装していけば理解が深まるはず。
得られたもの
2 進/10 進/16 進の変換とか、ちょっとマスクかけたり反転させたりシフトさせたりとかが、ある程度はすらすらっとできるようになりました。符号付き整数とかもわかってきたと思います。
Go は凝らないものであればとりあえずスラスラ書けるようになった気はします。ライブラリとか作ってるわけではないのであれですが、だいぶ文法的なものはなれました。設計思想はまだまだわかってないので、いつかいろんなコードを読んで勉強したいところです。
なにより、レジスタやメモリ、フラグの動きについて教科書的なふわっとした話しか知らなかったのが、実際ビットレベルでどうなってるのかがはっきり理解できたのが一番の収穫です。デバッグでは yes はヒープ的なところは使わないので、レジスタとフラグとスタックを吐き出して必死に追いかけました。あとはまだ 8086 特有のメモリセグメントについては実際がわかってないので、それは追々。。。
今後
とりあえずここまでは早くたどり着きたかったのでゴリゴリ書きましたが、一旦手をゆるめてゆるゆると実装を進めて行きたいと思います。@7shi さんがやっているみたいに MINIX のカーネルをビルドできるようになりたい!