SHOEISHA iD

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

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

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

AMD64の特徴と機械語コーディング

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

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

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

 なんか、この連載では、おなじみになってきましたね。今回も負けじと64ビットバイナリを生成するプログラムを組んでみます。『x86系CPUのネイティブコードを解析する』で作成したソースコード(ExeCreator)をベースに、コーディングを行います(ここでは新しいプロジェクト名を「ExeCreator64」としています)。

 ただし、一つだけ注意点があります。それは64ビットバイナリが動作する環境がないと、実際に生成されたEXEファイルの動作確認が取れないこと(当たり前ですが)。裏を返せば、64ビット版Windowsをお持ちの方には、ぜひとも試してもらいたいサンプルプログラムです。

EXEのPEヘッダ生成部分の変更点

 64ビット版のEXEファイルは、内部のコードセクション(.text)の内容が、AMD64対応のネイティブコードになっただけではありません。当然ながら、PEヘッダにおけるポインタ周りのデータは、32ビットから64ビットへ拡張がなされています。

 ネイティブコードに関する話題に入る前に、これらPEヘッダに関する部分の生成コードが、以前のExeCreatorのソースコードとどのような部分で違うのかを紹介していきます。

 まず、IMAGE_NT_HEADERS構造体がIMAGE_NT_HEADERS64という名前に変化します。IMAGE_NT_HEADERS64構造体の中にはIMAGE_OPTIONAL_HEADER64構造体(オプショナルヘッダを保有)が含まれており、イメージベースの指定やスタック領域、ヒープ領域に関するメンバが64ビット化されています。メンバ変数が64ビット化されているだけですので、書き込む値の内容には変化がない場合がほとんどです。

 マシン識別IDをIMAGE_FILE_MACHINE_AMD64に変化させている部分に注目しましょう。

IMAGE_NT_HEADERS64の定義とマシン識別ID
IMAGE_NT_HEADERS64 ImagePeHdr;
ImagePeHdr.Signature=IMAGE_NT_SIGNATURE;

//マシンタイプ
ImagePeHdr.FileHeader.Machine=         IMAGE_FILE_MACHINE_AMD64;

 オプショナルヘッダが64ビット化されたことで、当然のように構造体のサイズにも違いが生じます。また、その直下で指定するEXE特性やマジックナンバーも下記のコードのように変化しています。

 EXE特性に関しては、32ビットコードであることを示すIMAGE_FILE_32BIT_MACHINEフラグが省かれると同時に、2GB以上のメモリ空間を利用可能にするためのIMAGE_FILE_LARGE_ADDRESS_AWAREフラグが追加されます。

ヘッダサイズとEXE特性値
ImagePeHdr.FileHeader.SizeOfOptionalHeader= IMAGE_SIZEOF_NT_OPTIONAL64_HEADER;
ImagePeHdr.FileHeader.Characteristics= IMAGE_FILE_EXECUTABLE_IMAGE|
                                       IMAGE_FILE_LARGE_ADDRESS_AWARE;
ImagePeHdr.OptionalHeader.Magic=       IMAGE_NT_OPTIONAL_HDR64_MAGIC;

 この他、IMAGE_OPTIONAL_HEADER64構造体で注意すべき点と言えばBaseOfCodeメンバが省かれ、そこを埋めるかのようにImageBaseメンバが64ビット化されている部分が挙げられます。PEヘッダのフォーマットは昔からのものなので、できれば崩したくないという意図が見え隠れしているような気がします(筆者の考えすぎでしょうか)。

 以上のように、PEヘッダ周りはサイズに気をつければ特段問題ありません。とはいっても、当然ながらミスはつきもの。うまくいかない場合は、「dumpbin.exe」を用いてPEヘッダが正常に書き込まれているのかをチェックしてみるのも一つの手です。ファイルの書き込み部分で、データ自体は64ビット化されていても、書き込みバッファサイズが32ビットのままだったといった事態も考えられます。デバッグ不可能なPEヘッダ周りに関して、「dumpbin.exe」は最強の威力を発揮するものだということを覚えておきましょう(筆者は何度も助けられています)。

