proguru

posts(11) comments(62) trackbacks(0)
  • C++博客
  • 联系
  • RSS 2.0 Feed 聚合
  • 管理

常用链接

  • 我的随笔
  • 我的评论
  • 我参与的随笔

留言簿

  • 给我留言
  • 查看公开留言
  • 查看私人留言

随笔分类(11)

  •  C++(1)
  •  Design Patterns(1)
  •  GUI(6)
  •  Linux(1)
  •  Misc(2)
  •  Plugins architecture

随笔档案(11)

  • 2011年4月 (1)
  • 2011年3月 (1)
  • 2009年8月 (1)
  • 2009年7月 (2)
  • 2009年6月 (1)
  • 2009年1月 (1)
  • 2008年8月 (4)

搜索

  •  

最新评论

  • 1. re: 彻底放弃CN域名
  • 可是我的com,而且买国内空间,刚上线一下子。根本无敏感内容就被墙了。com也会被墙啊,汗。
  • --do1do2
  • 2. re: 彻底放弃CN域名
  • 哥一水的.net和.com, 哈哈
  • --打击装B犯
  • 3. re: 彻底放弃CN域名
  • 可是现在,当你访问慢的时候,被墙的时候,你会觉得不仅仅被鱼肉。。。
  • --溪流
  • 4. re: 彻底放弃CN域名
  • 楼主,这个可以不用发到首页吧。。
  • --Bill Hsu
  • 5. re: 轻量级开源C++ GUI开发框架KWinUI正式发布[未登录]
  • x64下的thunk代码还是有问题。
    普通thunk,非wndprocthunk。
    期待与你联系。

    我的QQ:1090833
  • --Loaden

阅读排行榜

评论排行榜

View Post

GUI之窗口过程thunk

    转载请注明出处: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阻止,程序执行失败。因此可以申请一个具有执行权限的堆来解决这个问题:
1     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只是开了一个头。

posted on 2008-08-24 20:52 proguru 阅读(3646) 评论(20)  编辑 收藏 引用 所属分类: GUI

View Comments

# re: GUI之窗口过程thunk  回复  更多评论   
其实command模式就行了.
2008-08-24 21:51 | Condor
# re: GUI之窗口过程thunk  回复  更多评论   
@Condor
command可以解决?能说的稍微详细一些吗?很有兴趣.
2008-08-24 22:17 | proguru
# re: GUI之窗口过程thunk  回复  更多评论   
只要wndproc能找到application对象,application对象自然就有所有已经创建的窗口的总列表了。因此application是singleton,一切都迎刃而解。
2008-08-25 01:30 | 陈梓瀚(vczh)
# re: GUI之窗口过程thunk  回复  更多评论   
@陈梓瀚(vczh)
是的。有若干种方法可以达到目的。各有优劣罢了...
2008-08-25 08:48 | proguru
# re: GUI之窗口过程thunk  回复  更多评论   
通过这种thunk方法,不用记录已经创建的窗口列表,因为窗口与其对象是天然绑定在一起的。
2008-08-25 08:50 | proguru
# re: GUI之窗口过程thunk  回复  更多评论   
@proguru
自己用个map把窗口与指针装起来,另有好处。比如可以对map进行循环,进程结束时可以利用这个map做一些其它的事情,例如帮助用户销毁没有销毁的句柄,删除未删除的指针。
2008-08-25 09:13 | cexer
# re: GUI之窗口过程thunk  回复  更多评论   
@cexer
"帮助用户销毁没有销毁的句柄,删除未删除的指针"
---典型的责任不明确,一般的原则应该是谁的对象谁负责销毁,除非特殊情况。
2008-08-25 09:17 | proguru
# re: GUI之窗口过程thunk[未登录]  回复  更多评论   
GUI完全属于特殊情况,因为我们知道容器的子控件在容器消失的时候也应当消失。所以我们没有理由要求用户在删除form之前删除里面的东西。这个是非常合理的。
2008-08-25 10:07 | 陈梓瀚(vczh)
# re: GUI之窗口过程thunk  回复  更多评论   
@陈梓瀚(vczh)
form里使用了哪些组件,form是完全清楚的,form被删除的时候有责任在析构函数里销毁掉那些子组件。
2008-08-25 10:23 | proguru
# re: GUI之窗口过程thunk  回复  更多评论   
@proguru
说的就是“特殊情况”。
2008-08-25 11:49 | cexer
# re: GUI之窗口过程thunk  回复  更多评论   
在windows下,可以把this指針和窗口實例連接起來:

WNDCLASSEX wcex; //窗口类结构
//填充窗口类结构
memset(&wcex, 0, sizeof(WNDCLASSEX));
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS; //类风格
wcex.lpfnWndProc = lpfnWndProc; //窗口过程
wcex.cbClsExtra = 0; // 窗口类额外数据
wcex.cbWndExtra = 4; // 窗口实例额外数据,存放 GXWin的this指针
............

RegisterClassEx( &wcex );

