8086のバイナリが汚すぎる
池袋バイナリ勉強会に参加して、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