理解ATL中的一些汇编代码(通过Thunk技术来调用类成员函数)
我们知道ATL(活动模板库)是一套很小巧高效的COM开发库,它本身的核心文件其实没几个,COM相关的(主要是atlbase.h, atlcom.h),另外还有一个窗口相关的(atlwin.h), 所以拿来学习应该是很方便的。但是因为ATL的代码充满了模板和宏,内部还夹杂着汇编,所以如果没有比较丰富的C++模板和系统底层的知识,一般人会看得一头雾水。
下面我们主要分析一下ATL中的一些汇编代码。
ATL中出现汇编代码主要是2处,一处是通过Thunk技术来调用类成员函数处理消息;还有一处是通过打开_ATL_DEBUG_INTERFACES宏来跟踪接口的引用计数。
通过Thunk技术来调用类成员函数
我们知道Windows窗口的消息处理函数要求是面向过程的C函数,所以我们C++普通成员函数就不能作为窗口的消息处理函数,所以这里的问题就是如何让我们的C++成员函数和Windows的窗口的消息处理函数关联起来?MFC是通过一个Map来实现的,而ATL选择了更为高效的Thunk技术来实现。
我们将主要代码贴出来,然后介绍它的创建过程:
HWND CWindowImplBaseT< TBase, TWinTraits >::Create(HWND hWndParent, RECT& rcPos, LPCTSTR szWindowName,
DWORD dwStyle, DWORD dwExStyle, UINT nID, ATOM atom, LPVOID lpCreateParam)
{
ATLASSERT(m_hWnd == NULL);
if(atom == 0)
return NULL;
_Module.AddCreateWndData(&m_thunk.cd, this);
if(nID == 0 && (dwStyle & WS_CHILD))
nID = (UINT)this;
HWND hWnd = ::CreateWindowEx(dwExStyle, (LPCTSTR)MAKELONG(atom, 0), szWindowName,
dwStyle, rcPos.left, rcPos.top, rcPos.right - rcPos.left,
rcPos.bottom - rcPos.top, hWndParent, (HMENU)nID,
_Module.GetModuleInstance(), lpCreateParam);
ATLASSERT(m_hWnd == hWnd);
return hWnd;
}
WPARAM wParam, LPARAM lParam)
{
CContainedWindowT< TBase >* pThis = (CContainedWindowT< TBase >*)_Module.ExtractCreateWndData();
ATLASSERT(pThis != NULL);
pThis->m_hWnd = hWnd;
pThis->m_thunk.Init(WindowProc, pThis);
WNDPROC pProc = (WNDPROC)&(pThis->m_thunk.thunk);
WNDPROC pOldProc = (WNDPROC)::SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pProc);
#ifdef _DEBUG
// check if somebody has subclassed us already since we discard it
if(pOldProc != StartWindowProc)
ATLTRACE2(atlTraceWindowing, 0, _T("Subclassing through a hook discarded.\n"));
#else
pOldProc; // avoid unused warning
#endif
return pProc(hWnd, uMsg, wParam, lParam);
}
(1)通过调用类成员函数Create来创建窗口, Create时通过_Module.AddCreateWndData(&m_thunk.cd, this)将this指针保存起来.
(2)因为注册时将StartWindowProc设为窗口消息的回调处理函数,所以第一个窗口消息会进入到该函数,在函数入口通过_Module.ExtractCreateWndData()将保存的This指针取出来。
(3)将窗口函数WindowProc和This指针传给Thunk进行初始化。
Thunk初始化时写入一些汇编代码thunk.m_mov = 0x042444C7;thunk.m_this = (DWORD)pThis;这2行代码表示汇编代码mov dword ptr [esp+0x4], pThis, 而esp+0x4对应的是我们的第一个参数hWnd, 所以这个代码表示把我们的第一参数hWnd用This替代。
接下来汇编代码thunk.m_jmp = 0xe9; thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk));表示通过相对地址JMP跳转到WindowProc。
(4)回到StartWindowProc, 将Thunk地址作为我们新的窗口消息处理函数地址, 这样以后有任何新的窗口消息,调用的都是我们新的Thunk代码了。
(5)下一个窗口消息到来,调用我们新的Thunk代码,我们的Thunk代码将第一个hWnd参数替换成This指针,然后跳转到WindowProc
(6)在WindowProc函数中的第一参数已经被转成This指针,接下来我们就可以根据这个This指针调用它的虚函数ProcessWindowMessage了。
我们可以看到ATL这种通过Thunk关联类成员函数处理消息的方法非常高效,只是参数修改和跳转,基本上没有任何性能损失。
我们知道COM中引用计数的管理一直是个难题,因为你的接口是大家共用的,如果你引用计数管理出错,就会导致一些非常难查的问题,因此ATL中我们可以通过打开_ATL_DEBUG_INTERFACES宏 ,让我们通过Debug信息察看每个接口的引用计数情况。那么ATL是如何做到的呢?
相信用过ATL的人都会看到过这个代码:
{
STDMETHOD(f3)();
STDMETHOD(f4)();
STDMETHOD(f5)();
STDMETHOD(f1022)();
STDMETHOD(f1023)();
STDMETHOD(f1024)();
.
};
里面有1000多个方法,相信很多人到现在还不知道这些东西有什么用,其实我以前一直也没看懂这个东西。
下面我们来分析下ATL跟踪接口引用计数的过程,同样先贴代码:
const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject)
{
ATLASSERT(pThis != NULL);
// First entry in the com map should be a simple map entry
ATLASSERT(pEntries->pFunc == _ATL_SIMPLEMAPENTRY);
#if defined(_ATL_DEBUG_INTERFACES) || defined(_ATL_DEBUG_QI)
LPCTSTR pszClassName = (LPCTSTR) pEntries[-1].dw;
#endif // _ATL_DEBUG_INTERFACES
HRESULT hRes = AtlInternalQueryInterface(pThis, pEntries, iid, ppvObject);
#ifdef _ATL_DEBUG_INTERFACES
_Module.AddThunk((IUnknown**)ppvObject, pszClassName, iid);
#endif // _ATL_DEBUG_INTERFACES
return _ATLDUMPIID(iid, pszClassName, hRes);
}
__declspec(naked) inline HRESULT _QIThunk::f##n()\
{\
__asm mov eax, [esp+4]\
__asm cmp dword ptr [eax+8], 0\
__asm jg goodref\
__asm call atlBadThunkCall\
__asm goodref:\
__asm mov eax, [esp+4]\
__asm mov eax, dword ptr [eax+4]\
__asm mov [esp+4], eax\
__asm mov eax, dword ptr [eax]\
__asm mov eax, dword ptr [eax+4*n]\
__asm jmp eax\
}
....
(1)ATL内部是通过调用InternalQueryInterface来查询接口(QueryInterface)的,我们看到如果定义了宏_ATL_DEBUG_INTERFACES,它会增加一行代码 _Module.AddThunk((IUnknown**)ppvObject, pszClassName, iid)。
(2)AddThunk会创建一个_QIThunk,然后把我们的指针改成它新建的_QIThunk指针,这意味着我们上面QueryInterface的得到的指针已经被改成_QIThunk指针了, 因为我们所有的COM接口指针都是通过QueryInterface得到的,所以接下来任何COM接口的调用都会跑到_QIThunk中。
(3)_QIThunk是严格按照IUnknow布局的,它虚表函数依次是QueryInterface, AddRef, Release, f3, f4, ... f1023, f1024。现在任何AddRef和Release的调用我们都可以拦截到了,这样我们也就能跟踪每个接口的引用计数情况了。
(4)那如果调用其他接口函数怎么办?因为虚函数的调用实际上是根据虚表中索引位置来调用的,所以调用其他虚函数实际上就是调用f3, f4 ... f1024等。现在我们应该知道我们这1000多个虚函数的作用了。对,他们实际上只是占位函数,ATL假设任何接口都不会超过1024个方法。所以我们这些占位函数要实现的功能就是如何通过我们保存的原始IUnknown* pUnk, 转去调用它真正的虚函数。
(5)我们可以看到每个占位函数的实现都是一样的,他们会去调用一段汇编代码,我们看到这段汇编是裸代码(naked),下面我们来分析这段汇编代码.
__declspec(naked) inline HRESULT _QIThunk::f##n()\
{\
__asm mov eax, [esp+4]\ //将第一参数,即pQIThunk保存到eax
__asm cmp dword ptr [eax+8], 0\ //判断QIThunk的引用计数是否为0
__asm jg goodref\ //大于0才是正确的
__asm call atlBadThunkCall\
__asm goodref:\
__asm mov eax, [esp+4]\ //将第一参数,即pQIThunk保存到eax
__asm mov eax, dword ptr [eax+4]\ //取出QIThunk的原始接口指针IUnknown* pUnk
__asm mov [esp+4], eax\ //将原始接口指针保存替换刚调用过来的第一参数
__asm mov eax, dword ptr [eax]\ //取出原始接口指针保存的虚表地址,保存到eax
__asm mov eax, dword ptr [eax+4*n]\ //根据索引,取出原始虚表中对应的函数地址
__asm jmp eax\ //跳转到该函数地址
}
可以看到,通过上面的汇编代码,将原来是针对QIThunk的调用又转回到了我们原始的接口中。呵呵, 实际上应该是ATL拦截了我们原始的接口调用,转到了QIThunk中,而QIThunk最终又通过Thunk机制转回了原始的接口调用。
以前我看ATL也很吃力,以我个人的经验,一些东西刚开始看不太懂就放一放,先去看一些基本的东西,比如不懂COM,先去学下C++ 中的虚函数;不懂C++模板,先去学下STL;不懂Thunk,先去看一下汇编,等有了一定的积累,回头再看,一切就觉得没这么难了。
http://www.cppblog.com/weiym/archive/2012/10/23/193701.html
理解ATL中的一些汇编代码(通过Thunk技术来调用类成员函数)的更多相关文章
- 直接调用类成员函数地址(用汇编取类成员函数的地址,各VS版本还有所不同)
在C++中,成员函数的指针是个比较特殊的东西.对普通的函数指针来说,可以视为一个地址,在需要的时候可以任意转换并直接调用.但对成员函数来说,常规类型转换是通不过编译的,调用的时候也必须采用特殊的语法. ...
- C++中 线程函数为静态函数 及 类成员函数作为回调函数
线程函数为静态函数: 线程控制函数和是不是静态函数没关系,静态函数是在构造中分配的地址空间,只有在析构时才释放也就是全局的东西,不管线程是否运行,静态函数的地址是不变的,并不在线程堆栈中static只 ...
- 深入理解类成员函数的调用规则(理解成员函数的内存为什么不会反映在sizeof运算符上、类的静态绑定与动态绑定、虚函数表)
本文转载自:http://blog.51cto.com/9291927/2148695 总结: 一.成员函数的内存为什么不会反映在sizeof运算符上? 成员函数可以被看作是类 ...
- c++类成员函数重载常量与非常量版本时避免代码重复的一种方法
c++有时候需要为类的某个成员函数重载常量与非常量的版本,定义常量版本是为了保证该函数可作用于常量类对象上,并防止函数改动对象内容.但有时两个版本的函数仅仅是在返回的类型不同,而在返回前做了大量相同的 ...
- 在C++里一个类成员函数多少行代码才是最好呢?
这个问题,很多同事以及学生都问我这个问题.其实这是一个比较实际的问题,因为设计一个类成员函数的好与坏,决定了一个类代码的质量. 为了回答这个问题,昨晚又重新看看斯坦福大学的编程视频,可以用下面这个截图 ...
- vs2010 c++中内嵌汇编代码
在研究汇编时,需要自己写点汇编代码测试,用Ollydbg写每次加载程序就没了,不是很方便. 可以考虑直接在程序中写入汇编代码,只需要加上关键字“_asm”宏(C++代码中). 如下示例 编写环境 :v ...
- 如何在C或C++代码中嵌入ARM汇编代码
转载自:http://blog.csdn.net/roland_sun/article/details/42921131 大家知道,用C或者C++等高级语言编写的程序,会被编译器编译成最终的机器指令. ...
- ulua c#调用lua中模拟的类成员函数
项目使用ulua,我神烦这个东西.lua单纯在lua环境使用还好,一旦要跟外界交互,各种月经不调就来了.要记住贼多的细节,你才能稍微处理好.一个破栈,pop来push去,位置一会在-1,一会在-3,2 ...
- Delphi中methodaddress的汇编代码解析
class function TObject.MethodAddress(const Name: ShortString): Pointer;asm { -> EAX ...
随机推荐
- python用parammiko模块实现linux的远程操作
parammiko 可以实现远程的带密码登录,解决ssh远程登陆需要交互的问题 (当然很多其他的,如tcl也可以).但这个用python做比较简单 1.parammiko 的安装 1.1.依赖模块 ...
- 网络编程TCP总结及实践-C语言
网络变成首先要注意IP和port的转换,如今电脑基本上是主机字节序,存储依照小端方式,而在网络中传输统一使用大端方式,所以网络变成首先要注意字节序的转换. 一个经常使用的ip转换程序的实现: #inc ...
- SAX PULL解析实例
XML三种解析方式: SAX解析:基于事件驱动,事件机制基于回调函数的,得到节点和节点之间内容时也会回调事件 PULL解析:相同基于事件驱动,仅仅只是回调时是常量 DOM解析:是先把XML文件装入内存 ...
- 初次使用cocoapods注意事项
在仅仅用cocoapods时可能会遇到各种各样的错误和问题 这里中总结下: 1.首先使用cocoapods有非常多优点,在github上非常多优秀的开源项目都用到了它;假设你不会使用它,那么非常多优秀 ...
- javascript內容向上不間斷滾動
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- Mybatis插入语句useGeneratedKeys="true"的用法
<!-- 插入新的问题件 --> <!-- useGeneratedKeys="true"把新增加的主键赋值到自己定义的keyProperty(id)中 --&g ...
- MySQL(14):Select-limit(限制获得的记录数量)
1. limit 限制获得记录的数量 2.limit 语法: (1) limit offset, row_count: offset偏移量,从0开始. row_count总记录数. 分析: 案例演示 ...
- Hadoop32位和64位的查询
1.查看自己的hadoop版本是32位还是64位 进入: hadoop-2.6.4/lib/native 使用file命令 file libhadoop.so.1.0.0
- VS快速定位文件、代码插件——DPack
之前用Myeclipse开发一个Java项目,发现其中“Open Resource”(Ctrl+Shirft+R)的功能比较好用,回到.Net后就找了找VS相应的功能,试了几个后觉得Dpack比较好用 ...
- 纯 CSS 创建各种不同的图形形状
使用代码 矩形 .rectangle { width: 250px; height: 150px; background-color: #6DC75F; } <div></div&g ...