⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 thunk.htm

📁 打造自己的MFC:thunk技术实现窗口类的封装
💻 HTM
字号:
<html>

<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<meta name="GENERATOR" content="Microsoft FrontPage 4.0">
<meta name="ProgId" content="FrontPage.Editor.Document">
<title>打造自己的MFC</title>
</head>

<body>

<div class="postTitle">
  <a id="_db51046a3aa02ef8_HomePageDays_DaysList__ctl1_DayItem_DayList__ctl0_TitleUrl" href="http://blog.csdn.net/ringphone/archive/2004/09/28/118883.aspx">打造自己的MFC:thunk技术实现窗口类的封装</a>
</div>
<div class="postText">
  <p>MFC功能已经非常强大,自己做界面库也许没什么意思,但是这个过程中却能学到很多东西。比如说:</p>
  <p><strong>窗口类的封装,从全局窗口消息处理到窗口对象消息处理的映射方法:</strong></p>
  <p><strong>&nbsp;&nbsp;&nbsp; </strong>对界面进行封装,一般都是一个窗口一个类,比如实现一个最基本的窗口类CMyWnd,你一定会把窗口过程作为这个类的成员函数,但是使用WINAPI创建窗口时必须注册类WNDCLASS,里面有个成员数据lpfnWndProc需要WNDPROC的函数指针,一般想法就是把窗口类的消息处理函数指针传过去,但是类成员函数除非是静态的,否则无法转换到WNDPROC,而全局的消息处理函数又无法得到窗口类对象的指针。这里有几种解决办法:<br>
  <br>
  &nbsp;&nbsp;&nbsp; 
  一种解决方法是用窗口列表,开一个结构数组,窗口类对象创建窗口的时候把窗口HWND和this指针放入数组,全局消息处理函数遍历数组,利用HWND找出this指针,然后定位到对象内部的消息处理函数。这种方法查找对象的时间会随着窗口个数的增多而增长。<br>
  <br>
  &nbsp;&nbsp;&nbsp; 另一种方法比较聪明一点,WNDCLASS里面有个成员数据cbWndExtra一般是不用的,利用这点,注册类时给该成员数据赋值,这样窗口创建时系统会根据该值开辟一块内存与窗口绑定,这时把创建的窗口类的指针放到该块内存,那么在静态的窗口消息循环函数就能利用GetWindowLong(hWnd,GWL_USERDATA)取出该指针,return 
  (CMyWnd*)-&gt;WindowProc(...),这样就不用遍历窗口了。但是这样一来就有个致命弱点,对窗口不能调用SetWindowLong(hWnd,GWL_USERDATA,数据),否则就会导致程序崩溃。幸好这个函数(特定这几个参数)是调用几率极低的,对于窗口,由于创建窗口都是调用窗口类的Create函数,不用手工注册WNDCLASS类,也就不会调用SetWindowLong函数。但是毕竟缺乏安全性,而且当一秒钟内处理的窗口消息很多时,这种查找速度也可能不够快。<br>
  <br>
  &nbsp;&nbsp;&nbsp; 还有一种就是比较完美的解决办法,称之为thunk技术。thunk是一组动态生成的ASM指令,它记录了窗口类对象的this指针,并且这组指令可以当作函数,既也可以是窗口过程来使用。thunk先把窗口对象this指针记录下来,然后转向到静态stdProc回调函数,转向之前先记录HWND,然后把堆栈里HWND的内容替换为this指针,这样在stdProc里就可以从HWND取回对象指针,定位到WindowProc了。<br>
  <br>
  &nbsp;&nbsp;&nbsp; 我们先来看看窗口过程函数定义:<br>
  &nbsp;&nbsp;&nbsp;<br>
  &nbsp;&nbsp;&nbsp; LRESULT WINAPI WindowProc(HWND hWnd,UINT uMsg,WPARAM 
  wParam,LPARAM lParam)<br>
  &nbsp;&nbsp;&nbsp;&nbsp;<br>
  &nbsp;&nbsp;&nbsp; 其实当我们的窗口类CMyWnd创建窗口的时候,窗口句柄是可以得到并且作为成员数据保存,如此一来,第一个参数hWnd是可以不要的,因为可以通过this-&gt;m_hWnd得到,我们可以在这里做手脚,hWnd其实质是一个指针,如果把这个参数替换为窗口类对象的this指针,那么我们不就可以通过(CMyWnd*)hWnd-&gt;WindowProc转到窗口类内部的窗口过程了吗?但是窗口过程是系统调用的,怎么能把hWnd替换掉呢?我们先来看看系统调用这个函数时的堆栈情况:</p>
  <p>系统调用m_thunk时的堆栈:<br>
  ret HWND MSG WPARAM LPARAM<br>
  -------------------------------------------<br>
  栈顶&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  栈底<br>
  <br>
  系统把参数从右到左依次压栈,最后把返回地址压栈,我们只要在系统调用窗口过程时修改堆栈,把其中的hWnd参数替换掉就行了。这时thunk技术就有用武之地了,我们先定义一个结构:</p>
  <p>#pragma pack(push,1) //该结构必须以字节对齐<br>
  struct Thunk {<br>
  &nbsp;BYTE&nbsp;&nbsp;&nbsp;&nbsp;Call;<br>
  &nbsp;int&nbsp;&nbsp;&nbsp;&nbsp;Offset;<br>
  &nbsp;WNDPROC&nbsp;&nbsp;&nbsp;Proc;<br>
  &nbsp;BYTE&nbsp;&nbsp;&nbsp;&nbsp;Code[5];<br>
  &nbsp;CMyWnd*&nbsp;&nbsp; Window;<br>
  &nbsp;BYTE&nbsp;&nbsp;&nbsp;&nbsp;Jmp;<br>
  &nbsp;BYTE&nbsp;&nbsp;&nbsp;&nbsp;ECX;&nbsp;<br>
  };<br>
  #pragma pack(pop)<br>
  <br>
  类定义:<br>
  class CMyWnd<br>
  {<br>
  public:<br>
  &nbsp;&nbsp; BOOL Create(...);<br>
  &nbsp;&nbsp; LRESULT WINAPI WindowProc(UINT,WPARAM,LPARAM);<br>
  &nbsp;&nbsp; static LRESULT WINAPI InitProc(HWND,UINT,WPARAM,LPARAM);<br>
  &nbsp;&nbsp; static LRESULT WINAPI stdProc(HWND,UINT,WPARAM,LPARAM);<br>
  &nbsp;&nbsp;&nbsp;WNDPROC CreateThunk();<br>
  &nbsp;&nbsp;&nbsp;WNDPROC GetThunk(){return m_thunk}<br>
  &nbsp;&nbsp; ...<br>
  <br>
  private:<br>
  &nbsp;&nbsp; WNDPROC m_thunk<br>
  }</p>
  <p>在创建窗口的时候把窗口过程设定为this-&gt;m_thunk,m_thunk的类型是WNDPROC,因此是完全合法的,当然这个m_thunk还没有初始化,在创建窗口前必须初始化:</p>
  <p>WNDPROC CMyWnd::CreateThunk()<br>
  {<br>
  &nbsp;&nbsp; Thunk*&nbsp; thunk = new Thunk;<br>
  &nbsp;<br>
  &nbsp;///////////////////////////////////////////////<br>
  &nbsp;//<br>
  &nbsp;//系统调用m_thunk时的堆栈:<br>
  &nbsp;//ret HWND MSG WPARAM LPARAM<br>
  &nbsp;//-------------------------------------------<br>
  &nbsp;//栈顶&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  栈底<br>
  &nbsp;///////////////////////////////////////////////</p>
  <p>&nbsp;//call Offset<br>
  &nbsp;//调用code[0],call执行时会把下一条指令压栈,即把Proc压栈<br>
  &nbsp;thunk-&gt;Call = 0xE8;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // call 
  [rel]32<br>
  &nbsp;thunk-&gt;Offset = 
  (size_t)&amp;(((Thunk*)0)-&gt;Code)-(size_t)&amp;(((Thunk*)0)-&gt;Proc);&nbsp; 
  // 偏移量,跳过Proc到Code[0]<br>
  &nbsp;thunk-&gt;Proc = CMyWnd::stdProc;&nbsp; //静态窗口过程<br>
  &nbsp;<br>
  &nbsp;//pop ecx,Proc已压栈,弹出Proc到ecx&nbsp;<br>
  &nbsp;thunk-&gt;Code[0] = 0x59;&nbsp;&nbsp;//pop ecx<br>
  &nbsp;<br>
  &nbsp;//mov dword ptr [esp+0x4],this<br>
  &nbsp;//Proc已弹出,栈顶是返回地址,紧接着就是HWND了。<br>
  &nbsp;//[esp+0x4]就是HWND<br>
  &nbsp;thunk-&gt;Code[1] = 0xC7;&nbsp;&nbsp;//&nbsp;mov<br>
  &nbsp;thunk-&gt;Code[2] = 0x44;&nbsp;&nbsp;//&nbsp;dword ptr<br>
  &nbsp;thunk-&gt;Code[3] = 0x24;&nbsp;&nbsp;//&nbsp;disp8[esp]<br>
  &nbsp;thunk-&gt;Code[4] = 0x04;&nbsp;&nbsp;//&nbsp;+4<br>
  &nbsp;thunk-&gt;Window = this;<br>
  &nbsp;<br>
  &nbsp;//偷梁换柱成功!跳转到Proc<br>
  &nbsp;//jmp [ecx]<br>
  &nbsp;thunk-&gt;Jmp = 0xFF;&nbsp;&nbsp;&nbsp;&nbsp; // jmp [r/m]32<br>
  &nbsp;thunk-&gt;ECX = 0x21;&nbsp;&nbsp;&nbsp;&nbsp; // [ecx]<br>
  &nbsp;<br>
  &nbsp;m_thunk = (WNDPROC)thunk;<br>
  &nbsp;return m_thunk;<br>
  }<br>
  <br>
  这样m_thunk虽然是一个结构,但其数据是一段可执行的代码,而其类型又是WNDPROC,系统就会忠实地按窗口过程规则调用这段代码,m_thunk就把Window字段里记录的this指针替换掉堆栈中的hWnd参数,然后跳转到静态的stdProc:<br>
  <br>
  //本回调函数的HWND调用之前已由m_thunk替换为对象指针<br>
  LRESULT&nbsp;WINAPI CMyWnd::stdProc(HWND hWnd,UINT uMsg,UINT wParam,LONG 
  lParam)<br>
  {<br>
  &nbsp;CMyWnd* w = (CMyWnd*)hWnd;<br>
  &nbsp;<br>
  &nbsp;&nbsp;return&nbsp;w-&gt;WindowProc(uMsg,wParam,lParam);<br>
  }<br>
  <br>
  这样就把窗口过程转向到了类成员函数WindowProc,当然这样还有一个问题,就是窗口句柄hWnd还没来得及记录,因此一开始的窗口过程应该先定位到静态的InitProc,CreateWindow的时候给最后一个参数,即初始化参数赋值为this指针:</p>
  <p>CreateWindowEx(<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;dwExStyle,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  szClass,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  szTitle,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  dwStyle,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  x,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  y,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  width,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  height,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  hParentWnd,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  hMenu,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  hInst,<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  this&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; //初始化参数<br>
  &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
  );,</p>
  <p>在InitProc里面取出该指针:</p>
  <p>LRESULT&nbsp;WINAPI CMyWnd::InitProc(HWND hWnd,UINT uMsg,UINT wParam,LONG 
  lParam)<br>
  {&nbsp;&nbsp;<br>
  &nbsp;if(uMsg == WM_NCCREATE)<br>
  &nbsp;{<br>
  &nbsp;&nbsp;CMyWnd *w = NULL;<br>
  &nbsp;&nbsp;w = (CMyWnd*)((LPCREATESTRUCT)lParam)-&gt;lpCreateParams;<br>
  &nbsp;&nbsp;if(w)<br>
  &nbsp;&nbsp;{<br>
  &nbsp;&nbsp;&nbsp;//记录hWnd<br>
  &nbsp;&nbsp; w-&gt;m_hWnd = hWnd;<br>
  &nbsp;&nbsp;&nbsp;<br>
  &nbsp;&nbsp; //改变窗口过程为m_thunk<br>
  &nbsp;&nbsp;&nbsp;SetWindowLong(hWnd,GWL_WNDPROC,(LONG)w-CreateThunk());<br>
  &nbsp;&nbsp;&nbsp;return 
  (*(WNDPROC)(w-&gt;GetThunk()))(hWnd,uMsg,wParam,lParam);&nbsp;&nbsp;&nbsp;<br>
  &nbsp;&nbsp;}<br>
  &nbsp;}&nbsp;<br>
  &nbsp;return DefWindowProc(hWnd,uMsg,wParam,lParam);<br>
  }<br>
  <br>
  这样就大功告成。</p>
  <p><strong>窗口过程转发流程:<br>
  <br>
  </strong>&nbsp;&nbsp;&nbsp; 假设已建立CMyWnd类的窗口对象 CMyWnd 
  *window,初始化完毕后调用window-&gt;Create,这时Create的窗口其窗口过程函数是静态CMyWnd::InitWndProc</p>
  <p>&nbsp;</p>
  <p> 
  <table cellSpacing="0" cellPadding="0" border="0">
    <tbody>
      <tr>
        <td noWrap width="1" bgColor="#00ff00">InitWndProc</td>
        <td bgColor="#c0c0c0" colSpan="5" height="1">实现功能:window-&gt;Create创建窗口时已把对象this指针放入窗口初始化参数中,在此过程的WM_NCCREATE消息中把this指针取出来:CMyWnd 
          *w = (CMyWnd*)((LPCREATESTRUCT)lParam)-&gt;lpCreateParams;记录HWND:w-&gt;m_hWnd 
          = hWnd,然后设置窗口过程为w-&gt;m_thunk(thunk是一个WNDPROC类型的成员数据,所以可以设置)</td>
      </tr>
      <tr>
        <td align="middle">└→</td>
        <td noWrap bgColor="#00ff00" height="32">window-&gt;m_thunk</td>
        <td bgColor="#99ccff" colSpan="4" height="32">实现功能:跳转到静态CMyWnd::stdProc,在此之前替换系统的调用参数HWND为this指针</td>
      </tr>
      <tr>
        <td></td>
        <td align="middle">└→</td>
        <td bgColor="#00ff00">stdProc</td>
        <td noWrap bgColor="#c0c0c0" colSpan="3">实现功能:把HWND转换为窗口类指针:<br>
          CMyWnd *w = (CMyWnd*)hWnd;<br>
          return w-&gt;WindowProc(uMsg,wParam,lParam)</td>
      </tr>
      <tr>
        <td></td>
        <td></td>
        <td align="middle" height="32">└→</td>
        <td noWrap width="1%" bgColor="#00ff00">window-&gt;WindowProc</td>
        <td bgColor="#99ccff" colSpan="2">实现功能:执行实际的消息处理,窗口句柄已保存在m_hWnd</td>
      </tr>
    </tbody>
  </table>
  <p>&nbsp;题外话:thunk的汇编代码全部写在注释里了,把这段汇编转成数据可费了不少劲,当时手头没有合适的工具,只有一本《8086/8088汇编语言程序设计》,根据附录中的指令码汇总表转成机器码数据,那里面根本没有EAX,ECX,ESP等的概念,只能连蒙带猜加调试,非法操作了n(n&gt;10)回才得到那些数据,当时真是长出了一口气:TNND,终于搞定了!:-)</p>
</div>

</body>

</html>

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -