x86命令のバイトコードを理解する
ある命令をバイト表現でハンドアセンブルしたいとき、Intelのオペコード表の見方を理解していないと非常に苦労する。しかし、Intelのマニュアルはとっつきづらい*1ところがあり、理解するのに時間がかかるので、ヒントとしてまとめていく。ちなみに、小数点系やx64については扱わない。
用意するもの
- Intel命令セット リファレンス
- OllyDbg(ハンドアセンブルして確認するため。実際は何でもよい)
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命令の基本的な構造」