池袋バイナリ勉強会に参加して、8086の逆アセンブラーを作成しようとしているのだが、これが思いの外に難しい。いや、めんどくさい。
筆者は、これまでx86アセンブリには、ニーモニック経由でしか触れてこなかった。movはmovであり、それ以外の何者でもなかった。
mov ax, bx
などと書いたら、これをアセンブラーにビット列に変換させて、その後は何も考えなかった。
既存のバイナリを逆アセンブラーにかけて読む場合でも、やはり逆アセンブラーがバイナリをニーモニックに変換してくれるので、やはりビット列による表現方法に関しては、考えたこともなかった。
さて、池袋バイナリ勉強会に参加したので、この機会に、逆アセンブラーでも実装しようと思い立った。そこで、Intelの当時の8086の資料を読んでみた。
Index of /Intel/x86/808x/datashts/8086
そして、絶望した。8086のビット列は汚い。汚すぎる。我々はこんなに汚いバイナリの上に成り立っていたのかと思うと、愕然とする。
8086のバイナリは、最初の1バイトを読めば命令を判定できる。その後にオペランドが続く。問題は、ニーモニック上からはひとつの命令だと普段我々が認識している命令は、8086バイナリでは、複数のビット列で表現されるのだ。
例えば、8086のバイナリからみると、movというのは、7種類のバイナリ列で表現されている。レジスタ同士、レジスタとメモリのmovや、即値からレジスタやメモリといった操作で、ビット列が違うし、アキュムレーターとかセグメントレジスターといった特別なレジスターを操作するためにも、専用のビット列が用意されている。
MOV亜種 | ビット列 |
Register/Memory to/from Register | 100010 |
Immediate to Register Memory | 1100011 |
Immediate to Register | 1011 |
Memory to Accumulator | 1010000 |
Accumulator to Memory | 1010001 |
Register/Memory to Segment Register | 10001110 |
Segment Register to Register/Memory | 10001100 |
それだけではない。一つのバイナリ列にしても、複雑なビットフラグで、レジスタ間の操作や、あるいはメモリーへの操作であるなどを指定している。しかもそのビットフラグは、命令を表現する1バイトの一部を使っていることもあるのだ。そう、上の表で8bit使っていないのは、単に使われていないだけではない。フラグとして使っているのである。
MOV亜種 | ビット列 |
Register/Memory to/from Register | 100010dw |
Immediate to Register Memory | 1100011w |
Immediate to Register | 1011w reg |
Memory to Accumulator | 1010000w |
Accumulator to Memory | 1010001w |
Register/Memory to Segment Register | 10001110 |
Segment Register to Register/Memory | 10001100 |
dは1bitのフラグで、d = 1のとき、レジスターがdestinationであり、d = 0のときは、 レジスターがsourceとなる。つまり、sourceとdestinationを、このフラグによってひっくり返す必要がある。
wは1bitのフラグで、w = 1のとき、命令はワードサイズ(2バイト、16bit)で処理し、w = 0のときは、バイトサイズで処理する。つまり、レジスターやオペランドのサイズを変えなければならない。
regというのは3bitのレジスターを指定する識別番号である。wフラグによって、ワードサイズのレジスター(AX, BXなど)とバイトサイズのレジスター(AL, BLなど)を切り替えなければならない。
また、今回は違うが、regにはセグメントレジスターを指定する2bit版のものもある。
そして、これに続くオペランドがまたひどい。mov以外でもよく使われるオペランドに、
mod | reg | r/m |
というものがある。これは1バイトを、2bit, 3bit, 3bitに区切り、オペランドとして使うレジスターやメモリを指定している。
mod(mode)は2bitのフラグで、オペランドがどのようなものかのモードを指定する。これによって、r/mがレジスターになったりメモリーアドレッシング・モード指定になったりする。
メモリーアドレッシング・モード。これが、厄介だ。
8086では、やたらに複雑なメモリーアドレッシング・モードがある。これでも、まだ少ない方なのだ。8086の系譜は、この後ますますこのアドレッシング・モードを拡張していったのだ。とにかく、アドレッシング・モードとは、単にメモリを指し示すアドレスをそのまま用いるのではなく、SI + アドレスとか、DI + アドレスとか、BX + SI + アドレスとか、とにかくやたらに組み合わせがある。
このアドレスを、Intelの仕様書ではdisp(displacement)と書いている。
dispというのは基本的にバイトサイズだ。mod = 00のときは、dispは存在しない。ところが、である。mod = 00かつr/m = 110のときのみ、dispはワードサイズとなり、アドレッシング・モードはdispのみとなるのだ。ひどい例外的ルールもあったものだ。
さて、ビット列の文法はだいたい理解した。しかし、これはいったいどうすればいいのだろうか。どう考えても汚いコードしか思い浮かばない。どうせたかだか1バイトで命令を判定できるのだから、256個の配列や256個のswtich caseを書くのが一番手っ取り早いのだろうか。しかし、命令にもフラグが入っている都合上、極めて汚いコードになってしまう。
そして、オペランドのmod reg r/mだ。めんどくさい。極めてめんどくさい。
とりあえず書こうと思っては、思い直して止まっている。ビット列に対してこのようなパースを簡単にできるライブラリや言語機能が欲しい。
ドワンゴ広告
この記事はドワンゴの勤務時間を極端に減らして捻出した余裕で休日に池袋バイナリ勉強会に参加して書いた。
ドワンゴは本物のC++プログラマーを募集しています。
CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0
> ところが、である。mod = 00かつr/m = 110のときのみ、
ReplyDeleteもしかして、mod = 00かつr/m = 101のとき、ではありませんか?
イントリンシックスみたく一回関数にしてみるといいかもですね。
ReplyDelete逆汗は作ったことないですけど。Orz
っていうか、一回アセンブラのほうを作ってみた方が簡単だったりしません?
そうそう。
ReplyDelete秩序というものは基本的になるべくしてそうなるのではなく、誰かが発明したことに従う人種が現れることで成されるのではないでしょうか。
アセンブラも昔は発明でしたでしょうし。
昔の話ですが美しさでいったら28系といってましたが、結局インテルの天下になってしまいましたね。(自分は32bitになってメモリマップが頭に浮かばなくなって逃げ出したクチです)
ReplyDeleteMIPSやZ80のような綺麗なバイナリって結局余分な回路が要らないから組み込み系でって感じになり、高速なアプリケーションプロセッサは結局度重なる機能追加によりどんどん汚くなって行くんですよね(8086は元から汚ないけど)。ARMとか性能向上に合わせてどんどんヘンタイ命令化していきますし。
ReplyDeleteZ80って綺麗だったのか。
ReplyDeleteこんど、8086と比較してみよ。