SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

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

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

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


  • X ポスト
  • このエントリーをはてなブックマークに追加

ネイティブコード生成モジュールを作ってみよう

 前回作成した「ExeCreator.exe」を改造して、ネイティブコードを自由に生成できるモジュールを作ってみましょう。

 今回のサンプルでは、MessageBoxAおよびwsprintfAを呼び出したいため、前回の「ExeCreator.cpp」から、インポートセクションにおいて若干コードの変更が生じています。この変更に関してはソースコードをダウンロードして確かめてみてください。ここでは、新たに追加する「opcode.cpp」の内容を重点的に解説していきます。

 それでは前回のExeCreatorプロジェクトを開き、「opcode.cpp」を新規作成しましょう。

 まず、「opcode.cpp」の先頭部分では、ModR/M、SIBバイトに必要なフラグを定義しています。REG_EAXREG_EDIがレジスタを識別するための定数になります。REGISTER_OPERANDは下位3ビット以外のビットを排除し、正確なレジスタ識別値を得るためのマクロです。

 これから定義するネイティブコード生成モジュールでは、NativeBufferバッファにネイティブコードを書き込んでいきます。

「opcode.cpp」の先頭部分
#include "stdafx.h"

/////////////////////////////////////////////////
// ModR/Mバイト、SIBバイト、ディスプレースメント
/////////////////////////////////////////////////

//スケール
#define SCALE_NON   (char)0x00
#define SCALE_2     (char)0x40
#define SCALE_4     (char)0x80
#define SCALE_8     (char)0xC0

//インデックスなし
#define INDEX_NON   0x04

//Mod(モード)
#define MOD_BASE        (char)0x00
#define MOD_DISP32      (char)0xFF
#define MOD_BASE_DISP8  (char)0x40
#define MOD_BASE_DISP32 (char)0x80
#define MOD_REG         (char)0xC0

//レジスタを示す定数
#define REG_EAX 0
#define REG_ECX 1
#define REG_EDX 2
#define REG_EBX 3
#define REG_ESP 4
#define REG_EBP 5
#define REG_ESI 6
#define REG_EDI 7

#define REGISTER_OPERAND(reg) (reg&0x07)

extern BYTE NativeBuffer[65536];
extern int iNativeLength;

 次に、ネイティブコードの中核となるアドレッシングモードを解決するためのset_mod_rm_sib_disp関数を定義します。

set_mod_rm_sib_disp関数のパラメータ
パラメータ 説明
mod アドレッシングモード(MOD_BASEMOD_DISP32MOD_BASE_DISP8MOD_BASE_DISP32MOD_REGのいずれか)
reg レジスタ識別値
scale スケール(SCALE_NONSCALE_2SCALE_4SCALE_8のいずれか)
index_reg インデックスレジスタ識別値
base_reg ベースレジスタ識別値
disp ディスプレースメント

 これらの値をパラメータとして受け取り、ModR/M(1バイト)、SIB(1バイト)、disp(1~4バイト)を必要に応じて生成します。

ModR/M、SIB、dispを生成するモジュール
void set_mod_rm_sib_disp(char mod,int reg,int scale,int index_reg,
                         int base_reg,int disp){
    if(mod==MOD_DISP32){
        //ModR/Mバイト
        NativeBuffer[iNativeLength++]=(char)
            (REGISTER_OPERAND(reg)<<3 | REGISTER_OPERAND(0x04));

        base_reg=0x05;
        index_reg=INDEX_NON;
    }
    else{
        //ModR/Mバイト
        NativeBuffer[iNativeLength++]=(char)(mod |
            REGISTER_OPERAND(reg)<<3 | REGISTER_OPERAND(base_reg));
    }

    //レジスタモードの場合は、ここで終了
    if(mod==MOD_REG) return;

    if(REGISTER_OPERAND(base_reg)==0x04||mod==MOD_DISP32){
        //////////////////////
        // SIBバイトを使う
        //////////////////////

        NativeBuffer[iNativeLength++]=(char)(scale| 
            REGISTER_OPERAND(index_reg)<<3 | REGISTER_OPERAND(base_reg));
    }

    //ディスプレースメントを必要としない場合は、ここで終了
    if(mod==MOD_BASE) return;

    //////////////////////////
    // ディスプレースメント
    //////////////////////////

    if(mod==MOD_BASE_DISP8) NativeBuffer[iNativeLength++]=(char)disp;
    else{
        *((long *)(NativeBuffer+iNativeLength))=disp;
        iNativeLength+=sizeof(long);
    }
}

 与えられたオペコードの書き込みとset_mod_rm_sib_disp関数の呼び出しを行い、一つの命令を生成するための関数__op_formatを定義します。

 x86アーキテクチャでは命令の表現がさまざまな形式でなされ、その中には例外扱いに近い一意でないフォーマットも存在します。今回は、__op_format関数をオーバーロードさせることで、出現回数が高いと思われるフォーマットに絞って定義を行います。

 1つ目の__op_format関数は1バイトのオペコードのみで表現される命令に対応します。この関数は1バイトのオペコードのみで構成される命令のみに対応するため、オペランドは存在しません。

 2つ目の__op_format関数は1バイトのオペコードで表現される命令なのですが、レジスタ識別値をバイト中に持つことができます。例えば、「push eax」「push ecx」などはそれぞれ1バイトの命令コードで表現されます。

 3つ目の__op_format関数は1バイトのオペコード(レジスタ識別値含む)に加え、32ビットの即値が必要な命令に対応するものです。ここまでは、アドレッシングモードを指定する必要がないため、set_mod_rm_sib_disp関数は呼び出しません。

 4つ目の__op_format関数はアドレッシングモードを指定する必要がある命令を生成可能にします。オペコードに加え、内部でset_mod_rm_sib_disp関数を呼び出し、ModR/M、SIB、dispを生成します。

 5つ目の__op_format関数は4つ目のものに32ビットの即値生成を追加したものです。

