GUI之窗口过程thunk

时间:2022-07-25 17:01:08
转载请注明出处:http://www.cppblog.com/proguru/archive/2008/08/24/59831.html

    thunk是什么?查字典只能让人一头雾水。thunk是一段插入程序中实现特定功能的二进制代码,这个定义是我下的,对不对各位看官请自己斟酌,呵呵。

    我这里要讲的是窗口回调专用thunk,thunk的核心是调用栈动态修改技术。地球人都知道,windows的窗口回调函数是一个全局函数,类成员函数是不可以作为窗口回调函数的,因为它有this指针,这给我们用C++来包装窗口带来不小的麻烦。你说什么?用一个全局函数或类的静态成员函数来做窗口回调函数?这肯定没问题。但是这样带来的麻烦也许比你想象的要多,想想我们的GUI Framework不会只有一个类,而是一个类层级结构,会有许许多许多、形形色色的widget,每个都是一个窗口。对象与窗口之间的映射可能就是个不小的问题,像MFC那样搞?太落伍了吧!用thunk就要简单的多。WTL用了thunk,但是不够彻底。
    废话少说,先贴出thunk核心代码。

1 /*
  2  *    thunk with DEP support
  3  *
  4  *    author:proguru
  5  *    July 9,2008
  6  */
  7 #ifndef __KTHUNK_H__
  8 #define __KTHUNK_H__
  9 #include "windows.h"
 10 
 11 //#define USE_THISCALL_CONVENTION    //turn it off for c++ builder compatibility
 12 
 13 #ifdef USE_THISCALL_CONVENTION
 14     #define WNDPROC_THUNK_LENGTH      29     //For __thiscall calling convention ONLY,assign m_hWnd by thunk
 15     #define GENERAL_THUNK_LENGTH    10
 16     #define KCALLBACK                        //__thiscall is default
 17 #else
 18     #define WNDPROC_THUNK_LENGTH       22     //__stdcall calling convention ONLY,assign m_hWnd by thunk
 19     #define GENERAL_THUNK_LENGTH    16
 20     #define KCALLBACK __stdcall
 21 #endif
 22 
 23 extern HANDLE g_hHeapExecutable;
 24 
 25 class KThunkBase{
 26 public:
 27     KThunkBase(SIZE_T size){
 28         if(!g_hHeapExecutable){        //first thunk,create the executable heap
 29             g_hHeapExecutable=::HeapCreate(HEAP_CREATE_ENABLE_EXECUTE,0,0);
 30             //if (!g_hHeapExecutable) abort
 31         }
 32         m_szMachineCode=(unsigned char*)::HeapAlloc(g_hHeapExecutable,HEAP_ZERO_MEMORY,size);
 33     }
 34     ~KThunkBase(){
 35         if(g_hHeapExecutable)
 36             ::HeapFree(g_hHeapExecutable,0,(void*)m_szMachineCode);
 37     }
 38     inline void* GetThunkedCodePtr(){return &m_szMachineCode[0];}
 39 protected:
 40     unsigned char*    m_szMachineCode;
 41 };
 42 
 43 class KWndProcThunk : public KThunkBase{
 44 public:
 45     KWndProcThunk():KThunkBase(WNDPROC_THUNK_LENGTH){}
 46     void Init(INT_PTR pThis, INT_PTR ProcPtr){
 47 #ifndef _WIN64
 48         #pragma warning(disable: 4311)
 49         DWORD dwDistance =(DWORD)(ProcPtr) - (DWORD)(&m_szMachineCode[0]) - WNDPROC_THUNK_LENGTH;
 50         #pragma warning(default: 4311)
 51 
 52     #ifdef USE_THISCALL_CONVENTION
 53         /*
 54         For __thiscall, the default calling convention used by Microsoft VC++, The thing needed is
 55         just set ECX with the value of 'this pointer'
 56 
 57         machine code                       assembly instruction        comment
 58         ---------------------------       -------------------------    ----------
 59         B9 ?? ?? ?? ??                    mov ecx, pThis                 ;Load ecx with this pointer
 60         50                                PUSH EAX            
 61         8B 44 24 08                        MOV EAX, DWORD PTR[ESP+8]     ;EAX=hWnd
 62         89 01                            MOV DWORD PTR [ECX], EAX      ;[pThis]=[ECX]=hWnd
 63         8B 44 24 04                        mov eax,DWORD PTR [ESP+04H]    ;eax=(return address)
 64         89 44 24 08                        mov DWORD PTR [ESP+08h],eax    ;hWnd=(return address)
 65         58                                POP EAX            
 66           83 C4 04                        add ESP,04h            
 67                         
 68         E9 ?? ?? ?? ??                    jmp ProcPtr                  ;Jump to target message handler
 69         */
 70         m_szMachineCode[0]                 = 0xB9;
 71         *((DWORD*)&m_szMachineCode[1] ) =(DWORD)pThis;
 72         *((DWORD*)&m_szMachineCode[5] )    =0x24448B50;    
 73         *((DWORD*)&m_szMachineCode[9] )    =0x8B018908;
 74         *((DWORD*)&m_szMachineCode[13])    =0x89042444;
 75         *((DWORD*)&m_szMachineCode[17])    =0x58082444;
 76         *((DWORD*)&m_szMachineCode[21])    =0xE904C483;
 77         *((DWORD*)&m_szMachineCode[25]) =dwDistance;    
 78     #else    
 79         /*
 80          * 01/26/2008 modify
 81         For __stdcall calling convention, replace 'HWND' with 'this pointer'
 82 
 83         Stack frame before modify             Stack frame after modify
 84 
 85         :            :                        :             :
 86         |---------------|                        |----------------|
 87         |     lParam    |                        |     lParam     |
 88         |---------------|                        |----------------|
 89         |     wParam    |                        |     wParam     |
 90         |---------------|                        |----------------|
 91         |     uMsg      |                        |     uMsg       |
 92         |---------------|                        |----------------|
 93         |     hWnd      |                        | <this pointer> |
 94         |---------------|                        |----------------|
 95         | (Return Addr) | <- ESP                 | (Return Addr)  | <-ESP
 96         |---------------|                        |----------------|
 97         :            :                        :             | 
 98 
 99         machine code        assembly instruction            comment    
100         ------------------- ----------------------------    --------------
101         51                  push ecx
102         B8 ?? ?? ?? ??      mov  eax,pThis                    ;eax=this;
103         8B 4C 24 08         mov  ecx,dword ptr [esp+08H]    ;ecx=hWnd;
104         89 08                mov  dword ptr [eax],ecx          ;[this]=hWnd,if has vftbl shound [this+4]=hWnd            
105         89 44 24 08            mov  dword ptr [esp+08H], eax    ;Overwite the 'hWnd' with 'this pointer'
106         59                    pop  ecx
107         E9 ?? ?? ?? ??      jmp  ProcPtr                    ; Jump to target message handler
108         */
109 
110         *((WORD  *) &m_szMachineCode[ 0]) = 0xB851;
111         *((DWORD *) &m_szMachineCode[ 2]) = (DWORD)pThis;
112         *((DWORD *) &m_szMachineCode[ 6]) = 0x08244C8B;
113         *((DWORD *) &m_szMachineCode[10]) = 0x44890889;
114         *((DWORD *) &m_szMachineCode[14]) = 0xE9590824;
115         *((DWORD *) &m_szMachineCode[18]) = (DWORD)dwDistance;
116     #endif //USE_THISCALL_CONVENTION
117 #else    //_WIN64
118         /* 
119         For x64 calling convention, RCX hold the 'HWND',copy the 'HWND' to Window object,
120         then insert 'this pointer' into RCX,so perfectly!!!        
121 
122         Stack frame before modify                Stack frame after modify
123 
124         :            :                        :             :
125         |---------------|                        |----------------|
126         |               | <-R9(lParam)           |                | <-R9(lParam)
127         |---------------|                        |----------------|
128         |               | <-R8(wParam)           |                | <-R8(wParam)
129         |---------------|                        |----------------|
130         |               | <-RDX(msg)             |                | <-RDX(msg)
131         |---------------|                        |----------------|
132         |               | <-RCX(hWnd)            |                | <-RCX(this)
133         |---------------|                        |----------------|
134         | (Return Addr) | <-RSP                  | (Return Addr)  | <-RSP
135         |---------------|                        |----------------|
136         :            :                        :         :
137 
138         machine code            assembly instruction     comment
139         -------------------       -----------------------    ----
140         48B8 ????????????????    mov RAX,pThis
141         4808                    mov qword ptr [RAX],RCX    ;m_hWnd=[this]=RCX
142         4889C1                    mov RCX,RAX                ;RCX=pThis
143         48B8 ????????????????    mov RAX,ProcPtr    
144         FFE0                    jmp RAX        
145         */
146         *((WORD   *)&m_szMachineCode[0] )    =0xB848;
147         *((INT_PTR*)&m_szMachineCode[2] )    =pThis;
148         *((DWORD  *)&m_szMachineCode[10])    =0x89480848;
149         *((DWORD  *)&m_szMachineCode[14])    =0x00B848C1;
150         *((INT_PTR*)&m_szMachineCode[17])    =ProcPtr;
151         *((WORD   *)&m_szMachineCode[25])    =0xE0FF;
152 #endif
153     }
154 };


