MFC:thunk技术实现窗口类的封装

2016-02-19 13:58 1 1 收藏

今天天气好晴朗处处好风光,好天气好开始,图老师又来和大家分享啦。下面给大家推荐MFC:thunk技术实现窗口类的封装,希望大家看完后也有个好心情,快快行动吧!

【 tulaoshi.com - 编程语言 】

  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技术就有用武之地了,我们先定义一个结构:

(本文来源于图老师网站,更多请访问https://www.tulaoshi.com/bianchengyuyan/)

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

(本文来源于图老师网站,更多请访问https://www.tulaoshi.com/bianchengyuyan/)

  这样就大功告成。

  窗口过程转发流程:

  假设已建立CMyWnd类的窗口对象 CMyWnd *window,初始化完毕后调用window-Create,这时Create的窗口其窗口过程函数是静态CMyWnd::InitWndProc

  题外话:thunk的汇编代码全部写在注释里了,把这段汇编转成数据可费了不少劲,当时手头没有合适的工具,只有一本《8086/8088汇编语言程序设计》,根据附录中的指令码汇总表转成机器码数据,那里面根本没有EAX,ECX,ESP等的概念,只能连蒙带猜加调试,非法操作了n(n10)回才得到那些数据,当时真是长出了一口气:TNND,终于搞定了!:-)

来源:https://www.tulaoshi.com/n/20160219/1605867.html

延伸阅读
标签: PHP
类的另类用法--数据的封装 一般的情况下,如果使用classname::property是不能访问到类的属性的,但可以用classname::method()使用类的方法。同样的也不能用objectname-property访问到类的方法里的变量。利用这一特点,我们可以将一些数据保存于类中,有点象c++的私有属性。 <? class data {   function value($var) ...
在VCL中包含有一个TList类,几乎可以实现链表所有功能,Delphi的工程师真是伟大。但是在实际应用中需要TTree类,来实现树的功能,我写了两个类TyuTree,TYuNode。可以方便实现,树创建,结点增删、移动功能。请大家指教。代码实例:Procedure Test();Var YuTree: TyuTree;Node: TYuNode;Begin    //第1步:创建树、增加第一个结点0Yu...
标签: Web开发
  1using System;   2using System.Data;   3using System.Configuration;   4using System.Web;   5using System.Web.Security;   6using System.Web.UI;   7using System.Web.UI.WebControls;   8using System.Web.UI.WebControls.WebParts;   9using Sys...
常用类 CRect: 用来表示矩形的类,拥有四个成员变量:top left bottom right。分别表是左上角和右下角的坐标。可以通过以下的方法构造: CRect( int l , int t , int r , int b ); 指明四个坐标 CRect( const RECT& srcRect ); 由RECT结构构造 CRect( LPCRECT lpSrcRect ); 由RECT结构构造 CRect( POINT point , SIZE ...
步骤/方法 (一) 原理 1、最小化的原理:首先要将窗口隐藏,然后在右下角绘制图标。 2、恢复的原理:将窗口显示,再将托盘中的图片删除。 (二)程序实现 1、自定义消息WM_SHOWTASK: #define WM_SHOWTASK (WM_USER +1) 2、在MFC的 ::OnSysCommand(UINT nID, LPARAM lParam) 函数体中增加一个命令响应 if(nID==SC_MINIMIZE) ToT...

经验教程

727

收藏

86
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部