`
chinamming
  • 浏览: 151146 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

理解ATL中的一些汇编代码

 
阅读更多
我们知道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技术来实现。

我们将主要代码贴出来,然后介绍它的创建过程:
template<classTBase,classTWinTraits>
HWNDCWindowImplBaseT<TBase,TWinTraits>::Create(HWNDhWndParent,RECT&rcPos,LPCTSTRszWindowName,
DWORDdwStyle,DWORDdwExStyle,UINTnID,ATOMatom,LPVOIDlpCreateParam)
{
ATLASSERT(m_hWnd==NULL);

if(atom==0)
returnNULL;

_Module.AddCreateWndData(&m_thunk.cd,this);

if(nID==0&&(dwStyle&WS_CHILD))
nID=(UINT)this;

HWNDhWnd=::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);

returnhWnd;
}
staticLRESULTCALLBACKStartWindowProc(HWNDhWnd,UINTuMsg,
WPARAMwParam,LPARAMlParam)
{
CContainedWindowT<TBase>*pThis=(CContainedWindowT<TBase>*)_Module.ExtractCreateWndData();
ATLASSERT(pThis!=NULL);
pThis->m_hWnd=hWnd;
pThis->m_thunk.Init(WindowProc,pThis);
WNDPROCpProc=(WNDPROC)&(pThis->m_thunk.thunk);
WNDPROCpOldProc=(WNDPROC)::SetWindowLong(hWnd,GWL_WNDPROC,(LONG)pProc);
#ifdef_DEBUG
//checkifsomebodyhassubclassedusalreadysincewediscardit
if(pOldProc!=StartWindowProc)
ATLTRACE2(atlTraceWindowing,0,_T("Subclassingthroughahookdiscarded.\n"));
#else
pOldProc;//avoidunusedwarning
#endif
returnpProc(hWnd,uMsg,wParam,lParam);
}

class CWndProcThunk
{
public:
union
{
_AtlCreateWndData cd;
_WndProcThunk thunk;
};
void Init(WNDPROC proc, void* pThis)
{
#if defined (_M_IX86)
thunk.m_mov = 0x042444C7; //C7 44 24 0C
thunk.m_this = (DWORD)pThis;
thunk.m_jmp = 0xe9;
thunk.m_relproc = (int)proc - ((int)this+sizeof(_WndProcThunk));
#elif defined (_M_ALPHA)
thunk.ldah_at = (0x279f0000 | HIWORD(proc)) + (LOWORD(proc)>>15);
thunk.ldah_a0 = (0x261f0000 | HIWORD(pThis)) + (LOWORD(pThis)>>15);
thunk.lda_at = 0x239c0000 | LOWORD(proc);
thunk.lda_a0 = 0x22100000 | LOWORD(pThis);
thunk.jmp = 0x6bfc0000;
#endif
// write block from data cache and
// flush from instruction cache
FlushInstructionCache(GetCurrentProcess(), &thunk, sizeof(thunk));
}
};
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CContainedWindowT< TBase >* pThis = (CContainedWindowT< TBase >*)hWnd;
ATLASSERT(pThis->m_hWnd != NULL);
ATLASSERT(pThis->m_pObject != NULL);
// set a ptr to this message and save the old value
MSG msg = { pThis->m_hWnd, uMsg, wParam, lParam, 0, { 0, 0 } };
const MSG* pOldMsg = pThis->m_pCurrentMsg;
pThis->m_pCurrentMsg = &msg;
// pass to the message map to process
LRESULT lRes;
BOOL bRet = pThis->m_pObject->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, pThis->m_dwMsgMapID);
// restore saved value for the current message
ATLASSERT(pThis->m_pCurrentMsg == &msg);
pThis->m_pCurrentMsg = pOldMsg;
// do the default processing if message was not handled
if(!bRet)
{
if(uMsg != WM_NCDESTROY)
lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
else
{
// unsubclass, if needed
LONG pfnWndProc = ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC);
lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
if(pThis->m_pfnSuperWindowProc != ::DefWindowProc && ::GetWindowLong(pThis->m_hWnd, GWL_WNDPROC) == pfnWndProc)
::SetWindowLong(pThis->m_hWnd, GWL_WNDPROC, (LONG)pThis->m_pfnSuperWindowProc);
// clear out window handle
pThis->m_hWnd = NULL;
}
}
return lRes;
}

(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关联类成员函数处理消息的方法非常高效,只是参数修改和跳转,基本上没有任何性能损失。

打开_ATL_DEBUG_INTERFACES宏来跟踪接口的引用计数

我们知道COM中引用计数的管理一直是个难题,因为你的接口是大家共用的,如果你引用计数管理出错,就会导致一些非常难查的问题,因此ATL中我们可以通过打开_ATL_DEBUG_INTERFACES宏 ,让我们通过Debug信息察看每个接口的引用计数情况。那么ATL是如何做到的呢?

相信用过ATL的人都会看到过这个代码:
struct_QIThunk
{

STDMETHOD(f3)();
STDMETHOD(f4)();
STDMETHOD(f5)();


STDMETHOD(f1022)();
STDMETHOD(f1023)();
STDMETHOD(f1024)();
.
};
里面有1000多个方法,相信很多人到现在还不知道这些东西有什么用,其实我以前一直也没看懂这个东西。

下面我们来分析下ATL跟踪接口引用计数的过程,同样先贴代码:
staticHRESULTWINAPIInternalQueryInterface(void*pThis,
const_ATL_INTMAP_ENTRY*pEntries,REFIIDiid,void**ppvObject)
{
ATLASSERT(pThis!=NULL);
//Firstentryinthecommapshouldbeasimplemapentry
ATLASSERT(pEntries->pFunc==_ATL_SIMPLEMAPENTRY);
#ifdefined(_ATL_DEBUG_INTERFACES)||defined(_ATL_DEBUG_QI)
LPCTSTRpszClassName=(LPCTSTR)pEntries[-1].dw;
#endif//_ATL_DEBUG_INTERFACES
HRESULThRes=AtlInternalQueryInterface(pThis,pEntries,iid,ppvObject);
#ifdef_ATL_DEBUG_INTERFACES
_Module.AddThunk((IUnknown**)ppvObject,pszClassName,iid);
#endif//_ATL_DEBUG_INTERFACES
return_ATLDUMPIID(iid,pszClassName,hRes);
}

HRESULT AddThunk(IUnknown** pp, LPCTSTR lpsz, REFIID iid)
{
if ((pp == NULL) || (*pp == NULL))
return E_POINTER;
IUnknown* p = *pp;
_QIThunk* pThunk = NULL;
EnterCriticalSection(&m_csObjMap);
// Check if exists already for identity
if (InlineIsEqualUnknown(iid))
{
for (int i = 0; i < m_paThunks->GetSize(); i++)
{
if (m_paThunks->operator[](i)->pUnk == p)
{
m_paThunks->operator[](i)->InternalAddRef();
pThunk = m_paThunks->operator[](i);
break;
}
}
}
if (pThunk == NULL)
{
++m_nIndexQI;
if (m_nIndexBreakAt == m_nIndexQI)
DebugBreak();
ATLTRY(pThunk = new _QIThunk(p, lpsz, iid, m_nIndexQI, (m_nIndexBreakAt == m_nIndexQI)));
if (pThunk == NULL)
return E_OUTOFMEMORY;
pThunk->InternalAddRef();
m_paThunks->Add(pThunk);
}
LeaveCriticalSection(&m_csObjMap);
*pp = (IUnknown*)pThunk;
return S_OK;
}


struct _QIThunk
{
STDMETHOD(QueryInterface)(REFIID iid, void** pp)
{
ATLASSERT(m_dwRef >= 0);
return pUnk->QueryInterface(iid, pp);
}
STDMETHOD_(ULONG, AddRef)()
{
if (bBreak)
DebugBreak();
pUnk->AddRef();
return InternalAddRef();
}
ULONG InternalAddRef()
{
if (bBreak)
DebugBreak();
ATLASSERT(m_dwRef >= 0);
long l = InterlockedIncrement(&m_dwRef);
ATLTRACE(_T("%d> "), m_dwRef);
AtlDumpIID(iid, lpszClassName, S_OK);
if (l > m_dwMaxRef)
m_dwMaxRef = l;
return l;
}
STDMETHOD_(ULONG, Release)();
STDMETHOD(f3)();
STDMETHOD(f4)();
....
STDMETHOD(f1023)();
STDMETHOD(f1024)();
_QIThunk(IUnknown* pOrig, LPCTSTR p, const IID& i, UINT n, bool b)
{
lpszClassName = p;
iid = i;
nIndex = n;
m_dwRef = 0;
m_dwMaxRef = 0;
pUnk = pOrig;
bBreak = b;
bNonAddRefThunk = false;
}
IUnknown* pUnk;
long m_dwRef;
long m_dwMaxRef;
LPCTSTR lpszClassName;
IID iid;
UINT nIndex;
bool bBreak;
bool bNonAddRefThunk;
};


#defineIMPL_THUNK(n)\
__declspec(naked)inlineHRESULT_QIThunk::f##n()\
{\
__asmmoveax,[esp+4]\
__asmcmpdwordptr[eax+8],0\
__asmjggoodref\
__asmcallatlBadThunkCall\
__asmgoodref:\
__asmmoveax,[esp+4]\
__asmmoveax,dwordptr[eax+4]\
__asmmov[esp+4],eax\
__asmmoveax,dwordptr[eax]\
__asmmoveax,dwordptr[eax+4*n]\
__asmjmpeax\
}

IMPL_THUNK(3)
IMPL_THUNK(4)
IMPL_THUNK(5)
....

(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),下面我们来分析这段汇编代码.
根据QIThunk的内存布局, 前4个字节是虚表指针,4-8字节是保存的原始接口指针IUnknown* pUnk,8-12字节是引用计数long m_dwRef

#defineIMPL_THUNK(n)\
__declspec(naked)inlineHRESULT_QIThunk::f##n()\
{\
__asmmoveax,[esp+4]\ //将第一参数,即pQIThunk保存到eax
__asmcmpdwordptr[eax+8],0\ //判断QIThunk的引用计数是否为0
__asmjggoodref\ //大于0才是正确的
__asmcallatlBadThunkCall\
__asmgoodref:\
__asmmoveax,[esp+4]\ //将第一参数,即pQIThunk保存到eax
__asmmoveax,dwordptr[eax+4]\ //取出QIThunk的原始接口指针IUnknown* pUnk
__asmmov[esp+4],eax\ //将原始接口指针保存替换刚调用过来的第一参数
__asmmoveax,dwordptr[eax]\ //取出原始接口指针保存的虚表地址,保存到eax
__asmmoveax,dwordptr[eax+4*n]\ //根据索引,取出原始虚表中对应的函数地址
__asmjmpeax\ //跳转到该函数地址
}

可以看到,通过上面的汇编代码,将原来是针对QIThunk的调用又转回到了我们原始的接口中。呵呵, 实际上应该是
ATL拦截了我们原始的接口调用,转到了QIThunk中,而QIThunk最终又通过Thunk机制转回了原始的接口调用。

通过上面一些介绍,希望可以帮助你理解ATL, 我们可以看到Thunk本质上只是通过汇编实现参数的修改和指令的跳转。

以前我看ATL也很吃力,以我个人的经验,一些东西刚开始看不太懂就放一放,先去看一些基本的东西,比如不懂COM,先去学下C++ 中的虚函数;不懂C++模板,先去学下STL;不懂Thunk,先去看一下汇编,等有了一定的积累,回头再看,一切就觉得没这么难了。
分享到:
评论

相关推荐

    ATL-之深入淺出,ATL是ActiveX Template Library 的缩写,它是一套C++模板库。

    ATL作为一个生成C++/COM代码的框架,与C语言生成汇编代码的角色类似。因此,推荐读者在学习ATL前,先阅读Don Box的《Essential COM》以掌握COM的基础知识。 《ATL Internals》不仅提供了ATL的深入技术解析,还包含...

    WTL 8.1 修善版 附汇编高亮文件

    在VS2010中,默认情况下,汇编代码可能没有颜色高亮显示,这可能会影响开发者的阅读和理解。为了改善这种情况,WTL 8.1修善版附带了一个名为usertype.dat的文件,当导入到Visual Studio中时,可以增强对汇编语言的...

    VS2008下追踪windowsapi的方法

    在代码中调用ShellExecute并设置断点,然后在Debug -&gt; Windows -&gt; Disassembly中查看汇编代码。这将揭示函数内部的执行流程,让我们有机会单步执行并理解其工作原理。 总的来说,追踪Windows API涉及到设置Symbol...

    绍VC6.0的18个实用小技巧

    15. **生成汇编代码**:在项目设置中选择特定文件,将List Files Type设为Assembly and source code,指定输出汇编代码的文件,编译后即可得到相应源文件的汇编代码。 16. **手工编译纯资源成DLL**:通过Rc.exe、...

    关于C++调试方面的好书

    这包括了对内联汇编语言代码的调试,以及在Windows平台下使用特定的调试工具来定位、分析和修复Windows代码中的错误。 当开发使用MFC类库的Windows程序时,调试技巧同样适用,但需要结合MFC框架的特点来定位和分析...

    VC6实用技巧分析说明

    15. **生成汇编代码**:在“Project-&gt;Setting”中设置,选择指定文件,然后在“List Files Type”中选择“Assembly and source code”,最后指定输出汇编代码的文件名,编译后即可得到源代码的汇编版本。 16. **...

    一个简单的编译器

    中间代码的生成使得编译器能够处理不同的目标平台,因为它们不需要直接生成特定机器的汇编代码。 在“co_test”文件中,可能是编译器的测试用例或者测试代码,用于验证编译器各阶段的功能正确性。通过运行这些测试...

    一个老程序员体会如何学好软件编程

    1. **汇编语言**:学习汇编,可以通过分析CIH病毒的源代码,虽然只有1000行,但足以了解其运作机制。 2. **C语言**:要掌握C,研究Linux内核的IP栈源代码是很好的实践,这涉及几万行核心代码。 3. **C++**:对于MFC...

    VC++实用技巧

    14. **生成汇编代码**:在Project-&gt;Setting中选择指定文件,设置List Files Type为Assembly and source code,编译后会生成对应的汇编代码文件。 15. **手工编译纯资源成DLL**:通过Rc.exe、Cvtres.exe和Link命令,...

    VC++ Studio使用技巧.一些常用而且很实用的使用技巧。

    6. **预处理指令定位**:使用`Ctrl+K`可以在源文件中快速定位到匹配的`#if`和`#endif`,有助于理解代码结构。 7. **添加系统库到项目**:在`Project` -&gt; `Settings` -&gt; `Link` -&gt; `Object/library modules`中输入...

    微机原理课程设计---交通控制灯

    在微机原理课程设计中,交通控制灯的模拟通常是一个典型的实践项目,它涉及到计算机硬件与软件的交互,以及对微处理器操作的理解。在这个设计中,电子秒表的实现是核心部分,它利用了汇编语言来编程,这是一种低级...

    McGraw C++程序调试实用手册

    6.2.5 查看反汇编代码 6.3 进一步观察变量 6.3.l 使用QuickWatch窗口 6.3.2 使用Watch窗口 6.4 小结 第7章 调试内联汇编语言代码 7.l 汇编语言初步 7.1.l 数据类型 7.1.2 寄存器 7.1.3 寻址模式 ...

    VC++6.0 程序包

    5. **编译与链接**:理解编译器如何将源代码转换为可执行文件,包括预处理、编译、汇编和链接的步骤。 6. **调试技巧**:学会使用IDE的调试器进行单步调试,查看变量值,设置条件断点等。 7. **MFC与Windows API**...

    visual c++6.0帮助工具

    在VC++6.0中,源代码经过预处理、编译、汇编和链接四个步骤,生成可执行文件。理解这些过程有助于优化代码性能和解决编译错误。 7. **项目管理和配置** 用户可以通过项目管理器组织源代码文件,并进行编译设置,...

    wince目录解析

    - **处理器编译器**:针对平台特定的交叉编译器和交叉汇编器,允许开发者在不同架构间编译代码。 - **开发工具**:ErrorLookup、GuidGen、Link和Nmake等,涵盖了错误查询、GUID生成、链接及构建管理等功能。 - **...

    VC6++ 精简版

    此外,它的调试器也是一个强大的工具,可以用来查找和修复程序中的错误,这对于学习和理解程序执行流程至关重要。 精简版可能去掉了某些高级特性,如MFC(Microsoft Foundation Classes)库,这是一个用于构建...

    可视化编程课件02.ppt

    执行(Execute)运行程序,调试(Debug)则通过设置断点、单步执行、监视变量、查看汇编代码和调用堆栈等功能,帮助找出并修复错误。调试工具包括Start Debug、Go、Step Into、Step Over、Step Out和Break Point等...

Global site tag (gtag.js) - Google Analytics