尹德有 楊振龍
摘要:該文主要介紹了Thunk技術(shù)的基本原理,以及如何利用Thunk技術(shù)封裝基于C++語言的Windows窗口類。該文還介紹了如何利用ATL中已有的Thunk代碼實現(xiàn)窗口類的封裝以簡化代碼的編寫工作。利用本文介紹的技術(shù)封裝窗口類可極大的精簡代碼規(guī)模,同時代碼的運行效率也較高。
關(guān)鍵詞:Thunk;C++;Windows;窗口類;ATL;形實轉(zhuǎn)換
中圖分類號:TP311 文獻標(biāo)識碼:A 文章編號:1009-3044(2018)10-0248-03
一直以來,使用C++語言封裝Windows窗口類都是一個比較繁瑣的工作。大多數(shù)軟件開發(fā)人員都是使用第三方類庫進行Windows窗口類的編寫,比如最常使用的就是Microsoft公司的MFC類庫以及ATL模板庫等。這些類庫雖然功能強大,但是它們都過于龐大、復(fù)雜,如何使用簡單的技術(shù)實現(xiàn)輕量級的Windows窗口類(以下簡稱窗口類)是廣大軟件開發(fā)人員一直在探討的問題。
封裝窗口類的難點是如何讓W(xué)indows窗口處理回調(diào)機制在消息發(fā)生時調(diào)用窗口類的非靜態(tài)成員函數(shù)。
我們知道,C++在調(diào)用非靜態(tài)成員函數(shù)時需要傳遞this指針,由于Windows系統(tǒng)只能調(diào)用靜態(tài)的回調(diào)函數(shù),而類的靜態(tài)成員函數(shù)是沒有this指針的,因此,如何將this指針傳遞給類的靜態(tài)成員函數(shù)(窗口處理回調(diào)函數(shù))就成為封裝窗口類的關(guān)鍵技術(shù)。到目前為止,向類的靜態(tài)成員函數(shù)傳遞this指針的方法主要有三種:靜態(tài)表查詢方式,修改窗口用戶數(shù)據(jù)(USERDATA)方式,Thunk方式。其中Thunk方式是最直接、最高效的方式,本文主要討論Thunk方式。
1 基本原理
Thunk在程序設(shè)計領(lǐng)域被稱為形實轉(zhuǎn)換程序,其基本思想就是將若干連續(xù)存儲的數(shù)據(jù)直接解釋成代碼讓CPU執(zhí)行,本質(zhì)上相當(dāng)于直接使用機器語言編程。
利用Thunk技術(shù)封裝窗口類的基本思路是:將一段精心設(shè)計的數(shù)據(jù)(實際是一些機器指令,以下將這段數(shù)據(jù)簡稱為Thunk數(shù)據(jù))解釋成Windows窗口處理函數(shù),同時將this指針存儲在這些數(shù)據(jù)中,從而達到傳遞this指針的目的。
2 關(guān)鍵技術(shù)
雖然理論上可以在Thunk數(shù)據(jù)中使用機器代碼完成對類的非靜態(tài)成員函數(shù)的調(diào)用,但是這需要完全模擬C++對類的非靜態(tài)成員函數(shù)的調(diào)用機制,而這種機制是比較復(fù)雜的,并且可能會在將來有所改變。為了盡量縮短Thunk數(shù)據(jù)代碼的長度,一種更好的方式是在Thunk數(shù)據(jù)代碼中調(diào)用另一個靜態(tài)的窗口處理函數(shù)(以下簡稱中轉(zhuǎn)處理函數(shù)),同時采用一種技術(shù)將this指針傳遞給該中轉(zhuǎn)處理函數(shù)。
向中轉(zhuǎn)處理函數(shù)傳遞this指針的方式基本上有兩種:
1) 在標(biāo)準(zhǔn)Windows窗口處理函數(shù)的參數(shù)列表中增加一個參數(shù)并將this指針傳遞給這個參數(shù),即使用非標(biāo)準(zhǔn)形式的窗口處理函數(shù),但這種形式會增加Thunk數(shù)據(jù)代碼的復(fù)雜度;
2) 使用標(biāo)準(zhǔn)形式的窗口處理函數(shù)并將其參數(shù)列表中的某個參數(shù)修改為this指針值,這種方式將最大程度的降低Thunk數(shù)據(jù)代碼的復(fù)雜度,因此采用這種方式較好。
Windows窗口處理函數(shù)的標(biāo)準(zhǔn)形式如下:
代碼 1 WindowProc
其參數(shù)列表中一共有4個參數(shù),除hwnd參數(shù)以外的其他3個參數(shù)都有其特定用途,唯有hwnd,它代表目標(biāo)窗口句柄,而這個句柄完全可以預(yù)先保存在類(對象)中然后通過this指針訪問它,因此可以在Thunk數(shù)據(jù)代碼調(diào)用中轉(zhuǎn)處理函數(shù)之前將hwnd參數(shù)用this指針值替換掉,從而達到傳遞this指針的目的。
在中轉(zhuǎn)處理函數(shù)中,通過強制類型轉(zhuǎn)換,將hwnd參數(shù)硬性解釋成this指針,并通過該this指針調(diào)用類的非靜態(tài)窗口處理成員函數(shù),從而達到了讓W(xué)indows窗口處理回調(diào)機制調(diào)用窗口類非靜態(tài)成員函數(shù)的目的,至此就完成了窗口類的封裝。
3 實現(xiàn)細(xì)節(jié)
使用Thunk技術(shù)封裝窗口類的大體實現(xiàn)細(xì)節(jié)如下:
在窗口類中定義一個THUNK結(jié)構(gòu)類型的成員變量_thunk及初始化該結(jié)構(gòu)的成員函數(shù)InitThunk();
THUNK結(jié)構(gòu)實際是一段經(jīng)過仔細(xì)設(shè)計的機器代碼,當(dāng)將THUNK變量地址當(dāng)成函數(shù)地址來解釋并調(diào)用它時,將執(zhí)行這段代碼,在本例中就是要將其解釋成WNDPROC類型的指針,即Windows的窗口處理回調(diào)函數(shù),這樣在回調(diào)發(fā)生時THUNK代碼將被執(zhí)行。
在窗口類中提供一個attach(HWND h)函數(shù),其將修改目標(biāo)窗口h的窗口處理函數(shù)地址,使其指向this->_thunk,在執(zhí)行這條語句之前應(yīng)先調(diào)用InitThunk()初始化_thunk成員,使其中保存this指針值,也就是說,每個窗口類對象中都會有一個含有對象地址信息(即this)的成員變量(即_thunk)。
THUNK結(jié)構(gòu)中的其他代碼完成如下工作:將Windows調(diào)用THUNK數(shù)據(jù)代碼時傳遞過來的4個參數(shù)hwnd,uMsg,wParam,lParam中的hwnd替換成this指針值,然后調(diào)用中轉(zhuǎn)處理函數(shù)。由于Windows調(diào)用THUNK代碼時已經(jīng)將函數(shù)參數(shù)正確的入棧且中轉(zhuǎn)處理函數(shù)的調(diào)用方式及參數(shù)格式與標(biāo)準(zhǔn)Windows窗口處理函數(shù)相同,因此,調(diào)用中轉(zhuǎn)處理函數(shù)的代碼可簡單地使用一條跳轉(zhuǎn)指令直接跳轉(zhuǎn)到中轉(zhuǎn)處理函數(shù)的地址即可。需要注意的是,如果采用前面介紹的非標(biāo)準(zhǔn)形式的窗口處理函數(shù)來傳遞this指針,則需要增加修改堆棧指針、將this指針入棧等額外的操作,這無疑會增加THUNK數(shù)據(jù)代碼的復(fù)雜性。
在窗口類中定義一個靜態(tài)的窗口處理函數(shù)stdProc()充當(dāng)中轉(zhuǎn)處理函數(shù)。在stdProc()中,通過強制類型轉(zhuǎn)換,將hwnd參數(shù)強制轉(zhuǎn)換為this指針并通過該this指針調(diào)用窗口類的非靜態(tài)窗口處理成員函數(shù),這樣就實現(xiàn)了讓W(xué)indows窗口處理回調(diào)函數(shù)調(diào)用窗口類非靜態(tài)成員函數(shù)的過程。
通過上述方法封裝的窗口類,其每一個窗口對象都有不同的窗口處理函數(shù)地址,它指向this->_thunk成員變量。
使用用THUNK技術(shù)封裝窗口類的本質(zhì)是:用窗口類的數(shù)據(jù)成員而不是函數(shù)成員“冒充”窗口處理函數(shù),從而攔截到窗口消息。
4 借用ATL中的Thunk代碼
實現(xiàn)Thunk技術(shù)的關(guān)鍵是給出Thunk數(shù)據(jù)的對應(yīng)機器代碼。雖然可以通過查詢文檔等方式獲得相應(yīng)機器代碼,但還有更簡便的方式:利用ATL模板庫中的Thunk代碼。
ATL[3]是Microsoft公司繼MFC后推出的用于編寫COM組件的模板庫,它提供了很多模板類以方便COM組件的編寫,其中就包含對Windows窗口類的封裝。ATL封裝窗口類的方法中使用的就是Thunk技術(shù)。
ATL提供了4個與Thunk有關(guān)的類:_stdcallthunk,CDynamicStdCallThunk,CStdCallThunk,CWndProcThunk,其中前3個類位于
_stdcallthunk是ATL Thunk的基本實現(xiàn),它包含了全部Thunk數(shù)據(jù)代碼,其定義如下:
#pragma pack(push,1)
struct _stdcallthunk{
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
BOOL Init(DWORD_PTR proc, void* pThis){
m_mov = 0x042444C7; //C7 44 24 0C
m_this = PtrToUlong(pThis);
m_jmp = 0xe9;
m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));
// write block from data cache and
// flush from instruction cache
FlushInstructionCache(GetCurrentProcess(), this, sizeof(_stdcallthunk));
return TRUE;
}
//some thunks will dynamically allocate the memory for the code
void* GetCodeAddress(){ return this; }
void* operator new(size_t){ return __AllocStdCallThunk(); }
void operator delete(void* pThunk){ __FreeStdCallThunk(pThunk); }
};
#pragma pack(pop)
代碼 2 _stdcallthunk
_stdcallthunk中含有機器指令,因此是硬件相關(guān)的,上述是在x86平臺下的定義,ATL還提供了幾個在其他平臺下的_stdcallthunk定義,有興趣的讀者可以查閱相關(guān)代碼。限于篇幅,本文不對_stdcallthunk的機器代碼做過多解釋,因為那并不十分重要,在此只給出_stdcallthunk的使用方式:首先調(diào)用Init(DWORD_PTR proc, void* pThis)函數(shù)初始化該結(jié)構(gòu),其中傳給proc參數(shù)的值就是窗口類中定義的靜態(tài)中轉(zhuǎn)處理函數(shù)地址,傳給pThis參數(shù)的值就是窗口類對象的this指針值。初始化成功后,就可以調(diào)用GetCodeAddress()函數(shù)獲取窗口處理函數(shù)的地址(正如代碼中給出的那樣,它其實就是_stdcallthunk結(jié)構(gòu)的地址)并將目標(biāo)窗口的窗口處理函數(shù)地址修改為GetCodeAddress()的返回值。
CDynamicStdCallThunk是_stdcallthunk的動態(tài)分配版本,就像_stdcallthunk定義中的注釋說的那樣:某些平臺下,可執(zhí)行代碼所在的內(nèi)存必須使用動態(tài)分配方式獲取,即代碼必須位于堆中而不能位于棧中,x86平臺下的Windows系統(tǒng)中即是如此,因此,我們不能直接使用_stdcallthunk結(jié)構(gòu)。CDynamicStdCallThunk的定義如下:
#pragma pack(push,8)
class CDynamicStdCallThunk
{
public:
_stdcallthunk *pThunk;
CDynamicStdCallThunk(){ pThunk = NULL; }
~CDynamicStdCallThunk(){
if (pThunk){
delete pThunk;
}
}
BOOL Init(DWORD_PTR proc, void *pThis){
if (pThunk == NULL){
pThunk = new _stdcallthunk;
if (pThunk == NULL){
return FALSE;
}
}
return pThunk->Init(proc, pThis);
}
void* GetCodeAddress(){ return pThunk->GetCodeAddress(); }
};
#pragma pack(pop)
代碼 3 CDynamicStdCallThunk
CDynamicStdCallThunk的使用方式與stdcallthunk類似,在此不做贅述。
CStdCallThunk根據(jù)平臺的不同,可能是對CDynamicStdCallThunk的包裝,也可能是對_stdcallthunk的包裝:
typedef CDynamicStdCallThunk CStdCallThunk;
或
typedef _stdcallthunk CStdCallThunk;
代碼 4 CStdCallThunk
CWndProcThunk是ATL窗口類中實際使用的結(jié)構(gòu)(類),它除了對CStdCallThunk的成員函數(shù)簽名進行了類型上的限定外,還定義了一個_AtlCreateWndData數(shù)據(jù)成員,這個成員在本文編寫的窗口類中用不到,CWndProcThunk的定義如下:
class CWndProcThunk
{
public:
_AtlCreateWndData cd;
CStdCallThunk thunk;
BOOL Init(WNDPROC proc, void* pThis){ return thunk.Init((DWORD_PTR)proc, pThis); }
WNDPROC GetWNDPROC(){ return (WNDPROC)thunk.GetCodeAddress(); }
};
代碼 5 CWndProcThunk
可以借用ATL提供的4個Thunk結(jié)構(gòu)中的任意一個來編寫我們的窗口類,但很明顯,使用CWndProcThunk無論在兼容性方面還是在可維護性方面都是最好的,因此,本文使用CWndProcThunk結(jié)構(gòu)封裝窗口類。
這里需要說明一下,ATL已經(jīng)提供了一個封裝好的窗口類CWindowImpl,之所以借用ATL的Thunk結(jié)構(gòu)來封裝一個新的窗口類而不是直接使用ATL的窗口類是因為:ATL的窗口類額外引入了一些我們不需要的成員,并且其窗口類的行為未必完全滿足某些特定需要,又或者對于某些要求精簡化的程序來說,CWindowImpl過于復(fù)雜了。
5 結(jié)論
采用Thunk技術(shù)封裝的窗口類無論在代碼規(guī)模還是執(zhí)行效率上都有很大優(yōu)勢,借用ATL的Thunk結(jié)構(gòu)又可以進一步簡化代碼編寫工作,同時代碼的兼容性和可維護性也得到了很大的提高。
參考文獻:
[1] Stanley B.Lippman,Josee Lajoie. C++ Primer[M].潘愛民,張麗,譯.北京:中國電力出版社,2004.
[2] Charles Petzold. Windows程序設(shè)計[M].方敏,張勝,梁路平,譯.北京:清華大學(xué)出版社,2010.
[3] BRENT RECTOR,CHRIS SELLS.深入解析ATL[M].潘愛民,新語,譯.北京:中國電力出版社,2001.