x86命令のバイトコードを理解する

ある命令をバイト表現でハンドアセンブルしたいとき、Intelのオペコード表の見方を理解していないと非常に苦労する。しかし、Intelのマニュアルはとっつきづらい*1ところがあり、理解するのに時間がかかるので、ヒントとしてまとめていく。ちなみに、小数点系やx64については扱わない。

用意するもの

OllyDbgで適当なバイナリを開いて、命令行を右クリックして表示されるメニューでバイナリを編集できる。

オペコードの表記

バイナリアンが最も好む命令はINT 3、NOP、JMPであることは疑いようがないので、ここではJMPを例にとる。JMPのオペコード表を見ると、次の表現形式があると書かれている。

オペコード 命令 意味
EB cb JMP rel8 cbで表現される値で相対JMP
E9 cw JMP rel16 cwで表現される値で相対JMP
E9 cd JMP rel32 cdで表現される値で相対JMP
FF /4 JMP r/m16 /4で表現される値で絶対JMP
FF /4 JMP r/m32 /4で表現される値で絶対JMP
EA cd JMP ptr16:16 cdで表現される値で絶対JMP
EA cp JMP ptr16:32 cpで表現される値で絶対JMP
FF /5 JMP m16:16 /5で表現される値で絶対JMP
FF /5 JMP m16:32 /5で表現される値で絶対JMP


Intelリファレンスのこのような表現形式は、「中巻A:3.1.1.1. オペコード欄」で説明がされているが、これがさくっと理解できれば苦労はしないので独自にまとめる。
固定バイト(EBとかE9とかね)の後ろにつく表現は以下の6パターンがあり、これを理解することが重要。

  • cb cw cd cp
  • ib iw id
  • /0-7
  • /r
  • +rb +rw +rd

cb cw cd cp / ib iw id

固定バイトにbyte(1byte), word(2byte), dword(4byte), packed-word(6byte)が続く。EB cbを例にとると、EB XX(1byte)でJMPとして解釈される。

CPU Disasm
Address   Hex dump          Command                                  Comments
0043317E      EB 00         JMP SHORT 00433180

同様にE9 cwはE9 XX XX、E9 cdはE9 XX XX XX XXになる。実際のところはプロセッサが16ビットモードの場合はcw 、32ビットモードの場合はcdが採用されると理解してよい。

CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      E9 00000000        JMP 00433183


EA cd(4byte) / EA cp(6byte)の場合も同じように解釈できる。この場合、16ビットモードの場合cdが、32ビットモードの場合cpが採用される。
同一の固定バイトに対して、cwとcd両方、あるいはcdとcp両方の表現がある場合、常にこの解釈でよい。

CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      EA 00000000 0000   JMP FAR 0000:00000000                    ; Far jump or call

/0-7

この形式はリファレンス中巻の「表 2-2. ModR/Mバイトによる32 ビット・アドレス指定形式」を見ながら解釈していく。リファレンス上は/digitと表記されるこの形式は、固定バイトの後ろに可変長のバイトが続く。

まずFF /4を例に表を見ていく。上段から、/digitと書かれた行を探し、右の数字が一致する場所を見つける。


そしてそのまま下に移ると、20-27/60-67/A0-A7/E0-E7が書かれている。


ということで、さっそく20を使い、FF 20とすると表の「実効アドレス」が示すように[EAX]になる。

CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      FF20               JMP DWORD PTR DS:[EAX]

FF /5の場合は、28からの列なので、FF 28で表現される。

CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      FF28               JMP FAR FWORD PTR DS:[EAX]               ; Far jump or call

/0-7 レジスタ以外の実効アドレスの指定

実効アドレスの列にはレジスタ以外に、disp32、[..][..]、[register]+disp8、[register]+disp32があるので、試しながら見ていく。

disp32
続く32ビット(4byte)の即値で指定することを示す。したがってバイトコードは FF XX XXXXXXXX となる。

CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      FF25 00000000      JMP DWORD PTR DS:[0]
[register]+disp8 / [register]+disp32
続く8ビット(1byte) / 32ビット(4byte)の即値をregisterに加算することを示す。

CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      FF60 01            JMP DWORD PTR DS:[EAX+1]
CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      FFA0 01000000      JMP DWORD PTR DS:[EAX+1]

/0-7 [..][..](SIB)表現

実効アドレスが[..][..]となっているものは、続く1byteが示す値で指定することになる(この表現部分をSIBというが、ここでは用語はどうでもよい)。この1byteはどんな値でも受け付けるが、その意味はリファレンス中巻の「表2-3. SIB バイトによる32 ビット・アドレス指定形式」を参照して解釈する必要がある。


表の意味としては、[表の左側+表の最上段]となる。したがって、FF 24 XXに01を指定すると、[EAX+ECX]となり、43を指定すると[EAX*2+EBX]になる。

CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      FF2401             JMP DWORD PTR DS:[EAX+ECX]
CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      FF2443             JMP DWORD PTR DS:[EAX*2+EBX]


左側のnoneはレジスタを指定しないことを示すので、上段の値のみが利用される。

CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      FF24E0             JMP DWORD PTR DS:[EAX]


表の上段の[*]は[EBP]を示すが、前のバイトの特定のビット(Modビット)が00のときは続く4byteの即値で示す値が加算される。今回の例では前のバイトは24で、問題のビットが00なので、[EBP]は利用されずに続く4byteの即値が利用される。

CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      FF2405 01000000    JMP DWORD PTR DS:[EAX+1]


ここまで理解すれば、後の/rなどもはほぼ同じ。

/r

この形式もリファレンス中巻の「表 2-2. ModR/Mバイトによる32 ビット・アドレス指定形式」を見ながら解釈していく。ここで特に興味深くないMOV命令を例に見てみる。

オペコード 命令 意味
88 /r MOV r/m8, r8 r8を r/m8 に転送する。
89 /r MOV r/m16, r16 r16 を r/m16 に転送する。
89 /r MOV r/m32, r32 r32 を r/m32 に転送する。

/r形式は、表に示す1byteで2つのレジスタを指定し、表は

を示している。表の上段から見ていくのは/digitと同じだが、/digitとは違い任意の列(つまり第一オペランドレジスタ)を指定することができる。たとえば、MOV [EDI], ECXしたいのであれば、CL/CX/ECXの列と、実効アドレスが[EDI]の行の交差点を指定すればよい。

つまり 88 XX に0Fを指定する。

CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      880F               MOV BYTE  PTR DS:[EDI],CL
00433180      890F               MOV DWORD PTR DS:[EDI],ECX


他の組み合わせの解釈も/0-7のときと同じになる。

+rb +rw +rd

この形式は、固定バイトに特定の値を加算することを示している。リファレンス中巻の「表3-1. +rb、+rw、および +rdに対応するレジスタのコード化」に、この加算すべき値が書かれている。
表によると、A系のレジスタを利用したい場合、加算すべき値は0になる。また、C系のレジスタを利用したい場合、加算すべき値は1になる。


やはり退屈なMOVを例にとってみてみる。

オペコード 命令 意味
B0 +rb MOV r8, imm8 imm8を r8 に転送する。
B8 +rw MOV r16, imm16 imm16を r16 に転送する。
B8 +rd MOV r32, imm32 imm32を r32 に転送する。
CPU Disasm
Address   Hex dump               Command                                  Comments
0043317E      B0 00              MOV AL,0                                 ; B0+0
00433180      B1 00              MOV CL,0                                 ; B0+1
00433182      B8 00000000        MOV EAX,0                                ; B8+0
00433187      B9 00000000        MOV ECX,0                                ; B8+1

very easy.

参考

Intel マニュアルの読み方メモ
Wizard Bible vol.27の「Intel x86命令の基本的な構造」

*1:などと言ったらid:ucqに「いや、十分わかりますよ(笑)」と笑顔で返されて、むきーとなったので調べたというのはヒミツ!