程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> VC++ >> MFC:thunk技術實現窗口類的封裝

MFC:thunk技術實現窗口類的封裝

編輯:VC++

  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

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved