当前位置: 代码迷 >> 综合 >> Thunk::在类中封装回调函数
  详细解决方案

Thunk::在类中封装回调函数

热度:9   发布时间:2023-12-07 23:55:40.0

文章来自:http://hi.baidu.com/%D3%A3%C1%E8%D7%BC/blog/item/ead069dd2e2ec3335882dd02.html

在类中封装回调函数


有几种解决办法:  
   
          一种解决方法是用窗口列表,开一个结构数组,窗口类对象创建窗口的时候把窗口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);  
}  
   
这样就大功告成。   

 

一种实现Win32窗口过程函数(Window Procedure)的新方法: 基于Thunk实现的类成员消息处理函数

Thunk 技术的一个改进