我在这个答案中描述了一种生成您想要的任何thunk 代码的通用方法。让我们为您的案例重做它,作为练习。
假设您的类定义为:
struct YourClass {
LRESULT YourMemberFunc(int nCode, WPARAM wParam, LPARAM lParam);
};
用 C++ 编写您的 thunk,并使用实际地址的占位符:
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);
}
在 release 中编译并查看生成的程序集(在 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
这将是您的 thunk,在运行时44 33 44 33 44 33 44 33
替换为指向您的成员 ( &YourClass::YourMemberFunc
) 的指针并22 11 22 11 22 11 22 11
替换为指向实际对象实例的指针。
thunk 中发生的事情的解释
在 x64 调用约定中(在 Windows 上只有一个),前四个参数rcx, rdx, r8, r9
按从左到右的顺序在寄存器中传递。所以当我们的 thunk 被调用时,我们有
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
用于保存返回值,因此不必在函数调用之间保留。这就是为什么这里使用它来临时保存目标地址,这为尾调用优化提供了机会(一个调用/返回对替换为单跳转)。
为什么我们必须进行间接调用?因为否则我们会得到一个相对的跳跃。但是我们不能使用硬编码的相对跳转,因为 thunk 将被复制到内存中的不同地址!因此,我们求助于在运行时设置绝对地址并进行间接跳转。
高温高压