是不是有些头晕?且待我慢慢分解。
    类成员函数有两种调用约定,MS VC++默认采用thiscall调用约定,而Borland C++默认采用stdcall调用约定。thiscall采用ECX寄存器来传递this指针,而stdcall则通过栈来传递this指针,this指针是成员函数隐藏的第一个参数。而到了x64平台,则问题有了新的变化。为了充分利用寄存器,提高效率,函数的前四个参数默认用寄存器传递,分别是RCX,RDX,R8和R9。对于成员函数,其this指针通过RCX传递。x64 thunk代码我并未测试过,因为一直未使用x64平台,不过应该不会有太大问题。
    在这里,我只分析x86平台上使用stdcall调用习惯的thunk代码。因为这段代码将窗口回调函数调用栈上的HWND直接修改this指针,所以有两个问题需要提前了解一下。
    第一、我将回调函数的signature修改为如下形式:
LRESULT KCALLBACK KWndProc(UINT uMsg, WPARAM wParam, LPARAM lParam) ;
请注意这是个成员函数,而且没有HWND hWnd这个参数。
    第二、窗口类的第一个数据成员必须是窗口句柄变量,我的是HWND m_hWnd.至于为什么要这样,后面会有提及。
    现在请看代码第85行开始的图形,前一个是修改前windows调用我们提供的回调函数的栈结构,后一个则是为了适应我们的需求修改过后的调用栈。首先,我们的回调函数需要一个this指针,而且要放到栈上第一个参数的位置上,这是通过第46行的thunk初始化函数Init传
递进来的。其次我们的窗口对象必须要得到自己所对应的窗口句柄,不然一切都是空谈。
    那么我们可以用thunk来修改调用栈。首先用初始栈上的第一个参数,也就是实际的窗口句柄,传递给窗口对象。如何传递呢?因为m_hWnd成员是对象的第一个数据成员,那么很简单,如果没有虚函数的存在,那么这个m_hWnd就静静地待在对象的最开始处,就是this指针所指向的位置。如果有虚函数的存在,那么事情也不是太复杂,对象的起始处现在是VPTR,m_hWnd紧随其后,代码略作调整即可。其次用this指针覆盖栈上的第一个参数,也就是窗口句柄HWND。下面是逐条注释的汇编格式指令:

1 push ecx                        ;保护ecx,后面会用到
2 mov  eax,pThis                    ;传送this指针到eax. eax=this; 
3 mov  ecx,dword ptr [esp+08H]    ;把调用栈上的第一个参数送ecx. ecx=hWnd
4 mov  dword ptr [eax],ecx          ;把窗口句柄赋予窗口对象数据成员m_hWnd.
5                                 ;[this]=hWnd,if has vftbl shound [this+4]=hWnd            
6 mov  dword ptr [esp+08H], eax    ;用this指针覆盖调用栈上的第一个参数即窗口句柄
7                                 ;Overwite the 'hWnd' with 'this pointer'
8 pop  ecx                        ;弹出先前ecx
9 jmp  ProcPtr                    ;跳转到消息处理函数.Jump to target message handler
这样就把窗口(句柄)和窗口对象完美的绑定到一起,不需要一个对应查找表,不使用任何全局或静态的数据,满足thread safe。
    至于汇编格式指令翻译到机器码的问题,下载intel的指令手册,查查表就可以了。
    下面的代码展示了thunk的使用(删除了不相干的代码):

1 template <typename T,typename TBase=KWindow>
 2 class  KWindowRoot : public TBase{
 3 public:
 4     KWindowRoot():TBase(){
 5         T* pT=static_cast<T*>(this);
 6         m_thunk.Init((INT_PTR)pT, pT->GetMessageProcPtr());
 7     }
 8 
 9     INT_PTR GetMessageProcPtr(){
10         typedef LRESULT (KCALLBACK T::*KWndProc_t)(UINT,WPARAM,LPARAM);
11         union{
12             KWndProc_t     wndproc;
13             INT_PTR        dwProcAddr;            
14         }u;
15         u.wndproc=&T::KWndProc;
16         return u.dwProcAddr;
17     }
18 
19     LRESULT KCALLBACK KWndProc(UINT uMsg, WPARAM wParam, LPARAM lParam){
20         T* pT=static_cast<T*>(this);
21         return pT->ProcessWindowMessage(uMsg,wParam,lParam);
22     }
23 
24 
25 protected:
26     KWndProcThunk    m_thunk;
27     inline INT_PTR     GetThunkedProcPtr(){return (INT_PTR)m_thunk.GetThunkedCodePtr();}
28 };

在基类KWindow中HWND m_hWnd是其第一个数据成员。因为使用了模板的静态多态特性,故对象没有VPTR指针。
    到了这里事情还没有结束。既然使用thunk就不得不面对DEP。DEP会阻止没有执行权限的内存执行代码。如果我们的thunk分配在栈上或new出来的堆上,则会被DEP阻止,程序执行失败。因此可以申请一个具有执行权限的堆来解决这个问题:

 KThunkBase(SIZE_T size){
2         if(!g_hHeapExecutable){        //first thunk,create the executable heap
3             g_hHeapExecutable=::HeapCreate(HEAP_CREATE_ENABLE_EXECUTE,0,0);
4             //if (!g_hHeapExecutable) abort
5         }
6         m_szMachineCode=(unsigned char*)::HeapAlloc(g_hHeapExecutable,HEAP_ZERO_MEMORY,size);
7     }

   总的来讲thunk的空间和时间开销都是足够小的,甚至可以忽略不计。但是却带来了极大的便利。
    thunk只是开了一个头。