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

来源:互联网 发布:mac上的作图软件 编辑:程序博客网 时间:2024/05/21 18:36

本文基本只讨论原理,具体实现请参见后续文章《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_instancejmp $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;                                             // thizwin._thunk->jmp = 0xe9;                                              // jmpwin._thunk->relproc = window::static_window_proc - (win._thunk + 1); // relprocFlushInstructionCache(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释放该内存。