ネイティブコードの構造
基礎的なアセンブラ命令を説明したところで、早速ネイティブコードの構造を突き詰めていきましょう。x86は拡張命令用のビットなどが用意されているため、すべてが統一されたフォーマットで表現できるわけではありません。まずは一般的なネイティブコードの構造から紹介していきます。
- 命令プリフィックスは、16ビット演算の場合や拡張命令を指定したいときなど、少し特殊な場合に使用します。32ビット演算の
mov
、add
、sub
などの単純な命令のときは省略されることがほとんどです。 - オペコードには命令を識別するための値を指定します。
- ModR/M、SIBにはレジスタ・メモリを参照する際のアドレッシングモードを指定します。詳細は後述します。
- ディスプレースメント、即値には8ビット~32ビットの値を指定できます。
これらの各要素はオペコードの種類、アドレッシングモードの状態により、省略されることがあります。
アドレッシングモード
アドレッシングモードはx86のオペコードを理解する上で一番重要なポイントであり、最も複雑なポイントであるとも言えます。mov eax,ecx
など、レジスタ間のやり取りだけであれば比較的簡単な機械語フォーマットになっているはずですが、x86では、下記のようなケースにも対応しなければなりません。
mov eax,dword ptr[ecx] mov eax,dword ptr[ecx+edx] mov eax,dword ptr[0x00400000] mov eax,dword ptr[0x00400000+ecx]
そこで登場するのがアドレッシングモード「ModR/M」および「SIB」です。ModR/Mは8086の時代からありましたが、SIBはx86が32ビット化されたときに追加された機能です。アドレッシングモード自体が拡張されているので、そのフォーマットは残念ながら汚くなるはずです。
ModR/M
ModR/Mではレジスタ・メモリのモードを指定します。1バイトで表現され、各ビットは下記の図のような意味を持ちます。
まずは先頭のModビットから説明します。2ビットなので4種類のフラグを表現でき、それぞれ下記のような意味づけがされています(※実効アドレスを指定した場合はそのアドレスに書き込まれている値が評価されます)。
値 | 意味 |
00 | レジスタのみで実効アドレスを指定するモード。 |
01 | レジスタおよび8ビットのディスプレースメントで実効アドレスを指定するモード。 |
10 | レジスタおよび32ビットのディスプレースメントで実効アドレスを指定するモード。 |
11 | レジスタ値を指定するモード。 |
Reg/Opecodeビットおよびr/mビットは機械語の種類により、意味が異なります。独自のオペコードが指定されることがありますが、一般的には下記のようにレジスタを表すことが多いです。
値 | 意味 |
000 | eax (またはal 、ax 、mm0 、xmm0 ) |
001 |
ecx (またはcl 、cx 、mm1 、xmm1 ) |
010 |
edx (またはdl 、dx 、mm2 、xmm2 ) |
011 |
ebx (またはbl 、bx 、mm3 、xmm3 ) |
100 |
esp (またはah 、sp 、mm4 、xmm4 ) |
101 |
ebp (またはch 、bp 、mm5 、xmm5 ) |
110 |
esi (またはdh 、si 、mm6 、xmm6 ) |
111 |
ebi (またはbh 、di 、mm7 、xmm7 ) |
SIB
SIBとはscale(スケール)、index(インデックスレジスタ)、base(ベースレジスタ)の略です。ModR/Mだけでも32ビットのアドレス空間を扱うことができるのですが、SIBバイトを活用すると下記のような複雑なアドレス指定を表現できるようになります。
mov eax,dword ptr[ecx+edx*4] mov eax,dword ptr[0x00400000+ecx+edx*4]
SIBバイトは1バイトで表現され、各ビットは下記の図のような意味を持ちます。
indexはインデックスレジスタ、baseはベースレジスタのことを示します。レジスタの識別コードは、先ほどr/mの説明で出てきたものと同様です。scaleにはスケール(インデックスレジスタの値×スケール)を指定します。スケールのビット内容は下記のようになります。
値 | 説明 |
00 | スケールは1 |
01 | スケールは2 |
10 | スケールは4 |
11 | スケールは8 |
実際のネイティブコードを解析してみよう
お察しの通り、ネイティブコードのフォーマットは非常に複雑な内容となっています。いきなりすべてを理解するのは大変なので、実際のネイティブコードをビット単位で解析し、一つ一つの機械語がどのような意味を持つビットで構成されているのかを調べてみましょう。
mov eax,1 mov ecx,2 add eax,ecx mov dword ptr[edx],eax
1+2の演算結果を、edx
が示すアドレスに書き込むだけの単純なプログラムです。このアセンブラは下記のようなネイティブコードと対になります。
10: mov eax,1 004113AE B8 01 00 00 00 mov eax,1 11: mov ecx,2 004113B3 B9 02 00 00 00 mov ecx,2 12: add eax,ecx 004113B8 03 C1 add eax,ecx 13: mov dword ptr[edx],eax 004113BA 89 02 mov dword ptr [edx],eax
それでは、1行目のmov eax,1
から見ていきましょう。この命令のポイントは下記のようになります。
- 命令プリフィックスは無し
- オペコード(即値からレジスタへの
mov
)は10111(2)である - レジスタが
eax
であるというのはオペコードバイト内で指定されている - 32ビットの即値が付加されている
既にお気づきの方もいるかとは思いますが、この一つの命令の意味を構成するのは、実は1バイト目の0xB8なのです。後に続く4バイトは即値である1を示しています。即値がリトルエンディアン形式で指定される点にも注意しておきましょう。
先頭バイトの0xB8は下記の図のような構成になっています。
この命令の場合はレジスタと即値のみを扱うため、アドレッシングモードを指定する必要はありません。レジスタを識別するための値は、オペコード内の2~0ビットに保持されます。
2行目のmov ecx,2
も上記の内容とまったく同じです。レジスタを示す値と即値の内容が変わっただけです。
3行目のadd eax,ecx
はどのような構造なのでしょうか? レジスタ-レジスタ間ということで、単純なアドレッシングモードが要求されることになります。0x03、0xC1は下図のような構成からなる値です。
4行目のmov dword ptr[edx],eax
は0x89、0x02というネイティブコードに変換されていますが、同じく、下図のような構成からなります。