C++ 通过Thunk在WNDPROC中访问this指针 [转]

本文基本只讨论原理,具体实现请参见后续文章《C++ 通过Thunk在WNDPROC中访问this指针实现细节

当注册窗口类时,WNDCLASSEX结构的lpfnWndProc成员应设置为窗口过程函数的地址,这是一个C风格的函数指针,所以我们只能使用全局或静态函数的地址,这在我们将窗口封装为C++类时会很麻烦,因为我们无法在一个全局或静态的WindowProc函数中直接访问类实例,这就需要一些手段了(MS的API设计着实不怎么样)

  第一种方案,建立一个HWND到C++类实例的映射表,在WindowProc中通过这个映射表从HWND得到C++类实例,由于可能有多线程安全问题,在访问这个映射表时可能涉及到线程同步,再加上可能应用程序要处理的消息频率十分高,从而带来性能问题(一般情况还是可以接受的)。
  另一种方案是通过SetWindowLongPtr/GWLP_USERDATA将类实例指针存放在窗口的用户数据字段中,这样就可以在WindowProc中通过调用GetWindowLongPtr/GWLP_USERDATA来获取类实例指针了,缺点就是当别人使用你的C++类时可能会不留神把你存放的this指针覆盖,从而导致不可预知的后果;此外每一条window消息都要调用GetWindowLongPtr这个系统API也是一点点额外开销。
  第三种也就是本文要讨论的方案,是传说中的Thunk方案。这也是MFC/ATL所使用的方现。Thunk在这里是指一小段代码,这段代码无法用C/C++来表示(因为是动态代码),只能用机器码写(汇编都不好使),这也就造成本方案在跨平台时有点小麻烦,好在Windows本身也支持不了几种CPU。这里仅以x86体系来讨论。

  首先说下x86下__stdcall调用约定。 Windows API要求窗口过程必须使用__stdcall调用约定。 该约定通过栈来传递参数,通过eax寄存器返回值。参数压栈顺序为从右到左。 那么对于窗口过程的定义

LRESULT (CALLBACK *WNDPROC)(HWND,UINT,WPARAM,LPARAM);

来看,当系统调用我们指定的窗口过程时,从右向左依次将LPARAM, WPARAM, UINT, HWND压入栈中,然后使用call指令进入窗口过程。 THUNK的目标就是在这个时候将栈上的HWND参数替换为C++类实例指针。看下此时的栈结构先

栈底
......
......
栈顶 + 7
栈顶 + 6
栈顶 + 5
栈顶 + 4    
4-7 原本存放着HWND参数,在执行完Thunk后,其值为类实例地址
-------------------------------------------
栈顶 + 3    
0-3 存放着窗口过程的返回地址,
栈顶 + 2    WindowProc里在return之后会返回到该地址继续运行
栈顶 + 1
栈顶 + 0

因为THUNK代码在运行时生成,此时C++类实例的地址已经确定,那么对于THUNK代码来说,类实例指针就是个立即数(常数)。 那么基本指令应该是

mov mov dword ptr [esp+0x4], $class_instance
jmp $real_window_proc

其中 $class_instance 是我们要填入的C++类实际的指针, 而$real_window_proc是我们真正的windowproc的地址,但该windowproc第一个参数不是HWND,而是C++类指针,也就是该函数应该类似于:

复制代码
LRESULT CALLBACK cpp_window_proc(cpp_window_class* thiz, UINT msg, WPARAM wParam, LPARAM lParam) {
    thiz->window_proc(msg, wParam, lParam); 
    /* 实际上thiscall正好是第一个(隐形)参数为this指针,
     * 所以这里也可以直接把 cpp_window_class::window_proc的地址作为$real_window_proc的值
     * 但那样对于使用虚函数的情况有些复杂, 所以最好还是用静态或全局函数转一下。
     */
}
复制代码

以下是一个实现这个机制的伪代码片段

复制代码
struct wndproc_thunk;
struct window
{
    HWND _handle;
    wndproc_thunk* _thunk;
    HRESULT WINAPI static static_window_proc(HWND, UINT, WPARAM, LPARAM);
    HRESULT WINAPI window_proc(UINT, WPARAM, LPARAM);
};

#pragma pack(push,1)
struct wndproc_thunk
{
     DWORD   mov;          // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
     DWORD   thiz;         //
     BYTE    jmp;          // jmp WndProc
     DWORD   relproc;      // relative jmp
};
#pragma pack(pop)


window win;
win._thunk = alloc_wndproc_thunk();
win._thunk->mov = 0x042444C7;                                        // mov dword ptr [esp+0x4],
win._thunk->thiz = &win;                                             // thiz
win._thunk->jmp = 0xe9;                                              // jmp
win._thunk->relproc = window::static_window_proc - (win._thunk + 1); // relproc
FlushInstructionCache(GetCurrentProcess(), win._thunk, sizeof(wndproc_thunk));
SetWindowLongPtr(win._handle, GWLP_WNDPROC, (LONG_PTR) win._thunk);
复制代码

最后补充一句,因为新版Windows及最新的Server Packs都加入了数据执行保护(DEP)功能,因此如果直接在堆或栈上分配空间构造thunk的话,因为堆和栈所在内存都被默认标记为不可执行,从而导致系统异常。这里就需要VirtualAlloc方法动态为thunk分配内在,并使用PAGE_EXECUTE_READWRITE标志,记得最后使用VirtualFree释放该内存。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值