当注册窗口类时,WNDCLASSEX结构的lpfnWndProc成员应设置为窗口过程函数的地址,这是一个C风格的函数指针,所以我们只能使用全局或静态函数的地址,这在我们将窗口封装为C++类时会很麻烦,因为我们无法在一个全局或静态的WindowProc函数中访问类实例,这时Thunk技术可以大显身手了
我们先分析一下其它的解决方案:一种是建立一个HWND到C++类实例的映射表,在WindowProc中通过这个映射表从HWND得到C++类实例。如果我们不考虑那一点点性能损失的话(每个消息在处理时都要查一次映射表)我想这也是个不错的办法(我们不考虑WM_NCDESTROY消息出现两次的变态情况)
而另有一种方式是通过WndExtra来存放类实例指针,这样就可以在WindowProc中通过调用GetWindowLong来获取类实例指针了,缺点就是当别人使用你的C++类时如果不知道你用的WndExtra,他可能会把其它数据放到WndExtra中,于是后果不可设想
现在让我们来看看Thunk方案吧,据我了解MFC/ATL都是基于这种方案实现。Thunk在这里是指一小段代码,可惜的是这段代码无法用C/C++来表示(因为是动态代码),只能用机器码写(汇编都不好使),这也就造成本方案应对多平台有点名义上的不完美。这里仅以i386体系来讨论。
我们先假设一个C++窗口类
struct _THUNK;
class CWnd
{
m_hWnd;
HRESULT WINAPI static StdWindowProc(HWND, UINT, WPARAM, LPARAM);
HRESULT WINAPI WindowProc(UINT, WPARAM, LPARAM);
_THUNK* pThunk;
}
_THUNK结构定义如下:
#pragma pack(push,1)
struct _THUNK
{
DWORD m_mov; // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
DWORD m_this; //
BYTE m_jmp; // jmp WndProc
DWORD m_relproc; // relative jmp
};
#pragma pack(pop)
该结构实质是一段CPU指令,我将来要将其设置为窗口过程,这段指令将传入的第一个参数改为m_this(m_this是CWnd实例的this指针),然后由一个jmp指令跳转到代码地址m_relproc,这里m_relproc的值将是静态WindowProc函数的地址,当WindowProc将第一个参数取出时,这个参数已经不再是HWND,而是CWnd*了,这时就可以间接调用CWnd的实例成员了。
实例化_THUNK结构的代码如下:其中pThis指CWnd实例的指针,proc指静态StdWindowProc的地址,pThunk指_THUNK结构实例地址
m_mov = 0x042444C7; //C7 44 24 0C
m_this = PtrToUlong(pThis);
m_jmp = 0xe9;
m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)pThunk+sizeof(_THUNK)));
FlushInstructionCache(GetCurrentProcess(), pThunk, sizeof(_THUNK));
最后这句是告诉CPU清除当前指令缓存,因为我们更改了“代码”。m_relproc的值是proc到thunk下一条指令(也就是thunk后边紧挨着的那个地址)的相对地址偏移。m_mov与m_this合起来是:mov dword ptr[esp+0x4], pThis,意思是将 [栈顶+4] 地址处的4字节(DWORD)的值为pThis,也就是将第一上参数hWnd的值改为pThis。下面是栈的结构
栈底
......
......
栈顶 + 7
栈顶 + 6
栈顶 + 5
栈顶 + 4 4-7 原本存放着hWnd参数,在执行完Thunk后,其值为CWnd实例地址
-------------------------------------------
栈顶 + 3 0-3 存放着窗口过程的返回地址,
栈顶 + 2 WindowProc里在return之后会返回到该地址继续运行
栈顶 + 1
栈顶 + 0
因为Thunk执行后栈结构只是第一个参数被更改,返回地址并没有被破坏,所以StdWindowProc并不知道Thunk的存在而错误的认为自己是被直接调用,其内部的return语句会按栈顶的返回地址返回。
理论解决了,看一下流程,
创建并初始化CWnd::m_pThunk
先注册一个windows窗口类其中WNDCLASSEX的lpfnWindowProcx字段为CWnd::StdWindowProc的地址
然后用CreateWindowEx创建窗口,最后一个参数设置为当前CWnd实例的this指针的值
在StdWindowProc中我们响应WM_NCCREATE消息,从lParam参数指向的CREATESTRUCT结构体的lpCreateParams字段中取出CWnd的实例指针,通过SetWindowLong将窗口过程更改为CWnd实例的m_pThunk的值,然后手动调用CWnd实例的WindowProc
在StdWindowProc中响应任何其它消息时,我们将参数hWnd强制类型转换为CWnd*,将通过这个指针调用CWnd::WindowProc。
最扣补充一句,因为新版Windows或最新的Server Packs都加入了数据执行保护功能,按上面的方法弄很可能出现异常,因为Thunk是数据结构,会被放在数据段中并被标记不不可执行。解决办法之一是使用VirtualAlloc方法动态为thunk分配内在,并使用PAGE_EXECUTE_READWRITE标志,记得最后使用VirtualFree释放该内在。