打造自己的MFC:thunk技術實現窗口類的封裝

MFC功能已經非常強大,自己做界面庫也許沒什麼意思,但是這個過程中卻能學到很多東西。比如說:

窗口類的封裝,從全局窗口消息處理到窗口對象消息處理的映射方法:

    對界面進行封裝,一般都是一個窗口一個類,比如實現一個最基本的窗口類CMyWnd,你一定會把窗口過程作爲這個類的成員函數,但是使用WINAPI創建窗口時必須註冊類WNDCLASS,裏面有個成員數據lpfnWndProc需要WNDPROC的函數指針,一般想法就是把窗口類的消息處理函數指針傳過去,但是類成員函數除非是靜態的,否則無法轉換到WNDPROC,而全局的消息處理函數又無法得到窗口類對象的指針。這裏有幾種解決辦法:

    一種解決方法是用窗口列表,開一個結構數組,窗口類對象創建窗口的時候把窗口HWND和this指針放入數組,全局消息處理函數遍歷數組,利用HWND找出this指針,然後定位到對象內部的消息處理函數。這種方法查找對象的時間會隨着窗口個數的增多而增長。

    另一種方法比較聰明一點,WNDCLASS裏面有個成員數據cbWndExtra一般是不用的,利用這點,註冊類時給該成員數據賦值,這樣窗口創建時系統會根據該值開闢一塊內存與窗口綁定,這時把創建的窗口類的指針放到該塊內存,那麼在靜態的窗口消息循環函數就能利用GetWindowLong(hWnd,GWL_USERDATA)取出該指針,return (CMyWnd*)->WindowProc(...),這樣就不用遍歷窗口了。但是這樣一來就有個致命弱點,對窗口不能調用SetWindowLong(hWnd,GWL_USERDATA,數據),否則就會導致程序崩潰。幸好這個函數(特定這幾個參數)是調用機率極低的,對於窗口,由於創建窗口都是調用窗口類的Create函數,不用手工註冊WNDCLASS類,也就不會調用SetWindowLong函數。但是畢竟缺乏安全性,而且當一秒鐘內處理的窗口消息很多時,這種查找速度也可能不夠快。

    還有一種就是比較完美的解決辦法,稱之爲thunk技術。thunk是一組動態生成的ASM指令,它記錄了窗口類對象的this指針,並且這組指令可以當作函數,既也可以是窗口過程來使用。thunk先把窗口對象this指針記錄下來,然後轉向到靜態stdProc回調函數,轉向之前先記錄HWND,然後把堆棧裏HWND的內容替換爲this指針,這樣在stdProc裏就可以從HWND取回對象指針,定位到WindowProc了。

    我們先來看看窗口過程函數定義:
   
    LRESULT WINAPI WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
    
    其實當我們的窗口類CMyWnd創建窗口的時候,窗口句柄是可以得到並且作爲成員數據保存,如此一來,第一個參數hWnd是可以不要的,因爲可以通過this->m_hWnd得到,我們可以在這裏做手腳,hWnd其實質是一個指針,如果把這個參數替換爲窗口類對象的this指針,那麼我們不就可以通過(CMyWnd*)hWnd->WindowProc轉到窗口類內部的窗口過程了嗎?但是窗口過程是系統調用的,怎麼能把hWnd替換掉呢?我們先來看看系統調用這個函數時的堆棧情況:

系統調用m_thunk時的堆棧:
ret HWND MSG WPARAM LPARAM
-------------------------------------------
棧頂                                              棧底

系統把參數從右到左依次壓棧,最後把返回地址壓棧,我們只要在系統調用窗口過程時修改堆棧,把其中的hWnd參數替換掉就行了。這時thunk技術就有用武之地了,我們先定義一個結構:

#pragma pack(push,1) //該結構必須以字節對齊
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
}