m_hWnd = CreateWindowEx(
dwExStyle, //扩展风格
m_szClassName, // 窗口类名
lpszTitle, // 窗口标题
dwStyle, // 风格
(GetSystemMetrics(SM_CXSCREEN) - m_iWidth) / 2 , // 位置(x,y)
(GetSystemMetrics(SM_CYSCREEN) - m_iHeight) / 2, //
m_iWidth, // 宽
m_iHeight, // 高
hParent, // 父窗口句柄
NULL, // 菜单句柄
hInstance, // 进程实例
NULL // lParam, 用户定义数据
);
if(m_hWnd == NULL)
{
return FALSE;
}
//---------------------------------------------
// 将窗口对象 与 HWND 关联起来
// 之后窗口函数可以作为
// GXWin 类成员函数实现了
//---------------------------------------------
SetWindowLong(m_hWnd, GWL_USERDATA, (LONG)this);

//----------------------------------------------------
// 静态成员函数
//----------------------------------------------------
LRESULT CALLBACK GXWin::DoCallback(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
LRESULT rt;

GXWin* pWnd = (GXWin*)GetWindowLong(hwnd, GWL_USERDATA);

if(pWnd) // 调用 JWindow 类定义事件处理函数
{
rt = pWnd->WinProc(message, wParam, lParam);
}
else // 调用缺省的事件处理函数
{
rt = GXWin::gxDefWinProc (hwnd, message, wParam, lParam);
}
return rt;
}
2008-08-25 11:59 | jmchxy
# re: GUI之窗口过程thunk  回复  更多评论   
@jmchxy
是的,全局函数和static成员函数都可以,文中有述!
2008-08-25 12:23 | proguru
# re: GUI之窗口过程thunk  回复  更多评论   
所說不在全局或靜態成員問題,而是針對"每个都是一个窗口。对象与窗口之间的映射可能就是个不小的问题", 想說的是:簡潔的解決方法不是沒有。
2008-08-25 15:52 | jmchxy
# re: GUI之窗口过程thunk[未登录]  回复  更多评论   
map<control* , HWND>
2008-08-25 17:06 | 陈梓瀚(vczh)
# re: GUI之窗口过程thunk  回复  更多评论   
GXWin* pWnd = (GXWin*)GetWindowLong(hwnd, GWL_USERDATA);

低效的做法
2008-08-29 14:09 | biinwi
# re: GUI之窗口过程thunk  回复  更多评论   
thunk
方法确实是一个巧妙的办法,不过如果不想用汇编的话,其实弄一个模板也能实现的,在窗口类中定义一个模板类就行了。

2008-08-30 13:43 | hsen
# re: GUI之窗口过程thunk[未登录]  回复  更多评论   
非常好的思路,可以实现跨x86和x64
在x86下测试通过,但在x64下失败:崩溃了。
能否加我QQ:1090833,或我加你QQ,想请教一下崩溃的原因以及如何修正。
2009-03-22 08:22 | Loaden
# re: GUI之窗口过程thunk  回复  更多评论   
@Loaden
x86下我做过完全的测试,x64下没有测试过,没有测试环境,如果你测试、修改通过,希望能给个反馈,谢谢。
2009-03-22 10:41 | proguru
# re: GUI之窗口过程thunk[未登录]  回复  更多评论   
我在CSDN发了个求助帖,能否帮忙看看。
http://topic.csdn.net/u/20090322/08/b6bf82ca-8ba2-452b-92f8-bb2adb05a1ef.html
因为我汇编外行,所以只能尝试修改。而我是在qemu虚拟x64机下测试,很慢。
我觉得理论上完全行得通,但是否x64的汇编机器码对应有误?能否帮忙检查一下。
2009-03-22 11:52 | Loaden
# re: GUI之窗口过程thunk[未登录]  回复  更多评论   
mov qword ptr [rax], rcx
这一行的机器码不对,要改成:488908
另,我在x64下用WinDBG调试发现:mov rcx, rax 的机器码应该是:488bc8
但如果用:4889c1 也可以。但我认为还是应该改成:488bc8

不知道你看过ATL的atlstdcall.h没有?如果能在他的基础上实现thunk,可能更加完善。毕竟要修改第一个数据成员:这还是有局限性了。Thunk的功能打折扣了。

但x64的难题是:前四个参数都在寄存器中。
我目前的思路是:把前四个参数依次后移,将第四个参数入栈,可我只会点汇编的皮毛,能否指点一下如何实现?只要写出汇编代码即可,机器码我可以通过WinDBG来调试得到。
2009-03-22 19:07 | Loaden
刷新评论列表

只有注册用户登录后才能发表评论。
【推荐】100%开源!大型工业跨平台软件C++源码提供,建模,组态!
相关文章:
  • 基于KWinUI的换肤框架KSkinX的一个简单Demo
  • KWinUI最新sample
  • 轻量级开源C++ GUI开发框架KWinUI正式发布
  • KWinGUI的一个DEMO
  • GUI之窗口过程thunk
  • CPP博客首篇-兼论GUI轮子
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理


 
Powered by:
C++博客
Copyright © proguru