この回答では、必要なサンク コードを生成する一般的な方法について説明しました。演習として、あなたの場合に合わせてやり直しましょう。
クラスが次のように定義されているとします。
struct YourClass {
LRESULT YourMemberFunc(int nCode, WPARAM wParam, LPARAM lParam);
};
実際のアドレスのプレースホルダーを使用して、サンクを C++ で記述します。
LRESULT CALLBACK CallWndProc(int nCode, WPARAM wParam, LPARAM lParam) {
YourClass *x = reinterpret_cast<YourClass*>(0x1122112211221122);
__int64 im = 0x3344334433443344;
LRESULT (YourClass::*m)(int,WPARAM,LPARAM) = *reinterpret_cast<LRESULT (YourClass::**)(int,WPARAM,LPARAM)>(&im);
return (x->*m)(nCode, wParam, lParam);
}
そして、コンパイラが呼び出しをインライン化しないように呼び出します。
int main() {
LRESULT (CALLBACK *volatile fp)(int, WPARAM, LPARAM) = CallWndProc;
fp(0, 0, 0);
}
リリースでコンパイルし、生成されたアセンブリを確認します (Visual Studio では、デバッグ中にアセンブリ ウィンドウを確認し、[コード バイトの表示] をオンにします)。
4D 8B C8 mov r9,r8
4C 8B C2 mov r8,rdx
8B D1 mov edx,ecx
48 B9 22 11 22 11 22 11 22 11 mov rcx,1122112211221122h
48 B8 44 33 44 33 44 33 44 33 mov rax,3344334433443344h
48 FF E0 jmp rax
これはサンクになり、実行時44 33 44 33 44 33 44 33
にメンバーへのポインター ( &YourClass::YourMemberFunc
) に22 11 22 11 22 11 22 11
置き換えられ、実際のオブジェクト インスタンスへのポインターに置き換えられます。
サンクで何が起こっているかの説明
x64 呼び出し規則 (Windows には 1 つしかありません) では、最初の 4 つのパラメーターはrcx, rdx, r8, r9
、左から右の順序でレジスターに渡されます。したがって、サンクが呼び出されると、
rcx = nCode, rdx = wParam, r8 = lParam
メンバー関数の場合this
、ポインターを保持する暗黙の最初のパラメーターがあるため、入力するときにYourMemberFunc
持っている必要があります
rcx = this, rdx = nCode, r8 = wParam, r9 = lParam
コンパイラによって生成されたコードは、まさにこの調整を行います。つまり、 をシフトし、プレースホルダーを にr8 -> r9, rdx -> r8, ecx -> edx
割り当てます。これでパラメーターが設定され、関数自体への間接的なジャンプを続行できます。戻り値を保持するために使用されるため、関数呼び出し間で保持する必要はありません。これが、宛先アドレスを一時的に保持するためにここで使用されている理由です。これにより、テール コールの最適化 (コールとリターンのペアが単一のジャンプに置き換えられます) の機会が与えられます。this = 1122112211221122
rcx
rax
なぜ間接呼び出しを行う必要があるのでしょうか。そうしないと、相対的なジャンプが発生するためです。しかし、サンクがメモリ内の異なるアドレスにコピーされるため、ハードコーディングされた相対ジャンプを使用することはできません! したがって、実行時に絶対アドレスを設定し、代わりに間接ジャンプを行うことに頼っています。
HTH