C++之旅

  C++博客 :: 首页 :: 联系 :: 聚合  :: 管理
  7 Posts :: 0 Stories :: 8 Comments :: 0 Trackbacks

当注册窗口类时,WNDCLASSEX结构的lpfnWndProc成员应设置为窗口过程函数的地址,这是一个C风格的函数指针,所以我们只能使用全局或静态函数的地址,这在我们将窗口封装为C++类时会很麻烦,因为我们无法在一个全局或静态的WindowProc函数中访问类实例,这时Thunk技术可以大显身手了

  我们先分析一下其它的解决方案:一种是建立一个HWND到C++类实例的映射表,在WindowProc中通过这个映射表从HWND得到C++类实例。如果我们不考虑那一点点性能损失的话(每个消息在处理时都要查一次映射表)我想这也是个不错的办法(我们不考虑WM_NCDESTROY消息出现两次的变态情况)
  而另有一种方式是通过WndExtra来存放类实例指针,这样就可以在WindowProc中通过调用GetWindowLong来获取类实例指针了,缺点就是当别人使用你的C++类时如果不知道你用的WndExtra,他可能会把其它数据放到WndExtra中,于是后果不可设想
  现在让我们来看看Thunk方案吧,据我了解MFC/ATL都是基于这种方案实现。Thunk在这里是指一小段代码,可惜的是这段代码无法用C/C++来表示(因为是动态代码),只能用机器码写(汇编都不好使),这也就造成本方案应对多平台有点名义上的不完美。这里仅以i386体系来讨论。
  我们先假设一个C++窗口类

struct _THUNK;
class CWnd
{
    m_hWnd;
    HRESULT WINAPI static StdWindowProc(HWND, UINT, WPARAM, LPARAM);
    HRESULT WINAPI WindowProc(UINT, WPARAM, LPARAM);
    _THUNK* pThunk;
}

  _THUNK结构定义如下:

#pragma pack(push,1)
struct _THUNK
{
     DWORD   m_mov;          // mov dword ptr [esp+0x4], pThis (esp+0x4 is hWnd)
     DWORD   m_this;         //
     BYTE    m_jmp;          // jmp WndProc
     DWORD   m_relproc;      // relative jmp
};
#pragma pack(pop)

该结构实质是一段CPU指令,我将来要将其设置为窗口过程,这段指令将传入的第一个参数改为m_this(m_this是CWnd实例的this指针),然后由一个jmp指令跳转到代码地址m_relproc,这里m_relproc的值将是静态WindowProc函数的地址,当WindowProc将第一个参数取出时,这个参数已经不再是HWND,而是CWnd*了,这时就可以间接调用CWnd的实例成员了。

实例化_THUNK结构的代码如下:其中pThis指CWnd实例的指针,proc指静态StdWindowProc的地址,pThunk指_THUNK结构实例地址
  m_mov = 0x042444C7;  //C7 44 24 0C
  m_this = PtrToUlong(pThis);
  m_jmp = 0xe9;
  m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)pThunk+sizeof(_THUNK)));
  FlushInstructionCache(GetCurrentProcess(), pThunk, sizeof(_THUNK));
最后这句是告诉CPU清除当前指令缓存,因为我们更改了“代码”。m_relproc的值是proc到thunk下一条指令(也就是thunk后边紧挨着的那个地址)的相对地址偏移。m_mov与m_this合起来是:mov dword ptr[esp+0x4], pThis,意思是将 [栈顶+4] 地址处的4字节(DWORD)的值为pThis,也就是将第一上参数hWnd的值改为pThis。下面是栈的结构

栈底
......
......
栈顶 + 7
栈顶 + 6
栈顶 + 5
栈顶 + 4       4-7 原本存放着hWnd参数,在执行完Thunk后,其值为CWnd实例地址
-------------------------------------------
栈顶 + 3      0-3 存放着窗口过程的返回地址,
栈顶 + 2      WindowProc里在return之后会返回到该地址继续运行
栈顶 + 1
栈顶 + 0

因为Thunk执行后栈结构只是第一个参数被更改,返回地址并没有被破坏,所以StdWindowProc并不知道Thunk的存在而错误的认为自己是被直接调用,其内部的return语句会按栈顶的返回地址返回。

理论解决了,看一下流程,
创建并初始化CWnd::m_pThunk
先注册一个windows窗口类其中WNDCLASSEX的lpfnWindowProcx字段为CWnd::StdWindowProc的地址
然后用CreateWindowEx创建窗口,最后一个参数设置为当前CWnd实例的this指针的值
在StdWindowProc中我们响应WM_NCCREATE消息,从lParam参数指向的CREATESTRUCT结构体的lpCreateParams字段中取出CWnd的实例指针,通过SetWindowLong将窗口过程更改为CWnd实例的m_pThunk的值,然后手动调用CWnd实例的WindowProc
在StdWindowProc中响应任何其它消息时,我们将参数hWnd强制类型转换为CWnd*,将通过这个指针调用CWnd::WindowProc。

