実行時にコードを操作する小さなdemo(二)


前編は、スタックに空間を割り当て、実行可能なコードを生成し、実行することを示す.一般的に実行可能なコードはコードセグメントにあり、スタック上でもデータセグメントでも「正常にコンパイル」されたコードではありません.今回はデータセグメントにコードを入れる例を示してみましょう~
コードは以下の通りです.前編同様、対応するマシンコードやアセンブリをコメントに書いてあり、分かりやすいです.
#include <stdio.h>

typedef int (*puts_ptr)(const char*);
typedef void (*myfunc_ptr)(puts_ptr);

/*
void foo(puts_ptr p) {
    p("greetings from generated code!");
}
*/
/* code as string in data section:
offset | bytes (in hex) | mnemonics
00     | 55             | push EBP
01     | 8BEC           | mov  EBP, ESP
03     | E8 00000000    | call next instruction
08     | 58             | pop  EAX
09     | 83C0 0D        | add  EAX, 0D
12     | 50             | push EAX
13     | FF55 08        | call dword ptr [EBP + 8]
16     | 83C4 04        | add  ESP, 04
19     | 5D             | pop  EBP
20     | C3             | ret
*/

int main() {
    myfunc_ptr pMyfunc;
    puts_ptr pPuts = &puts;
    const char* code = "\x55\x8B\xEC\xE8\x00\x00\x00\x00"
                       "\x58\x83\xC0\x0D\x50\xFF\x55\x08"
                       "\x83\xC4\x04\x5D\xC3"
                       "greetings from generated code!";
    
    pMyfunc = (myfunc_ptr)code;
    pMyfunc(pPuts);
    
    return 0;
}

この例の鍵は、ソースコードの文字列の字面量である奇妙なxNNとその後の挨拶文の山であることがわかります.C/C++に隣接する文字列の字面量がコンパイラによって1つの文字列定数に統合されることを忘れないでください.Windowsの
PEファイルは、コードセグメント(一般に.text section)とデータセグメント(一般に.data sectionと.rdata section;後者は読取り専用データセグメント)を区別し、この文字列の字面量はデータセグメントに配置される.
この例では、データセグメントに配置されたコードの構造は、前のスタックに配置されたコードと基本的に同じであり、コードと必要なデータを混在させ、前はコードであり、文字列データはコードの後に続く.
異なるのは、前編で生成されたコードの3番目の命令はpush imm 32であり、スタックに圧入されたのは直接量である.この直接量は,コード生成時に相対オフセット量により算出される.この例では,ベースアドレスを取得するために小さなtrickを用い,直接コード内でpushの値を計算し,圧入スタック命令を用いてpush EAXである.このtrickは以下の命令シーケンスです.
offset | bytes (in hex) | mnemonics
03     | E8 00000000    | call next instruction
08     | 58             | pop  EAX
09     | 83C0 0D        | add  EAX, 0D

まずcall imm 32命令で、パラメータは0 x 00000000です.このコマンドの意味は、次のコマンドのアドレスを戻りアドレスとしてスタックに押し込み、次のコマンドのアドレス+0 x 00000000の位置にジャンプすることです.この命令の32ビット直接量は2の補符号であり、記号付きであることに注意してください.このコマンドを実行した後、CPUにcallコマンドの次のコマンドのアドレスをスタックに押し付けて、「ベースアドレス」がどれだけあるかを知ることができます.
次はpop EAXで、さっきスタックに押した「戻りアドレス」を弾き出し、EAXに値を割り当てます.
そしてadd EAX、imm 8です.命令の長さを数えると、表示する文字列距離pop EAX命令のオフセット量は13、すなわち0 x 0 Dであることがわかる.そこで,先に得られた「ベースアドレス」にこのオフセット量を加えると,表示する文字列の先頭アドレスが得られる.
後の命令シーケンスは基本的に前編と同じで、あまり説明する必要はありません.
このようなアドレスを取得するtrickは、再配置可能なコード(relocatable code)でよく見られる.例えば、コンパイラがDLLを生成する場合、実際に実行するときにローダがDLLイメージをどのアドレスにロードするか分からないので、アドレスに関連するコードはベースアドレス+オフセット量の形式にコンパイルされなければなりません.ベースアドレスの取得は、類似のtrickで行われることが多い.
前編ではWindows VistaとWindows 7のDEPについて述べましたが、データセグメントの実行性にも制限があります.データセグメントメモリは
実行できません.この例では、上記の2つのオペレーティングシステム(および対応するサーバ版)で直接コンパイルして実行するとエラーが発生します.これらのシステムは、実行可能なコードをコードセグメントに保存すべきであると考えているので、前例の文字列に置かれたコードもコードセグメントに移動すべきである.
スタックとデータセグメントにコードを入れるのはどういう意味ですか?時々Cのswitchのように...Case文はテーブルベースのジャンプにコンパイルされますが、このジャンプテーブルは命令シーケンスに混在しています.コードとデータには明らかな境界はありません.フォン・ノイマンアーキテクチャの機械では、メモリはコードを保存するためにも、データを保存するためにも使用できます.コードはデータであり、データはコードとして実行することができる.この編と前編のdemoは、それをデモンストレーションしただけです.すなわち、汎用的で効率の低いコードを使用するのではなく、実行時の特定の条件に基づいて効率的で特化したコードを動的に生成することを望む場合があります.この場合、動的コード生成技術が必要です.生成されたコードはメモリに入れてあり、実行できないと面白くありません.この2つのdemoは,動的生成コードが実行可能であることを示した.
また今度书きますね…お茶を饮みに行きます~