Shoeisha Technology Media

CodeZine(コードジン)

記事種別から探す

x86系CPUのネイティブコードを解析する

Windows実行ファイル「EXE」の謎に迫る 第5回

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2006/07/14 00:00

今回はPC市場で一般的に採用されているx86系CPUの機械語に迫ります。最新技術が次々と現れる昨今ですが、実はx86の規格はあまり目新しいものではありません。クロック周波数は上がるものの、機械語の形態は昔から引き継がれています。

目次

はじめに

 今回は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に関するテクニックは出てきませんので、ご了承ください。

本連載の既出記事

アセンブラ

 ネイティブコードと言えばアセンブラです。アセンブラ言語で書かれたコードはネイティブコードと対になっており、オペコードを解読していく際にはアセンブラの知識は必須になります。まずはx86アセンブラに関するポイントを押さえておきましょう。

アセンブラコードの例

C言語のサンプルソース
#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では主に下記のようなレジスタが使用できます。

レジスタの種類
種類 レジスタ
汎用レジスタ eaxecxedxebxespebpesiedi
セグメントレジスタ csdsssesfsgs
ステータス制御レジスタ eflagseip
FPUレジスタ st(0)st(7)
MMXレジスタ mm0mm7
SSEレジスタ xmm0xmm7

 汎用レジスタは、さまざまなオペコードでオペランド(パラメータのようなもの)として利用できます。それぞれ32ビットの値を扱えます。

 汎用レジスタと言えど、実はこの中で既に役割が決まっているレジスタがあります。espはスタックポインタとしてpush/pop命令などで利用されるので、直接的に値を書き換えてしまうのは好ましくありません。ebpはモジュール領域内(一般的には一つの関数内)のスタックフレームのベースポインタを保持することが多いです。よって、このレジスタもむやみやたらに利用すべきではありません。

 セグメントレジスタは汎用レジスタでは表すことができないメモリ領域を指し示す際に必要なレジスタであり、CPUが8ビット、16ビットの時代に活躍していました。最近ではアドレスを指定する際は32ビットの汎用レジスタでほとんど事足りてしまうため、利用頻度はあまり高くありません。

 eflagsは演算過程の状態を保存するためのフラグレジスタです。加減算命令(addsub)やcmp命令、test命令などの比較命令を実行した際に自動的にセットされます。eflags内のデータは下記のようなフラグを持ち合わせます。

フラグの種類
フラグ 説明
CF(キャリーフラグ) 算術命令を処理した際に、最上位ビットに対して繰り上げまたは繰り下げが行われたときにセットされます。
ZF(ゼロフラグ) 算術命令を処理した際に、結果が0だったときにセットされます。
SF(サインフラグ) 算術命令を処理した際に、結果がマイナス値だった場合にセットされます。
OF(オーバーフラグ) 算術命令を処理した際に、結果格納用のレジスタよりも結果の値が大きいときにセットされます。

 eipは次に実行すべきネイティブコードのアドレスを保持します。ジャンプ命令などで自動的に書き換わるため、プログラマが任意に操作することはありません。

 FPUレジスタ、MMXレジスタ、SSEレジスタは浮動小数点演算や64ビット・128ビットの高精度な整数型の演算を行う場合に使用されます。FPUレジスタはx86に標準で搭載されていますが、MMXレジスタ、SSEレジスタは386/486、初代Pentiumなどでは搭載されていないので注意が必要です。

mov命令

 まずは出現頻度がかなり高い、レジスタ・メモリ間またはレジスタ・レジスタ間で値をコピーするための命令を紹介します。

mov命令の例
mov ecx,10      ;ecx=10
mov eax,ecx     ;eax=ecx

 例えば、上記のようなコードを実行すると、eaxecxの内容は共に10になります。セミコロン;の後は注釈文です。

add命令、sub命令

 それぞれ加算・減算になります。

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 無条件分岐
jcjnc CF(キャリーフラグ)が立っているかどうか
jzjnz ZF(ゼロフラグ)が立っているかどうか
jsjns SF(サインフラグ)が立っているかどうか
jojno OF(オーバーフラグ)が立っているかどうか

 cmp命令で大小を比較することもできます。例えば、cmp eax,ecxとした場合、下記のような条件分岐を行えます。

条件分岐命令のケース
状況 符号あり命令 符号なし命令
eax > ecx jg ja
eax < ecx jl jb
eax >= ecx jge jae
eax <= ecx jle jbe
eax == ecx je

  • LINEで送る
  • このエントリーをはてなブックマークに追加

修正履歴

  • 2010/04/19 10:10 誤字修正: 16バイト演算⇒16ビット演算

  • 2006/08/28 15:46 表記誤りを修正:誤)ビッグエンディアン → 正)リトルエンディアン

著者プロフィール

バックナンバー

連載:Windows実行ファイル「EXE」の謎に迫る

もっと読む

All contents copyright © 2005-2017 Shoeisha Co., Ltd. All rights reserved. ver.1.5