最扣补充一句,因为新版Windows或最新的Server Packs都加入了数据执行保护功能,按上面的方法弄很可能出现异常,因为Thunk是数据结构,会被放在数据段中并被标记不不可执行。解决办法之一是使用VirtualAlloc方法动态为thunk分配内在,并使用PAGE_EXECUTE_READWRITE标志,记得最后使用VirtualFree释放该内在。

posted on 2008-01-27 18:13 汪江涛 阅读(1094) 评论(6)  编辑 收藏 引用

Feedback

# re: 通过Thunk将类的非静态成员函数设置为WindowProc 2008-01-28 09:51 Fox
没试过这个方法,我一直用类静态func做的,有时间试试。。  回复  更多评论
  

# re: 通过Thunk将类的非静态成员函数设置为WindowProc[未登录] 2008-01-31 16:25 hongsion
你最后提到

最扣补充一句,因为新版Windows或最新的Server Packs都加入了数据执行保护功能,按上面的方法弄很可能出现异常,因为Thunk是数据结构,会被放在数据段中并被标记不不可执行。解决办法之一是使用VirtualAlloc方法动态为thunk分配内在,并使用PAGE_EXECUTE_READWRITE标志,记得最后使用VirtualFree释放该内在。


如果这么说,难道在新版的windows中,ATL和MFC写的代码都不能用了?
因为我发现ATL和MFC都用了这样的thunk代码。

  回复  更多评论
  

# re: 通过Thunk将类的非静态成员函数设置为WindowProc 2008-01-31 17:46 汪江涛
回楼上:ATL 8.0 中用的内存函数如下:
PVOID __stdcall __AllocStdCallThunk(VOID);
VOID __stdcall __FreeStdCallThunk(PVOID);
具体实现不清楚,但应该已经处理了DEP的问题,不确定是不是用的VirtualAlloc和VirtualFree,但MS官方文档中关于DEP的内容提到了用VirtualAlloc和VirtualFree可以解决,其它方式我就不表楚了。

对于ATL 7.1 及以前版本中确实是有DEP问题的,应用程序可以通过SetProcessDEPPolicy来禁用当前进程的DEP,但似乎并不保证总是成功。

以下是MS的DEP相关资料

http://msdn2.microsoft.com/en-us/library/bb736299.aspx

http://technet2.microsoft.com/WindowsServer/zh-CHS/Library/b0de1052-4101-44c3-a294-4da1bd1ef2272052.mspx?mfr=true

  回复  更多评论
  

# re: 通过Thunk将类的非静态成员函数设置为WindowProc 2008-02-01 14:04 XuQ
使用SetProcessDEPPolicy禁用DEP会不会需要程序有管理员级别的权限?
另外在VS2005中使用VS2003编译的thunk代码,就会出现问题,在VS2003中就没有问题,好像与.net2.0有关。  回复  更多评论
  

# re: 通过Thunk将类的非静态成员函数设置为WindowProc 2008-02-02 11:31 汪江涛
应该不需要管理员权限,但依赖系统的DEP策略,所以可能调用会失败。“在VS2005中使用VS2003编译的thunk代码”不太明白什么意思?你是说在VS2005中使用VS2003编译的包含thunk代码的静态或动态链接库吗?这个我就不楚了,你不防把错误信息贴出来,说不定有人知道,不过有一点注意,VC++2005默认设置是最大限度的与C++标准兼容,而VC++2003与C++标准兼容程度似乎差很多,这可能导致VC++2003兼容的代码在VC++2005中无法编译。可以肯定跟.net2.0无关,在VS2005和VS2003中,C++分托管和非托管,非托管C++跟.net几乎毫不相干。  回复  更多评论
  

# re: 通过Thunk将类的非静态成员函数设置为WindowProc 2008-06-16 20:46 XuQ
@汪江涛
更正一下,应该是基于.net2.0的应用程序加载C++编写的dll(vs2005编译),然后再在dll中加载vs2003编译的com,如果com中使用atl窗体(会使用thunk技术),在运行时就会出现异常。如果改为vs2003(基于.net1.0)编译应用程序,就不会出现问题。
所以猜测跟.net2.0有关。
不知道楼主是否遇到过此类现象,因为种种原因,加载方式不能更改,请教是否有其他解决方案,如果要改thunk就只能动atl库的代码了。  回复  更多评论
  


标题  
姓名  
主页
验证码 *
内容(提交失败后,可以通过“恢复上次提交”恢复刚刚提交的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
[使用Ctrl+Enter键可以直接提交]
相关链接:
网站导航: