thunk技術(shù)完成窗口類的封裝
發(fā)表時(shí)間:2024-06-10 來源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]MFC功能已經(jīng)非常強(qiáng)大,自己做界面庫也許沒什么意思,但是這個(gè)過程中卻能學(xué)到很多東西。比如說:窗口類的封裝,從全局窗口消息處理到窗口對(duì)象消息處理的映射方法: 對(duì)界面進(jìn)行封裝,一般都是一個(gè)窗口一個(gè)類,比如實(shí)現(xiàn)一個(gè)最基本的窗口類CMyWnd,你一定會(huì)把窗口過程作為這個(gè)類的成員函數(shù),但是使用WINA...
MFC功能已經(jīng)非常強(qiáng)大,自己做界面庫也許沒什么意思,但是這個(gè)過程中卻能學(xué)到很多東西。比如說:
窗口類的封裝,從全局窗口消息處理到窗口對(duì)象消息處理的映射方法:
對(duì)界面進(jìn)行封裝,一般都是一個(gè)窗口一個(gè)類,比如實(shí)現(xiàn)一個(gè)最基本的窗口類CMyWnd,你一定會(huì)把窗口過程作為這個(gè)類的成員函數(shù),但是使用WINAPI創(chuàng)建窗口時(shí)必須注冊(cè)類WNDCLASS,里面有個(gè)成員數(shù)據(jù)lpfnWndProc需要WNDPROC的函數(shù)指針,一般想法就是把窗口類的消息處理函數(shù)指針傳過去,但是類成員函數(shù)除非是靜態(tài)的,否則無法轉(zhuǎn)換到WNDPROC,而全局的消息處理函數(shù)又無法得到窗口類對(duì)象的指針。這里有幾種解決辦法:
一種解決方法是用窗口列表,開一個(gè)結(jié)構(gòu)數(shù)組,窗口類對(duì)象創(chuàng)建窗口的時(shí)候把窗口HWND和this指針放入數(shù)組,全局消息處理函數(shù)遍歷數(shù)組,利用HWND找出this指針,然后定位到對(duì)象內(nèi)部的消息處理函數(shù)。這種方法查找對(duì)象的時(shí)間會(huì)隨著窗口個(gè)數(shù)的增多而增長(zhǎng)。
另一種方法比較聰明一點(diǎn),WNDCLASS里面有個(gè)成員數(shù)據(jù)cbWndExtra一般是不用的,利用這點(diǎn),注冊(cè)類時(shí)給該成員數(shù)據(jù)賦值,這樣窗口創(chuàng)建時(shí)系統(tǒng)會(huì)根據(jù)該值開辟一塊內(nèi)存與窗口綁定,這時(shí)把創(chuàng)建的窗口類的指針放到該塊內(nèi)存,那么在靜態(tài)的窗口消息循環(huán)函數(shù)就能利用GetWindowLong(hWnd,GWL_USERDATA)取出該指針,return (CMyWnd*)->WindowProc(...),這樣就不用遍歷窗口了。但是這樣一來就有個(gè)致命弱點(diǎn),對(duì)窗口不能調(diào)用SetWindowLong(hWnd,GWL_USERDATA,數(shù)據(jù)),否則就會(huì)導(dǎo)致程序崩潰。幸好這個(gè)函數(shù)(特定這幾個(gè)參數(shù))是調(diào)用幾率極低的,對(duì)于窗口,由于創(chuàng)建窗口都是調(diào)用窗口類的Create函數(shù),不用手工注冊(cè)WNDCLASS類,也就不會(huì)調(diào)用SetWindowLong函數(shù)。但是畢竟缺乏安全性,而且當(dāng)一秒鐘內(nèi)處理的窗口消息很多時(shí),這種查找速度也可能不夠快。
還有一種就是比較完美的解決辦法,稱之為thunk技術(shù)。thunk是一組動(dòng)態(tài)生成的ASM指令,它記錄了窗口類對(duì)象的this指針,并且這組指令可以當(dāng)作函數(shù),既也可以是窗口過程來使用。thunk先把窗口對(duì)象this指針記錄下來,然后轉(zhuǎn)向到靜態(tài)stdProc回調(diào)函數(shù),轉(zhuǎn)向之前先記錄HWND,然后把堆棧里HWND的內(nèi)容替換為this指針,這樣在stdProc里就可以從HWND取回對(duì)象指針,定位到WindowProc了。
我們先來看看窗口過程函數(shù)定義:
LRESULT WINAPI WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
其實(shí)當(dāng)我們的窗口類CMyWnd創(chuàng)建窗口的時(shí)候,窗口句柄是可以得到并且作為成員數(shù)據(jù)保存,如此一來,第一個(gè)參數(shù)hWnd是可以不要的,因?yàn)榭梢酝ㄟ^this->m_hWnd得到,我們可以在這里做手腳,hWnd其實(shí)質(zhì)是一個(gè)指針,如果把這個(gè)參數(shù)替換為窗口類對(duì)象的this指針,那么我們不就可以通過(CMyWnd*)hWnd->WindowProc轉(zhuǎn)到窗口類內(nèi)部的窗口過程了嗎?但是窗口過程是系統(tǒng)調(diào)用的,怎么能把hWnd替換掉呢?我們先來看看系統(tǒng)調(diào)用這個(gè)函數(shù)時(shí)的堆棧情況:
系統(tǒng)調(diào)用m_thunk時(shí)的堆棧:
ret HWND MSG WPARAM LPARAM
-------------------------------------------
棧頂 棧底
系統(tǒng)把參數(shù)從右到左依次壓棧,最后把返回地址壓棧,我們只要在系統(tǒng)調(diào)用窗口過程時(shí)修改堆棧,把其中的hWnd參數(shù)替換掉就行了。這時(shí)thunk技術(shù)就有用武之地了,我們先定義一個(gè)結(jié)構(gòu):
#pragma pack(push,1) //該結(jié)構(gòu)必須以字節(jié)對(duì)齊
struct Thunk {
BYTE Call;
int Offset;
WNDPROC Proc;
BYTE Code[5];
CMyWnd* Window;
BYTE Jmp;
BYTE ECX;
};
#pragma pack(pop)
類定義:
class CMyWnd
{
public:
BOOL Create(...);
LRESULT WINAPI WindowProc(UINT,WPARAM,LPARAM);
static LRESULT WINAPI InitProc(HWND,UINT,WPARAM,LPARAM);
static LRESULT WINAPI stdProc(HWND,UINT,WPARAM,LPARAM);
WNDPROC CreateThunk();
WNDPROC GetThunk(){return m_thunk}
...
private:
WNDPROC m_thunk
}
在創(chuàng)建窗口的時(shí)候把窗口過程設(shè)定為this->m_thunk,m_thunk的類型是WNDPROC,因此是完全合法的,當(dāng)然這個(gè)m_thunk還沒有初始化,在創(chuàng)建窗口前必須初始化:
WNDPROC CMyWnd::CreateThunk()
{
Thunk* thunk = new Thunk;
///////////////////////////////////////////////
//
//系統(tǒng)調(diào)用m_thunk時(shí)的堆棧:
//ret HWND MSG WPARAM LPARAM
//-------------------------------------------
//棧頂 棧底
///////////////////////////////////////////////
//call Offset
//調(diào)用code[0],call執(zhí)行時(shí)會(huì)把下一條指令壓棧,即把Proc壓棧
thunk->Call = 0xE8; // call [rel]32
thunk->Offset = (size_t)&(((Thunk*)0)->Code)-(size_t)&(((Thunk*)0)->Proc); // 偏移量,跳過Proc到Code[0]
thunk->Proc = CMyWnd::stdProc; //靜態(tài)窗口過程
//pop ecx,Proc已壓棧,彈出Proc到ecx
thunk->Code[0] = 0x59; //pop ecx
//mov dword ptr [esp+0x4],this
//Proc已彈出,棧頂是返回地址,緊接著就是HWND了。
//[esp+0x4]就是HWND
thunk->Code[1] = 0xC7; // mov
thunk->Code[2] = 0x44; // dword ptr
thunk->Code[3] = 0x24; // disp8[esp]
thunk->Code[4] = 0x04; // +4
thunk->Window = this;
//偷梁換柱成功!跳轉(zhuǎn)到Proc
//jmp [ecx]
thunk->Jmp = 0xFF; // jmp [r/m]32
thunk->ECX = 0x21; // [ecx]
m_thunk = (WNDPROC)thunk;
return m_thunk;
}
這樣m_thunk雖然是一個(gè)結(jié)構(gòu),但其數(shù)據(jù)是一段可執(zhí)行的代碼,而其類型又是WNDPROC,系統(tǒng)就會(huì)忠實(shí)地按窗口過程規(guī)則調(diào)用這段代碼,m_thunk就把Window字段里記錄的this指針替換掉堆棧中的hWnd參數(shù),然后跳轉(zhuǎn)到靜態(tài)的stdProc:
//本回調(diào)函數(shù)的HWND調(diào)用之前已由m_thunk替換為對(duì)象指針
LRESULT WINAPI CMyWnd::stdProc(HWND hWnd,UINT uMsg,UINT wParam,LONG lParam)
{
CMyWnd* w = (CMyWnd*)hWnd;
return w->WindowProc(uMsg,wParam,lParam);
}
這樣就把窗口過程轉(zhuǎn)向到了類成員函數(shù)WindowProc,當(dāng)然這樣還有一個(gè)問題,就是窗口句柄hWnd還沒來得及記錄,因此一開始的窗口過程應(yīng)該先定位到靜態(tài)的InitProc,CreateWindow的時(shí)候給最后一個(gè)參數(shù),即初始化參數(shù)賦值為this指針:
CreateWindowEx(
dwExStyle,
szClass,
szTitle,
dwStyle,
x,
y,
width,
height,
hParentWnd,
hMenu,
hInst,
this //初始化參數(shù)
);,
在InitProc里面取出該指針:
LRESULT WINAPI CMyWnd::InitProc(HWND hWnd,UINT uMsg,UINT wParam,LONG lParam)
{
if(uMsg == WM_NCCREATE)
{
CMyWnd *w = NULL;
w = (CMyWnd*)((LPCREATESTRUCT)lParam)->lpCreateParams;
if(w)
{
//記錄hWnd
w->m_hWnd = hWnd;
//改變窗口過程為m_thunk
SetWindowLong(hWnd,GWL_WNDPROC,(LONG)w-CreateThunk());
return (*(WNDPROC)(w->GetThunk()))(hWnd,uMsg,wParam,lParam);
}
}
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
這樣就大功告成。
窗口過程轉(zhuǎn)發(fā)流程:
假設(shè)已建立CMyWnd類的窗口對(duì)象 CMyWnd *window,初始化完畢后調(diào)用window->Create,這時(shí)Create的窗口其窗口過程函數(shù)是靜態(tài)CMyWnd::InitWndProc
InitWndProc 實(shí)現(xiàn)功能:window->Create創(chuàng)建窗口時(shí)已把對(duì)象this指針放入窗口初始化參數(shù)中,在此過程的WM_NCCREATE消息中把this指針取出來:CMyWnd *w = (CMyWnd*)((LPCREATESTRUCT)lParam)->lpCreateParams;記錄HWND:w->m_hWnd = hWnd,然后設(shè)置窗口過程為w->m_thunk(thunk是一個(gè)WNDPROC類型的成員數(shù)據(jù),所以可以設(shè)置)
└→ window->m_thunk 實(shí)現(xiàn)功能:跳轉(zhuǎn)到靜態(tài)CMyWnd::stdProc,在此之前替換系統(tǒng)的調(diào)用參數(shù)HWND為this指針
└→ stdProc 實(shí)現(xiàn)功能:把HWND轉(zhuǎn)換為窗口類指針:
CMyWnd *w = (CMyWnd*)hWnd;
return w->WindowProc(uMsg,wParam,lParam)
└→ window->WindowProc 實(shí)現(xiàn)功能:執(zhí)行實(shí)際的消息處理,窗口句柄已保存在m_hWnd