Golangの8086実装でとりあえずMINIXのyesコマンドが動いた

細かいことはおいといて、8086向けのバイナリとしてMINIXというOSが持っているyesコマンドをとりあえず動かすことができました。OSとかは省略して、ともかくこのバイナリを解釈して8086に命令を飛ばして動かしただけですが、自分としては大きな一歩です。文字を書き出すところのwrite system callはそのまま実行元のOSのsystem callを呼び出しています。

riywo/go8086

なお、@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のカーネルをビルドできるようになりたい!