インポートセクション生成部分の変更点

 インポートセクション(.idata)に存在するルックアップテーブルおよびインポートテーブルが64ビット化されている点に注意が必要です。

64ビット化されるルックアップ・インポート テーブル
64ビット化されるルックアップ・インポート テーブル

機械語コーディングの規則

 x86からAMD64への変化において、機械語フォーマット自体の規則は高い互換性が保たれていますが、そのデータの内容に多少なりとも変更点があるようです。筆者が確認しただけで、下記のような違いが生じています。このような細かい部分を知っておくだけでもデバッグの回数が減るので、ぜひともおさえておきたい内容です。

call命令の32ビット値

 call命令に引き渡す32ビット値(インポートアドレステーブル内のデータを示す)が「RVAからのオフセット」から「実行中アドレスからのオフセット」に変更されています。

push/popは事実上使われない!?

 VC++を利用してAMD64対応のネイティブコードを生成すると確認できるのですが、x86の環境では多用されているpushpopの出現率が著しく低下します。筆者が確認した限りでは、一回も出てきませんでした。ではpush/popはどのような命令に置き換わっているのでしょうか。

 x86では、

push rax
pop rax

 とやるところをAMD64では、

mov qword ptr[rsp+offset],rax
mov rax,qword ptr[rsp+offset]

 とするのが正しいようです。このmov命令の前後で

sub rsp,frame_size
;モジュールとしての何らかの処理
add rsp,frame_size

 という具合にスタックフレームを確保します。

 結局のところ、モジュールの開始部分で必要なだけrspを減算、終了部分でrspの値を元に戻すという方法でスタックフレームの確保を行っています。スタックの内容を読み書きしたいときは、rspからのオフセット値という形でmov命令を呼び出すというのがAMD64の作法のようです。

 こうすることにより、rbp(ベースポインタ)の出番が無くなる(=緊急用の汎用レジスタが増える)ことや、rspの加減算を行わなくて良いため、必要なクロック数が減るなどのメリットが生じているのだと予測します(あくまでも筆者の予測ですので、本当のところは分かりません)。

パラメータ引渡しにレジスタを利用

 x86で言うfast_callと同様の方式ですが、AMD64では第1パラメータ~第4パラメータをrcxrdxr8r9を介して引き渡すようになります。パラメータを引き渡すだけでメモリアクセスが生じることがなくなるので、速度的には大きなメリットになることと思います。第5パラメータ以降の値は従来どおり、スタックフレームに積まれます。

128ビット境界に気をつける

 しばしば、DLL関数内でmovdqaなどの128ビット命令が呼び出される場合があります。このmovdqa命令、rspからのオフセットをオペランドに指定することがあるのですが、そのときに指定したアドレスが128ビット境界でなければ強制終了してしまうという非常に困った自体に陥ります(さすがにどのDLLでこのような128ビット命令が呼ばれるのかは知る由もありません)。そこでモジュールの開始時と終了時にrspに対して加減算を行う際に、16で割ると8余る数をrspの値としておく必要があります。なぜ8で割り切れる値かと言うと、DLL関数をcallしたときに、ret用の処理の戻り先アドレスとして、rspが8バイト加減算されるためです。

 ちなみに、これから出てくるサンプルではモジュールの開始時にsub rsp,88という命令を実行しており、88バイト分のスタックフレームが利用可能になっています。これも88÷16=5(余り8)という法則にのっとっています。この部分を80などに変更してプログラムを実行してみると、上記の内容を直感的に理解できるかもしれません。

AMD64対応のネイティブコード生成

 さて、気持ちを新たにしてAMD64アーキテクチャに対応するネイティブコードを生成すべく、「opcode.cpp」に手を加えていきます。やり方はx86のときと同じ。今回はREXプリフィックスに重点を置き、64ビット命令の有効化を行います。

 まずは拡張されたレジスタの識別番号の定義から。REG_EAXREG_RAXなどという表記に変更している点にも注意しておきましょう。

