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

最近看了一篇讲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

// global
class 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);
	}
	// StartWndProc
	static 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);
	}
	// WndProc
	static 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 loop
	MSG 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函数做相应的更改:

	// StartWndProc
	static 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);
	}

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

你可能感兴趣的:(探秘窗口过程函数(WndProc)的Thunk技术)