さまざまな体質のネイティブコードに対応した生成モジュール
void __op_format(char opcode){
    //オペコード
    NativeBuffer[iNativeLength++]=opcode;
}
void __op_format(char op_prefix,char opcode,int reg){
    //命令プリフィックス
    if(op_prefix) NativeBuffer[iNativeLength++]=op_prefix;

    //オペコード、レジスタ
    NativeBuffer[iNativeLength++]=(char)(opcode|REGISTER_OPERAND(reg));
}
void __op_format(char op_prefix,char opcode,int reg,int const_value){
    //命令プリフィックス
    if(op_prefix) NativeBuffer[iNativeLength++]=op_prefix;

    //オペコード、レジスタ
    NativeBuffer[iNativeLength++]=(char)(opcode|REGISTER_OPERAND(reg));

    //即値
    *((long *)(NativeBuffer+iNativeLength))=const_value;
    iNativeLength+=sizeof(long);
}
void __op_format(char op_prefix,char opcode1,char opcode2,
                 int reg,int base_reg,int offset,char mod){
    //命令プリフィックス
    if(op_prefix) NativeBuffer[iNativeLength++]=op_prefix;

    //オペコード
    if(opcode1) NativeBuffer[iNativeLength++]=opcode1;
    if(opcode2) NativeBuffer[iNativeLength++]=opcode2;

    //ModR/M, SIB, disp
    set_mod_rm_sib_disp(mod,reg,SCALE_NON,INDEX_NON,base_reg,offset);
}
void __op_format(char op_prefix,char opcode1,char opcode2,
        int reg,int base_reg,int disp,char mod,int const_value){
    //命令プリフィックス
    if(op_prefix) NativeBuffer[iNativeLength++]=op_prefix;

    //オペコード
    if(opcode1) NativeBuffer[iNativeLength++]=opcode1;
    if(opcode2) NativeBuffer[iNativeLength++]=opcode2;

    //ModR/M, SIB, disp
    set_mod_rm_sib_disp(mod,reg,SCALE_NON,INDEX_NON,base_reg,disp);

    //即値
    *((long *)(NativeBuffer+iNativeLength))=const_value;
    iNativeLength+=sizeof(long);
}

 上記で定義した__op_format関数を駆使すれば、オペコード、レジスタ、アドレッシングモードの指定を行って実行可能なネイティブコードを生成できるようになります。ここからは実際の命令と対になる生成モジュールの定義に移ります。

 x86の命令は非常に多くの命令に対応しており、数え切れないほどのオペコードが存在します。今回は、後述するサンプルコードで必要になってくる命令に絞り、命令生成のための関数サンプルを定義していきます。

 まずは関数を定義する前に、オペコードを定義します。ここで定義するオペコードはx86が定めているものであり、おまじないだと思っておきましょう。

 今回は下に示す関数を定義します。

  • op_mov_RV
  • op_add_RR
  • op_add_RV
  • op_sub_RV
  • op_push_R
  • op_push_V
  • op_call_M
  • op_ret

 関数名の最後尾に付加される「R」「V」「M」には、下のようなオペランドの意味を持たせています。2文字重なったときは、指定された2つのオペランドを命令が保有することを意味します。