拡張された汎用レジスタを識別できるようにする
//レジスタを示す定数
#define REG_NON    -1
#define REG_RAX    0x00    //reg:000
#define REG_RCX    0x01    //reg:001
#define REG_RDX    0x02    //reg:010
#define REG_RBX    0x03    //reg:011
#define REG_RSP    0x04    //reg:100
#define REG_RBP    0x05    //reg:101
#define REG_RSI    0x06    //reg:110
#define REG_RDI    0x07    //reg:111
#define REG_R8     0x08    //reg:000(REXプリフィックス)
#define REG_R9     0x09    //reg:001(REXプリフィックス)
#define REG_R10    0x0A    //reg:010(REXプリフィックス)
#define REG_R11    0x0B    //reg:011(REXプリフィックス)
#define REG_R12    0x0C    //reg:100(REXプリフィックス)
#define REG_R13    0x0D    //reg:101(REXプリフィックス)
#define REG_R14    0x0E    //reg:110(REXプリフィックス)
#define REG_R15    0x0F    //reg:111(REXプリフィックス)

 下記のソースコードはREXプリフィックスを生成するための関数です。オペランドサイズ(op_size)、各種指定レジスタが拡張レジスタ(r8r15)であるかどうかなどでAMD64の拡張アーキテクチャが有効になる命令であるのかが判別され、適切なREXプリフィックスが生成される仕組みになります。もちろん、オペランドを識別して拡張する必要がなければ(x86でも通用する命令であれば)、この関数がREXプリフィックスを生成することはありません。

REXプリフィックス生成モジュール
void set_rex(int op_size, int reg, int index_reg, int base_reg){
    char RexByte;

    if(reg==REG_NON&&index_reg==REG_NON){
        /////////////////////////////////////
        // レジスタをr/mのみに指定するとき
        /////////////////////////////////////

        if((base_reg&0x08)==0){
            if(op_size==sizeof(char)&&(base_reg&0x04)){
                // r/m に spl,bpl,sil,dilを指定するとき
                RexByte=0x40;
            }
            else RexByte=0;
        }
        else RexByte=(char)0x41;
    }
    else{
        /////////////////
        // 通常
        /////////////////

        if((reg&0x08)==0){
            //reg … rax~rdi

            if((index_reg&0x08)==0){
                if((base_reg&0x08)==0) RexByte=0;
                else RexByte=(char)0x41;
            }
            else{
                if((base_reg&0x08)==0) RexByte=(char)0x42;
                else RexByte=(char)0x43;
            }
        }
        else{
            //reg … r8~r15

            if((index_reg&0x08)==0){
                if((base_reg&0x08)==0) RexByte=(char)0x44;
                else RexByte=(char)0x45;
            }
            else{
                if((base_reg&0x08)==0) RexByte=(char)0x46;
                else RexByte=(char)0x47;
            }
        }
    }

    if(op_size==sizeof(_int64)){
        //64ビットオペランド
        RexByte|=0x48;
    }

    if(RexByte) NativeBuffer[iNativeLength++]=RexByte;
}

 次に、以前のソースコードにset_rex関数の呼び出し手順を加えて、64ビット拡張された機械語を生成できるようにしてみましょう。

