はじめに
今回はPC市場で一般的に採用される、x86系のCPUの機械語に迫ります。最新技術が次々と現れる昨今ですが、実はx86の規格はあまり目新しいものではありません。クロック周波数は上がるものの、機械語の形態は昔から引き継がれています。
x86 CPUの種類
x86ネイティブコードを解読できるCPUには、下記のようなものが挙げられます(※AMD64を除く)。もはや説明はいりませんね。
- Intel Pentium 4
- Intel Pentium 3
- Intel Pentium 2
- Intel MMX Pentium
- Intel Pentium
- Intel 486/386
- AMD Athron
- AMD Duron
- AMD K-6
ここからは、32ビット以降のx86系 CPUに関する話題を扱っていきます。8ビットCPU、16ビットCPUに関するテクニックは出てきませんので、ご了承ください。
・第2回 『EXEファイルの内部構造(PEヘッダ)』
・第3回 『EXEファイルの内部構造(セクション)』
・第4回 『プログラムからEXEファイルを生成してみよう』
アセンブラ
ネイティブコードと言えばアセンブラです。アセンブラ言語で書かれたコードはネイティブコードと対になっており、オペコードを解読していく際にはアセンブラの知識は必須になります。まずはx86アセンブラに関するポイントを押さえておきましょう。
アセンブラコードの例
#include "stdafx.h" int _tmain(int argc, _TCHAR* argv[]) { int i; for(i=0;i<10;i++){ printf("%d",i); } return 0; }
C言語で書かれた簡単なソースコードでも、アセンブラさらにはネイティブコードにしてしまうと、下記のような少々長いコードになります。
アドレス,機械語, アセンブラ 1: // AsmTest.cpp : コンソール アプリケーションのエントリ ポイントを 定義します。 2: // 3: 4: #include "stdafx.h" 5: 6: 7: int _tmain(int argc, _TCHAR* argv[]) 8: { 00411390 55 push ebp 00411391 8B EC mov ebp,esp 00411393 81 EC CC 00 00 00 sub esp,0CCh 00411399 53 push ebx 0041139A 56 push esi 0041139B 57 push edi 0041139C 8D BD 34 FF FF FF lea edi,[ebp-0CCh] 004113A2 B9 33 00 00 00 mov ecx,33h 004113A7 B8 CC CC CC CC mov eax,0CCCCCCCCh 004113AC F3 AB rep stos dword ptr es:[edi] 9: int i; 10: for(i=0;i<10;i++){ 004113AE C7 45 F8 00 00 00 00 mov dword ptr [i],0 004113B5 EB 09 jmp main+30h (4113C0h) 004113B7 8B 45 F8 mov eax,dword ptr [i] 004113BA 83 C0 01 add eax,1 004113BD 89 45 F8 mov dword ptr [i],eax 004113C0 83 7D F8 0A cmp dword ptr [i],0Ah 004113C4 7D 1D jge main+53h (4113E3h) 11: printf("%d",i); 004113C6 8B F4 mov esi,esp 004113C8 8B 45 F8 mov eax,dword ptr [i] 004113CB 50 push eax 004113CC 68 3C 56 41 00 push offset string "%d" (41563Ch) 004113D1 FF 15 B8 82 41 00 call dword ptr [__imp__printf (4182B8h)] 004113D7 83 C4 08 add esp,8 004113DA 3B F4 cmp esi,esp 004113DC E8 50 FD FF FF call @ILT+300(__RTC_CheckEsp) (411131h) 12: } 004113E1 EB D4 jmp main+27h (4113B7h) 13: return 0; 004113E3 33 C0 xor eax,eax 14: } 004113E5 5F pop edi 004113E6 5E pop esi 004113E7 5B pop ebx 004113E8 81 C4 CC 00 00 00 add esp,0CCh 004113EE 3B EC cmp ebp,esp 004113F0 E8 3C FD FF FF call @ILT+300(__RTC_CheckEsp) (411131h) 004113F5 8B E5 mov esp,ebp 004113F7 5D pop ebp 004113F8 C3 ret
左端の16進表記の数値が「プロセスメモリ内におけるアドレス」です。前回出てきたEXEファイルのイメージベース(0x00400000)上に配置されていることがよく分かります。真ん中の1バイトずつに区切られた数値が「ネイティブコード」で、右端が「ネイティブコードに対するアセンブラコード」です。
所々でコードバイトに対するソースコードが表示されていますが、これはVCの表示機能によるものです。機械語単位でトレース実行したいときは、このように表示してもらえると便利ですね。
レジスタ
アセンブラと言えばまず出てくるのがレジスタです。レジスタとは、ある値を一時的に保持するためのCPUの機構です。値を保持するだけでなく、レジスタの値を使って演算を行うことができます。
x86では主に下記のようなレジスタが使用できます。
種類 | レジスタ |
汎用レジスタ | eax 、ecx 、edx 、ebx 、esp 、ebp 、esi 、edi |
セグメントレジスタ | cs 、ds 、ss 、es 、fs 、gs |
ステータス制御レジスタ | eflags 、eip |
FPUレジスタ | st(0) ~st(7) |
MMXレジスタ | mm0 ~mm7 |
SSEレジスタ | xmm0 ~xmm7 |
汎用レジスタは、さまざまなオペコードでオペランド(パラメータのようなもの)として利用できます。それぞれ32ビットの値を扱えます。
汎用レジスタと言えど、実はこの中で既に役割が決まっているレジスタがあります。esp
はスタックポインタとしてpush
/pop
命令などで利用されるので、直接的に値を書き換えてしまうのは好ましくありません。ebp
はモジュール領域内(一般的には一つの関数内)のスタックフレームのベースポインタを保持することが多いです。よって、このレジスタもむやみやたらに利用すべきではありません。
セグメントレジスタは汎用レジスタでは表すことができないメモリ領域を指し示す際に必要なレジスタであり、CPUが8ビット、16ビットの時代に活躍していました。最近ではアドレスを指定する際は32ビットの汎用レジスタでほとんど事足りてしまうため、利用頻度はあまり高くありません。
eflags
は演算過程の状態を保存するためのフラグレジスタです。加減算命令(add
、sub
)やcmp
命令、test
命令などの比較命令を実行した際に自動的にセットされます。eflags
内のデータは下記のようなフラグを持ち合わせます。
フラグ | 説明 |
CF(キャリーフラグ) | 算術命令を処理した際に、最上位ビットに対して繰り上げまたは繰り下げが行われたときにセットされます。 |
ZF(ゼロフラグ) | 算術命令を処理した際に、結果が0だったときにセットされます。 |
SF(サインフラグ) | 算術命令を処理した際に、結果がマイナス値だった場合にセットされます。 |
OF(オーバーフラグ) | 算術命令を処理した際に、結果格納用のレジスタよりも結果の値が大きいときにセットされます。 |
eip
は次に実行すべきネイティブコードのアドレスを保持します。ジャンプ命令などで自動的に書き換わるため、プログラマが任意に操作することはありません。
FPUレジスタ、MMXレジスタ、SSEレジスタは浮動小数点演算や64ビット・128ビットの高精度な整数型の演算を行う場合に使用されます。FPUレジスタはx86に標準で搭載されていますが、MMXレジスタ、SSEレジスタは386/486、初代Pentiumなどでは搭載されていないので注意が必要です。
mov命令
まずは出現頻度がかなり高い、レジスタ・メモリ間またはレジスタ・レジスタ間で値をコピーするための命令を紹介します。
mov ecx,10 ;ecx=10 mov eax,ecx ;eax=ecx
例えば、上記のようなコードを実行すると、eax
、ecx
の内容は共に10になります。セミコロン;の後は注釈文です。
add命令、sub命令
それぞれ加算・減算になります。
add eax,ecx ;eax=eax+ecx sub edx,ebx ;edx=edx-ebx
push命令、pop命令
スタックに値を入れるのがpush
命令、取り出すのがpop
命令です。レジスタ、メモリ間のデータのやり取りと同時にesp
(スタックポインタレジスタ)の加減算を自動的に行います。
jmp命令、その他条件分岐
eflags
レジスタのデータをもとに、任意のコード位置に条件分岐を行います。x86では下記に示す数種類の条件分岐命令が使用できます(※命令名の2文字目に「n」という文字があるときは「そうでないとき」という意味になります)。
命令 | 説明 |
jmp |
無条件分岐 |
jc 、jnc |
CF(キャリーフラグ)が立っているかどうか |
jz 、jnz |
ZF(ゼロフラグ)が立っているかどうか |
js 、jns |
SF(サインフラグ)が立っているかどうか |
jo 、jno |
OF(オーバーフラグ)が立っているかどうか |
cmp
命令で大小を比較することもできます。例えば、cmp eax,ecx
とした場合、下記のような条件分岐を行えます。
状況 | 符号あり命令 | 符号なし命令 |
eax > ecx |
jg |
ja |
eax < ecx |
jl |
jb |
eax >= ecx |
jge |
jae |
eax <= ecx |
jle |
jbe |
eax == ecx |
je |