天天看點

WTL學習之旅(三)WTL中 Thunk技術本質(含代碼)

轉載請标明是引用于 http://blog.csdn.net/chenyujing1234

 例子代碼:(編譯工具:VS2005)

 http://www.rayfile.com/zh-cn/files/62839154-8251-11e1-aaa6-0015c55db73d/

一、前言

通知我們要在視窗的回調函數裡獲得包含此視窗的類的指針得把視窗類指針放到視窗綁定的資料裡,可是有一種技術可以幫助你省去這種綁定的麻煩。

這就是Thunk技術。

二、正文

我們知道一個視窗對映一個對象,很自然設計一個類.

如下圖MyWin *p1和MyWin *p2,在類裡有視窗的句柄,這樣就進入到window的程式世界裡了.

通過類可以找到視窗句柄,但通過視窗句柄怎麼找到類.這就是一個比較難辦的問題。

WTL的解決方法叫Thunk技術。

WTL學習之旅(三)WTL中 Thunk技術本質(含代碼)

Thunk的定義:

它是一小段代碼,它在運作時臨時生成的。為了在調用"actual"函數時能被執行,

有時候非常重要。一個普通的功能是轉化一個C++的功能對象到一個正常的函數指針。

通常要求插入一個增加的指針,這個指針能表現指向此功能對象的"this"指針.

下面我們來講解Thunk技術。先從彙編的知識開始。

WTL學習之旅(三)WTL中 Thunk技術本質(含代碼)

通過下圖我們可以看到WinProc的參數的壓棧方法,這是典型的__stdcall調用方式(右邊先壓棧),當ESP指向Return Addr時,

通過位址+4可以找到hWnd,通過+8可以找到msg.

WTL學習之旅(三)WTL中 Thunk技術本質(含代碼)

===================================

補充:有些人認為不是ESP+4找到hWnd,而是+8。會有這樣的問題,把ESP看成在哪裡的問題。

在Thunk技術裡此時的ESP指向Return Addr。而認為要+8的可能此時的ESP指向了push ESP.

下圖是子程式調用時的記憶體情況。

WTL學習之旅(三)WTL中 Thunk技術本質(含代碼)

===================================

那麼是可以通過找到hWnd而找到對象指針呢?

大家可以通過thunk3.cpp檔案看到.大約分以下幾個步驟:

第一步:視窗調回調函數StartWindowProc

Thunk的Init初始化做了兩件事情:1、mov指令替換hWnd為對象指針,2、jump指令跳轉到WindowProc;

然後通過m_Thunk.GetCodeAddress獲得Thunk指針.

// 隻執行一次
	static LRESULT CALLBACK StartWindowProc(HWND hWnd, UINT uMsg, 
										WPARAM wParam, LPARAM lParam)
	{
		CWindow* pThis;
		WNDPROC pProc;
		pThis = (CWindow*)g_pObject;
		pThis->Attach(hWnd);
		// 初始化Thunk,做了兩件事:1、mov指令替換hWnd為對象指針,2、jump指令跳轉到WindowProc
		pThis->m_Thunk.Init((DWORD)WindowProc, pThis);

		// 得到Thunk指針
		pProc = (WNDPROC)pThis->m_Thunk.GetCodeAddress();
		// 調用下面的語句後,以後消息來了,都由pProc處理
		::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pProc);

		return pProc(hWnd, uMsg, wParam, lParam);
	}

           
// 按一位元組對齊
#pragma pack(push, 1)
struct tagThunk
{
	DWORD m_mov;  // 4個位元組
	DWORD m_this;
	BYTE  m_jmp;
	DWORD m_relproc;
	//關鍵代碼  此段代碼在x86或其他平台是不一樣的,若想做其它平台的Thunk,可以參考C:\Program Files\Microsoft Visual Studio 8\VC\atlmfc\include\atlwin.h檔案 //
	BOOL Init(DWORD proc, void* pThis)
	{
		m_mov = 0x042444C7; 
		m_this = (DWORD)pThis;  // mov [esp+4], pThis;而esp+4本來是放hWnd,現在被偷着放對象指針了.
		m_jmp = 0xe9;
		// 跳轉到proc指定的入口函數 
		m_relproc = (DWORD)((INT_PTR)proc - ((INT_PTR)this + sizeof(tagThunk)));
		// 告訴CPU把以上四條語句不當資料,當指令,接下來用GetCodeAddress獲得的指針就會運作此指令
		FlushInstructionCache(GetCurrentProcess(), this, sizeof(tagThunk));
		return TRUE;
	}
	void* GetCodeAddress()
	{
		return this; // 指向this,那麼由GetCodeAddress獲得的函數pProc是從DWORD m_mov;開始執行的
	}
};
#pragma pack(pop)
           

上面講到會跳到WindowProc裡去,那麼我們在來一下WindowProc函數.

static LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, 
										WPARAM wParam, LPARAM lParam)
	{
	    CWindow* pThis;
		pThis = (CWindow*)hWnd; // 強轉為對象指針

		return pThis->ProcessWindowMessage(message, wParam, lParam);
	}
           

這樣就實作了多個視窗調用時能調用各自的視窗處理函數了,先用下圖來給大家說明這點,再貼出代碼,.