__op_format関数の内容にset_rex関数を適用させたところ
void __op_format(int op_size, char op_prefix,
    char opcode, int reg, int const_value){
    //命令プリフィックス
    if(op_prefix) NativeBuffer[iNativeLength++]=op_prefix;

    //rexプリフィックス
    if(op_size!=-1) set_rex(op_size,REG_NON,REG_NON,reg);

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

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

    //rexプリフィックス
    set_rex(op_size,reg,0,base_reg);

    //オペコード
    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(int op_size, 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;

    //rexプリフィックス
    set_rex(op_size,reg,0,base_reg);

    //オペコード
    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関数の呼び出し部分も変更し、今回利用する一通りの命令生成モジュールを定義していきます。オペランドが64ビットであることを示す第1パラメータに気を配れば、特段注意すべき点はありません。

64ビットに対応した命令生成モジュール
void op_mov_RV(int reg, int iData){
    //mov reg,value

    __op_format(sizeof(_int64),0,(char)0xC7,0,0,reg,0,MOD_REG,iData);
}
void op_mov_RR(int reg1, int reg2){
    //mov reg,reg2

    __op_format(sizeof(_int64),0,(char)0x8B,0,reg1,reg2,0,MOD_REG);
}

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

    __op_format(sizeof(_int64),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(sizeof(_int64),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

    if(reg==REG_RAX){
        //raxのみ特殊
        __op_format(sizeof(_int64),0,(char)0x2D,0,iData);
    }
    else{
        int opcode_in_mod_rm=(char)0x05;

        __op_format(sizeof(_int64),0,(char)0x81,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(-1,0,OPCODE_PUSH_V,0,iData);
}

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

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

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

 最後に、仕上げのネイティブコード生成のための処理を記述します(インラインアセンブラを書いているような錯覚が生じますが、気にしないでください)。

ネイティブコード生成モジュール
void create_native_buffer(void){
    //ネイティブコードを生成

    //sub rsp,88
    //※スタックフレームの確保(wsprintfが_cdecl呼び出しのため、
    //                          予めパラメータ用の領域が必要)
    op_sub_RV(REG_RSP,88);

    //mov rax,10
    op_mov_RV(REG_RAX,10);

    //mov rcx,20
    op_mov_RV(REG_RCX,20);

    //add rax,rcx
    op_add_RR(REG_RAX,REG_RCX);

    //mov r8,rax
    op_mov_RR(REG_R8,REG_RAX);

    //mov rdx,dword ptr["10+20=%d"]
    op_mov_RV(REG_RDX,0x00403000);

    //mov rcx,dword ptr["0123456789abcdef"]
    op_mov_RV(REG_RCX,0x00403019);

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

    //mov r9,0
    op_mov_RV(REG_R9,0);

    //mov r8,dword ptr["64opcode test"]
    op_mov_RV(REG_R8,0x0040300A);

    //mov rdx,dword ptr["0123456789abcdef"]
    op_mov_RV(REG_RDX,0x00403019);

    //mov rcx,0
    op_mov_RV(REG_RCX,0);

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

    //add rsp,88
    //※スタックフレームの解放
    op_add_RV(REG_RSP,88);

    //ret 0
    op_ret();
}

実行してみよう

 ExeCreator64プロジェクトをコンパイルし、出来上がった「ExeCreator64.exe」を実行してみましょう。

 x86のときと同様、生成された「C:\test.exe」を実行し、下記のメッセージが表示されれば成功です。「test.exe」は必ず64ビット版Windows上で動かしましょう。

test.exeの実行結果
test.exeの実行結果

 64ビット環境が無くて「test.exe」を動作させることができないが、それでもAMD64対応のアプリケーションに興味があるという有望者は、生成された「test.exe」に対してdumpbinしてみましょう。ヘッダ部分だけでも、きちんと64ビット化されたことが確認できるはずです。

dumpbinの結果
Dump of file c:\test.exe

PE signature found

File Type: DLL

FILE HEADER VALUES
            8664 machine (x64)
               3 number of sections
        449FE329 time date stamp Mon Jun 26 22:37:45 2006
               0 file pointer to symbol table
               0 number of symbols
              F0 size of optional header
            2022 characteristics
                   Executable
                   Application can handle large (>2GB) addresses
                   DLL

OPTIONAL HEADER VALUES
             20B magic # (PE32+)
            4.00 linker version
              3F size of code
              2A size of initialized data
               0 size of uninitialized data
            1000 entry point (0000000000401000)
            1000 base of code
          400000 image base (0000000000400000 to 0000000000403FFF)
...

まとめ

 AMD64の世界を十分に堪能していただけたでしょうか。筆者の想定では、今回が一番ディープな内容だと察しています。広まりつつあるとはいうものの、ソフトウェアが追いついていない現状を抱えるAMD64、今回紹介しきれなかったテクニック、まだまだ発見されていない技術が64ビットにはあるものと信じています。

 次回はAMD64にも通じる部分である、SSE2について掘り下げていきます。

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

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

もっと読む

この記事の著者

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

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

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

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

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/457 2006/07/28 00:00

おすすめ

アクセスランキング

アクセスランキング

イベント

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング