探秘窗口过程函数(WndProc)的Thunk技术

来源:互联网 发布:花生壳域名怎么解析 编辑:程序博客网 时间:2024/05/29 00:32

最近看了一篇讲ATL Thunk技术的好文章(下载),收获较多,在此做一个总结。

Thunk技术的由来

我们知道,经典的Windows程序设计采用纯Windows API来实现,创建一个窗口必须严格遵循“定义窗口类,注册窗口类,创建窗口,显示窗口,更新窗口,启动消息循环”的步骤。虽然经典的Windows程序设计在一定程度上已经体现出了OOP的思想 (对象 = 数据 + 代码),但是与当代的OOP (封装、继承、多态) 还有很大的差距。这种差距类似于C struct和C++ class之间的差距。

为了使Windows编程更加简洁高效,需要对纯Windows API进行封装。市场上的应用程序框架,比如MFC,WTL等,主要就做了这件事情。

按照什么思路来对纯Windows API进行封装呢?那就是对各个函数进行分组,比如许多函数的第一个参数都是HWND hwnd。何不把HWND hwnd作为类的成员变量,调用与hwnd相关的函数就不必再写hwnd参数了。这样就完成了第一步。

接下来问题来了:WndProc函数怎么处理呢?它不能是普通的类方法,因为编译器会给普通类方法悄悄添加一个this指针,从而导致参数格式与WNDPROC不相同。有办法,那就是在方法前面添加static,这样就能将类的WndProc传递给窗口类的lpfnWndProc字段了。这样就完成了第二步。

可是,真正问题才刚刚出现。WndProc现在是类的static方法,但是如果想要WndProc内部调用非static方法呢?要是能够把this指针传递进去就好了。首先想到的方法就是用全局变量。(不然就要增加函数参数,那句跟WNDPROC不同了) 使用全局变量有很大的弊端,那就是这个全局变量到底应该设多大呢?因为你很可能用相同的窗口类创建多个窗口,这时候WndProc都一样,只能以类对象实例的指针来区分。也有解决办法,大不了用动态内存。还有查找的问题呢?用Red-Black tree够快么?显然,这样思考下去就太复杂。就算花了大力气实现了这种思路,动态内存太慢了,查找也得花时间,这跟ATL的要求不吻合:速度快,尺寸小。

实际上ATL采用了另外一种技术来解决这个问题,当然就是“Thunk技术”。Thunk技术看似高深,那是因为关注它的人不多,资料不够充足而已。

Thunk技术的实质

Thunk就是“转换”的意思,从ATL的实现来看,它是一种采用小段汇编代码对WndProc参数hwnd进行偷梁换柱的方法。不要看到汇编就却步,因为它只涉及到两条指令:mov和jmp。博主对汇编也只停留在本科时所学的那点知识,而且忘得差不多,但是理解Thunk技术已经足够。

Thunk技术的模拟

话不多说,下面是一个Thunk技术的模拟程序。窗口类的定义在zwindow.h头文件中,WinMain入口在simpleframework.cpp文件中。