WTL學習之旅(三)WTL中 Thunk技術本質(含代碼)
int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
	HWND hWnd = NULL;
	MSG msg;
	CWindow* pwnd = NULL;

	g_hInstance = hInstance; // Store instance handle in our global variable

	pwnd = new CWindow();
	pwnd->RegisterClassEx(hInstance);

	pwnd->Create(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
				CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
	pwnd->ShowWindow(nCmdShow);
	pwnd->UpdateWindow();

	// Thunk技術的好處是可以建立多個視窗。
	/*
	CWindow wnd;
		wnd.Create(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
					CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
		wnd.ShowWindow(nCmdShow);
		wnd.UpdateWindow();
	*/
	
	// Main message loop:
	while (GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	delete pwnd;
	return (int) msg.wParam;
}
           

=============================================================================================================================

在了解了Thunk技術的原理後,我們來看WTL8中Thunk技術的實作。

先了解WTL中各類的承關系,其中CWindowImpl是我們常繼承開發的類.

WTL學習之旅(三)WTL中 Thunk技術本質(含代碼)

檢視檔案C:\Program Files\Microsoft Visual Studio 8\VC\atlmfc\include\atlwin.h

template <class T, class TBase /* = CWindow */, class TWinTraits /* = CControlWinTraits */>
class ATL_NO_VTABLE CWindowImpl : public CWindowImplBaseT< TBase, TWinTraits >
{
public:
	DECLARE_WND_CLASS(NULL)
           
template <class TBase = CWindow, class TWinTraits = CControlWinTraits>
class ATL_NO_VTABLE CWindowImplBaseT : public CWindowImplRoot< TBase >
{
public:
	WNDPROC m_pfnSuperWindowProc;

	CWindowImplBaseT() : m_pfnSuperWindowProc(::DefWindowProc)
	{}

	static DWORD GetWndStyle(DWORD dwStyle)
	{
		return TWinTraits::GetWndStyle(dwStyle);
	}
	static DWORD GetWndExStyle(DWORD dwExStyle)
	{
		return TWinTraits::GetWndExStyle(dwExStyle);
	}

	virtual WNDPROC GetWindowProc()
	{
		return WindowProc;
	}
	static LRESULT CALLBACK StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
	static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
	HWND Create(HWND hWndParent, _U_RECT rect, LPCTSTR szWindowName,
			DWORD dwStyle, DWORD dwExStyle, _U_MENUorID MenuOrID, ATOM atom, LPVOID lpCreateParam = NULL);
	BOOL DestroyWindow()
	{
		ATLASSERT(::IsWindow(m_hWnd));
		return ::DestroyWindow(m_hWnd);
	}
           
template <class TBase, class TWinTraits>
LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{//     下面為多線程考慮,而用到了連結清單.連結清單資料的插入可參考Create(
	CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_AtlWinModule.ExtractCreateWndData();
	ATLASSERT(pThis != NULL);
	if(!pThis)
	{
		return 0;
	}
	pThis->m_hWnd = hWnd;

	// Initialize the thunk.  This is allocated in CWindowImplBaseT::Create,
	// so failure is unexpected here.
         // pThis->GetWindowProc()其實就是WindowProc
	pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
	WNDPROC pProc = pThis->m_thunk.GetWNDPROC();
	WNDPROC pOldProc = (WNDPROC)::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pProc);
#ifdef _DEBUG
	// check if somebody has subclassed us already since we discard it
	if(pOldProc != StartWindowProc)
		ATLTRACE(atlTraceWindowing, 0, _T("Subclassing through a hook discarded.\n"));
#else
	(pOldProc);	// avoid unused warning
#endif
	return pProc(hWnd, uMsg, wParam, lParam);
}


           
template <class TBase, class TWinTraits>
LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)hWnd;
	// set a ptr to this message and save the old value
	_ATL_MSG msg(pThis->m_hWnd, uMsg, wParam, lParam);
           

 是不是很熟悉。

=============================================================================================================================

其實Thunk技術的應用決非隻用于視窗管理。下面通過一個CSDN博友的部落格開給大家一點思路.

http://blog.csdn.net/tttyd/article/details/4562233

//塗遠東 2009 09 17 深圳
//聲明函數類型。
typedef void (*TESTFUN)(void*);
//定義修改代碼的結構。
#pragma pack(push,1)
struct Thunk
{
	DWORD   m_mov;          // 修改參數指令
	DWORD   m_this;         //修改後的參數
	BYTE    m_jmp;		// jmp TESTFUN,跳轉指令。
	DWORD   m_relproc;	// relative jmp,相對跳轉的位置。
	//初始化跳轉代碼。
	void Init(TESTFUN pFun, void* pThis)
	{
		//設定參數指令
		m_mov = 0x042444C7;  //C7 44 24 0C
		//設定修改後的參數
		m_this = PtrToUlong(pThis);

		//設定跳轉指針。
		m_jmp = 0xe9;
		
		//設定跳轉的相對位址。
		m_relproc = (int)pFun - ((int)this+sizeof(Thunk));
		//把CPU裡的緩沖資料寫到主記憶體。
		FlushInstructionCache(GetCurrentProcess(),
			this, sizeof(Thunk));
	}
};
#pragma pack(pop)
//測試動态修改記憶體裡的指令資料。
class CTest
{
public:
	//儲存動态修改代碼的記憶體。
	Thunk m_Thunk;
	//真實運作的函數。
	static void TestFun(void* p)
	{
		CTest* pTest = (CTest*)p;
		pTest->Print();
	}
	void Print()
	{
		printf("這裡僅僅是一個測試/n TestFun函數的參數被修改了/n");
	}
};

int main(int argc, char* argv[])
{
	//如下調用這個類:
	//測試運作。
	CTest Test;
	Test.m_Thunk.Init(Test.TestFun,&Test);
	TESTFUN pTestFun = (TESTFUN)&(Test.m_Thunk);
	char* psz = "test";
	pTestFun((void*)psz); 
	return 0;
}
           

繼續閱讀