在創建窗口的時候把窗口過程設定爲this->m_thunk,m_thunk的類型是WNDPROC,因此是完全合法的,當然這個m_thunk還沒有初始化,在創建窗口前必須初始化:

WNDPROC CMyWnd::CreateThunk()
{
   Thunk*  thunk = new Thunk;
 
 ///////////////////////////////////////////////
 //
 //系統調用m_thunk時的堆棧:
 //ret HWND MSG WPARAM LPARAM
 //-------------------------------------------
 //棧頂                                             棧底
 ///////////////////////////////////////////////

 //call Offset
 //調用code[0],call執行時會把下一條指令壓棧,即把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;  //靜態窗口過程
 
 //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;
 
 //偷樑換柱成功!跳轉到Proc
 //jmp [ecx]
 thunk->Jmp = 0xFF;     // jmp [r/m]32
 thunk->ECX = 0x21;     // [ecx]
 
 m_thunk = (WNDPROC)thunk;
 return m_thunk;
}

這樣m_thunk雖然是一個結構,但其數據是一段可執行的代碼,而其類型又是WNDPROC,系統就會忠實地按窗口過程規則調用這段代碼,m_thunk就把Window字段裏記錄的this指針替換掉堆棧中的hWnd參數,然後跳轉到靜態的stdProc:

//本回調函數的HWND調用之前已由m_thunk替換爲對象指針
LRESULT WINAPI CMyWnd::stdProc(HWND hWnd,UINT uMsg,UINT wParam,LONG lParam)
{
 CMyWnd* w = (CMyWnd*)hWnd;
 
  return w->WindowProc(uMsg,wParam,lParam);
}

這樣就把窗口過程轉向到了類成員函數WindowProc,當然這樣還有一個問題,就是窗口句柄hWnd還沒來得及記錄,因此一開始的窗口過程應該先定位到靜態的InitProc,CreateWindow的時候給最後一個參數,即初始化參數賦值爲this指針:

CreateWindowEx(
        dwExStyle,
                  szClass,
                  szTitle,
                  dwStyle,
                  x,
                  y,
                  width,
                  height,
                  hParentWnd,
                  hMenu,
                  hInst,
                  this            //初始化參數
                  );,

在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);
}

這樣就大功告成。

窗口過程轉發流程:

    假設已建立CMyWnd類的窗口對象 CMyWnd *window,初始化完畢後調用window->Create,這時Create的窗口其窗口過程函數是靜態CMyWnd::InitWndProc

 

InitWndProc 實現功能:window->Create創建窗口時已把對象this指針放入窗口初始化參數中,在此過程的WM_NCCREATE消息中把this指針取出來:CMyWnd *w = (CMyWnd*)((LPCREATESTRUCT)lParam)->lpCreateParams;記錄HWND:w->m_hWnd = hWnd,然後設置窗口過程爲w->m_thunk(thunk是一個WNDPROC類型的成員數據,所以可以設置)
└→ window->m_thunk 實現功能:跳轉到靜態CMyWnd::stdProc,在此之前替換系統的調用參數HWND爲this指針
└→ stdProc 實現功能:把HWND轉換爲窗口類指針:
CMyWnd *w = (CMyWnd*)hWnd;
return w->WindowProc(uMsg,wParam,lParam)
└→ window->WindowProc 實現功能:執行實際的消息處理,窗口句柄已保存在m_hWnd

 題外話:thunk的彙編代碼全部寫在註釋裏了,把這段彙編轉成數據可費了不少勁,當時手頭沒有合適的工具,只有一本《8086/8088彙編語言程序設計》,根據附錄中的指令碼彙總錶轉成機器碼數據,那裏面根本沒有EAX,ECX,ESP等的概念,只能連蒙帶猜加調試,非法操作了n(n>10)回纔得到那些數據,當時真是長出了一口氣:TNND,終於搞定了!:-)

發佈了26 篇原創文章 · 獲贊 5 · 訪問量 30萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章