// zwindow.h// 2012-11-17 by btwsmile// globalclass ZWindow;ZWindow* g_pWnd = NULL;#pragma pack(push, 1)typedef struct _WndProcThunk{DWORD m_mov;DWORD m_this;BYTE m_jmp;DWORD m_relproc;}THUNK;#pragma pack(pop)class ZWindow{public:THUNK m_thunk;HWND m_hwnd;void Init(WNDPROC proc, void* pThis){m_thunk.m_mov = 0x042444c7;m_thunk.m_this = (DWORD)pThis;m_thunk.m_jmp = 0xe9;m_thunk.m_relproc = (int)proc - ((int)&m_thunk + sizeof(m_thunk));FlushInstructionCache(GetCurrentProcess(), &m_thunk, sizeof(m_thunk));}public:ZWindow() : m_hwnd(NULL){}BOOL Create( LPCTSTR szClassName,LPCTSTR szWindowName,HINSTANCE hInstance,HWND hWndParent = NULL,DWORD dwStyle = WS_OVERLAPPEDWINDOW,HMENU hMenu = 0, int x = 200, int y = 200, int iWidth = 610, int iHeight = 420 ){HWND hwnd = CreateWindow(szClassName, szWindowName, dwStyle, x, y, iWidth, iHeight,hWndParent, hMenu, hInstance, NULL);if(hwnd != m_hwnd)MessageBox(NULL, _T("hwnd != m_hwnd"), _T("ERROR"), MB_OK);return hwnd != NULL;}void Attach(HWND hwnd){m_hwnd = hwnd;}BOOL ShowWindow(int iCmdShow){return ::ShowWindow(m_hwnd, iCmdShow);}BOOL UpdateWindow(){return ::UpdateWindow(m_hwnd);}HDC BeginPaint(LPPAINTSTRUCT lpps){return ::BeginPaint(m_hwnd, lpps);}BOOL EndPaint(LPPAINTSTRUCT lpps){return ::EndPaint(m_hwnd, lpps);}BOOL GetClientRect(LPRECT lpRect){return ::GetClientRect(m_hwnd, lpRect);}virtual BOOL OnPaint(WPARAM wParam, LPARAM lParam){PAINTSTRUCT ps;RECT rect;HDC hdc;hdc = BeginPaint(&ps);GetClientRect(&rect);rect.top += 20;DrawText(hdc, _T("Parent"), -1, &rect, DT_SINGLELINE | DT_CENTER);EndPaint(&ps);return TRUE;}virtual void OnLButtonUp(WPARAM wParam, LPARAM lParam){MessageBox(m_hwnd, _T("Press Parent"), _T("Info"), MB_OK | MB_ICONINFORMATION | MB_APPLMODAL);}// StartWndProcstatic LRESULT CALLBACK StartWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){ZWindow* pThis = g_pWnd;pThis->m_hwnd = hwnd;pThis->Init(WndProc, pThis);WNDPROC WindowProcess = (WNDPROC)&(pThis->m_thunk);SetWindowLong(hwnd, GWL_WNDPROC, (LONG)WindowProcess);return WindowProcess(hwnd, message, wParam, lParam);}// WndProcstatic LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){ZWindow* pThis = (ZWindow*)hwnd;switch(message){case WM_PAINT:pThis->OnPaint(wParam, lParam);break;case WM_LBUTTONUP:pThis->OnLButtonUp(wParam, lParam);break;case WM_DESTROY:PostQuitMessage(0);break;}return DefWindowProc(pThis->m_hwnd, message, wParam, lParam);}};class ZDerivedWindow1 : public ZWindow{public:BOOL OnPaint(WPARAM wParam, LPARAM lParam){PAINTSTRUCT ps;RECT rect;HDC hdc;hdc = BeginPaint(&ps);GetClientRect(&rect);::SetBkMode(hdc, TRANSPARENT);::Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);rect.top += 20;DrawText(hdc, _T("Derived 1"), -1, &rect, DT_SINGLELINE | DT_CENTER);EndPaint(&ps);return TRUE;}void OnLButtonUp(WPARAM wParam, LPARAM lParam){MessageBox(m_hwnd, _T("Press Derived 1"), _T("Info"), MB_OK | MB_ICONINFORMATION | MB_APPLMODAL);}};class ZDerivedWindow2 : public ZWindow{public:BOOL OnPaint(WPARAM wParam, LPARAM lParam){PAINTSTRUCT ps;RECT rect;HDC hdc;hdc = BeginPaint(&ps);GetClientRect(&rect);::SetBkMode(hdc, TRANSPARENT);::Rectangle(hdc, rect.left, rect.top, rect.right, rect.bottom);rect.top += 20;DrawText(hdc, _T("Derived 2"), -1, &rect, DT_SINGLELINE | DT_CENTER);EndPaint(&ps);return TRUE;}void OnLButtonUp(WPARAM wParam, LPARAM lParam){MessageBox(m_hwnd, _T("Press Derived 2"), _T("Info"), MB_OK | MB_ICONINFORMATION | MB_APPLMODAL);}};// simpleframework.cpp// 2012-11-17 by btwsmile#include <Windows.h>#include <tchar.h>#include "zwindow.h"LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow){TCHAR szAppName[] = _T("Thunk Model");WNDCLASS wndclass;wndclass.cbClsExtra = 0;wndclass.cbWndExtra = 0;wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);wndclass.hCursor = LoadCursor(hInstance, IDC_ARROW);wndclass.hIcon = LoadIcon(hInstance, IDI_APPLICATION);wndclass.hInstance = hInstance;wndclass.lpfnWndProc = ZWindow::StartWndProc;wndclass.lpszClassName = szAppName;wndclass.lpszMenuName = NULL;wndclass.style = CS_VREDRAW | CS_HREDRAW;if( !RegisterClass(&wndclass) ){MessageBox(NULL, _T("Fail to register wndclass"), _T("ERROR"), MB_OK);return 0;}ZWindow zw;g_pWnd = &zw;zw.Create(szAppName, szAppName, hInstance);zw.ShowWindow(iCmdShow);zw.UpdateWindow();ZDerivedWindow1 zdw1;g_pWnd = &zdw1;zdw1.Create(szAppName, NULL, hInstance, zw.m_hwnd, WS_VISIBLE | WS_CHILD, NULL, 50, 50, 225, 300);ZDerivedWindow2 zdw2;g_pWnd = &zdw2;zdw2.Create(szAppName, NULL, hInstance, zw.m_hwnd, WS_VISIBLE | WS_CHILD , NULL, 325, 50, 225, 300);// message loopMSG msg;while( GetMessage(&msg, NULL, 0, 0) ){TranslateMessage(&msg);DispatchMessage(&msg);}return msg.wParam;}

在VC 6.0中编译后运行正常,如图所示:

点击不同的区域,会弹出相应的message box,说明thunk成功了。

对例子代码的说明

请注意看WinMain函数中,wndclass.lpfnWndProc被赋值为了ZWindow::StartWndProc,所以zw、zdw1和zdw2窗口在收到第一条消息时,首先调用的是StartWndProc。然而StartWndProc做了什么呢?它对m_hwnd和m_thunk成员变量进行了赋值。然后将m_thunk强制转换成了WNDPROC类型的函数指针,并调用SetWindowLong函数将它设置成为窗口新的过程函数。对每个窗口来说,StartWndProc函数只被调用了一次,之后的消息都与它没有关系了。之后这个窗口的消息全部都会直接流向m_thunk代码,而m_thunk代码最终调用WndProc来完成消息的处理,不过它在调用之前将hwnd参数“纂改”成了窗口类实例的句柄。

可以说,StartWndProc只是昙花一现的过程函数,它“组装”m_thunk码并将用m_thunk来替换自己。我们甚至可以将StartWndProc看成是真实窗口过程函数的“工厂”,它在运行时创造出了“代码”,这也是为何Init函数的最后调用FlushInstructionCache的原因。

改进

如果你将上述代码拷贝到VS2010,然后编译运行会遇到问题:FlushInstructionCache函数抛出异常。这是因为VS2010的数据执行保护起了作用。可以对Thunk做下面的改进:

(1) 将ZWindow的成员变量THUNK m_thunk该为THUNK* m_pThunk,然后将Init函数修改为:

THUNK* m_pThunk;void Init(WNDPROC proc, void* pThis){m_pThunk = (THUNK*)VirtualAlloc(NULL, sizeof(THUNK), MEM_COMMIT, PAGE_EXECUTE_READWRITE);m_pThunk->m_mov = 0x042444c7;m_pThunk->m_this = PtrToLong(pThis);m_pThunk->m_jmp = 0xe9;m_pThunk->m_relproc = (int)proc - ((int)m_pThunk + sizeof(THUNK));FlushInstructionCache(GetCurrentProcess, m_pThunk, sizeof(THUNK));}

(2) StartWndProc函数需做相应的更改:

将WNDPROC WindowProcess = (WNDPROC)&(pThis->m_thunk);替换为WNDPROC WindowProcess = (WNDPROC)(pThis->m_pThunk);

最后的说明

如果你忍受不了例子中的全局变量g_pWnd,也有解决的方法。

(1) 删除ZWindow* g_wnd相关的代码。

(2) 修改ZWindow::Create方法,将CreateWindow函数的最后一个参数从NULL改为this。然后对StartWndProc函数做相应的更改:

// StartWndProcstatic LRESULT CALLBACK StartWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){ZWindow* pThis = (ZWindow*)((LPCREATESTRUCT)lParam)->lpCreateParams;pThis->m_hwnd = hwnd;pThis->Init(WndProc, pThis);WNDPROC WindowProcess = (WNDPROC)(pThis->m_pThunk);SetWindowLong(hwnd, GWL_WNDPROC, (LONG)WindowProcess);return WindowProcess(hwnd, message, wParam, lParam);}

文章写得比较粗糙,有些点可能未覆盖到,欢迎回帖交流!