接尾辞 保有するオペランド
R レジスタ(Register)
V 即値(Value)
M メモリ(Memory)
命令生成のための関数
//オペランドなしのオペコード(1バイトで表現可能)
#define OPCODE_RET_0    (char)0xC3
#define OPCODE_INT_3    (char)0xCC

#define OPCODE_ADD_RR   (char)0x03
#define OPCODE_SUB_RR   (char)0x2B
#define OPCODE_ADD_RV   (char)0x81
#define OPCODE_SUB_RV   (char)0x81

//オペコードバイト中にレジスタ識別ビットを保有するもの
#define OPCODE_PUSH_R    (char)0x50

//オペコードバイト中にレジスタ識別ビットを保有し、即値を必要とするもの
#define OPCODE_CALL_M   (char)0x15 //レジスタ=0
#define OPCODE_PUSH_V   (char)0x68 //レジスタ=0
#define OPCODE_MOV_RV   (char)0xB8

void op_mov_RV(int reg,int iData){
    //mov reg,value

    __op_format(0,OPCODE_MOV_RV,reg,iData);
}

void op_add_RR(int reg1,int reg2){
    //add reg1,reg2

    __op_format(0,OPCODE_ADD_RR,0,reg1,reg2,0,MOD_REG);
}

void op_add_RV(int reg,int iData){
    //add reg,iData

    int opcode_in_mod_rm=(char)0x00;

    __op_format(0,OPCODE_ADD_RV,0,opcode_in_mod_rm,reg,0,MOD_REG,iData);
}
void op_sub_RV(int reg,int iData){
    //sub reg,iData

    int opcode_in_mod_rm=(char)0x05;

    __op_format(0,OPCODE_SUB_RV,0,opcode_in_mod_rm,reg,0,MOD_REG,iData);
}

void op_push_R(int reg){
    //push reg

    //オペコード、レジスタ
    __op_format(0,OPCODE_PUSH_R,reg);
}

void op_push_V(int iData){
    //push 32ビット即値

    __op_format(0,OPCODE_PUSH_V,0,iData);
}

void op_call_M(long addr){
    //命令プリフィックス
    int op_prefix=(char)0xFF;

    __op_format(op_prefix,OPCODE_CALL_M,0,addr);
}

void op_ret(void){
    //オペコード
    __op_format(OPCODE_RET_0);
}

 それでは、ここまでで用意された関数を使用し、ネイティブコードを生成するプログラムを書いてみましょう。各種関数を呼び出すことで、コメントに記載したアセンブラコードと対をなす命令がNativeBufferバッファに書き込まれていきます。

 NativeBufferバッファは「ExeCreator.cpp」で定義している処理により、EXEファイルに埋め込まれます。

ネイティブコードを生成するcreate_native_buffer関数
void create_native_buffer(void){
    //ネイティブコードを生成

    //mov eax,10
    op_mov_RV(REG_EAX,10);

    //mov ecx,20
    op_mov_RV(REG_ECX,20);

    //add eax,ecx
    op_add_RR(REG_EAX,REG_ECX);

    //push eax
    op_push_R(REG_EAX);

    //push dword ptr["10+20=%d"]
    op_push_V(0x00403000);

    //push dword ptr["0123456789abcdef"]
    op_push_V(0x00403017);

    //call dword ptr[wsprintf in import_table]
    op_call_M(0x00402062);

    //add esp,sizeof(long)*3
    op_add_RV(REG_ESP,sizeof(long)*3);

    //push 0
    op_push_V(0);

    //push dword ptr["opcode test"]
    op_push_V(0x0040300A);

    //push dword ptr["0123456789abcdef"]
    op_push_V(0x00403017);

    //push 0
    op_push_V(0);

    //call dword ptr[MessageBoxA in import_table]
    op_call_M(0x0040205E);

    //ret 0
    op_ret();
}

まとめ

 ネイティブコードは1ビット単位で意味付けがされていることを実感できたでしょうか。このまま機械語の世界を突き進んでいってもよいのですが、今以上にディープな内容になりそうなので、まずはここで一区切りさせていただきます。

 次回からは気持ちを切り替え、デバッガの挙動を解説していきます。

修正履歴

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
Windows実行ファイル「EXE」の謎に迫る連載記事一覧

もっと読む

この記事の著者

山本 大祐(ヤマモト ダイスケ)

普段はActiveBasicと周辺ツールの開発を行っています。最近は諸先輩方を見習いながら勉強中の身。AB開発日記

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/420 2010/04/19 10:10

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング