我们知道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技术来实现。
我们将主要代码贴出来,然后介绍它的创建过程:
(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的人都会看到过这个代码:
里面有1000多个方法,相信很多人到现在还不知道这些东西有什么用,其实我以前一直也没看懂这个东西。
下面我们来分析下ATL跟踪接口引用计数的过程,同样先贴代码:
(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的调用又转回到了我们原始的接口中。呵呵, 实际上应该是 ATL拦截了我们原始的接口调用,转到了QIThunk中,而QIThunk最终又通过Thunk机制转回了原始的接口调用。
通过上面一些介绍,希望可以帮助你理解ATL, 我们 可以看到Thunk本质上只是通过汇编实现参数的修改和指令的跳转。
以前我看ATL也很吃力,以我个人的经验,一些东西刚开始看不太懂就放一放,先去看一些基本的东西,比如不懂COM,先去学下C++ 中的虚函数;不懂C++模板,先去学下STL;不懂Thunk,先去看一下汇编,等有了一定的积累,回头再看,一切就觉得没这么难了。
下面我们主要分析一下ATL中的一些汇编代码。
ATL中出现汇编代码主要是2处,一处是通过Thunk技术来调用类成员函数处理消息;还有一处是通过打开_ATL_DEBUG_INTERFACES宏来跟踪接口的引用计数。
通过Thunk技术来调用类成员函数
我们知道Windows窗口的消息处理函数要求是面向过程的C函数,所以我们C++普通成员函数就不能作为窗口的消息处理函数,所以这里的问题就是如何让我们的C++成员函数和Windows的窗口的消息处理函数关联起来?MFC是通过一个Map来实现的,而ATL选择了更为高效的Thunk技术来实现。
我们将主要代码贴出来,然后介绍它的创建过程:
template <
class
TBase,
class
TWinTraits>
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;
}
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;
}
static
LRESULT CALLBACK StartWindowProc(HWND hWnd, UINT uMsg,
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);
}
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);
}
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)();
.
};
{
STDMETHOD(f3)();
STDMETHOD(f4)();
STDMETHOD(f5)();
STDMETHOD(f1022)();
STDMETHOD(f1023)();
STDMETHOD(f1024)();
.
};
下面我们来分析下ATL跟踪接口引用计数的过程,同样先贴代码:
static
HRESULT WINAPI InternalQueryInterface(
void
*
pThis,
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);
}
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);
}
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;
};
#define
IMPL_THUNK(n)\
__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\
}
__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\
}
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
#define
IMPL_THUNK(n)\
__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\ //跳转到该函数地址
}
__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, 我们 可以看到Thunk本质上只是通过汇编实现参数的修改和指令的跳转。
以前我看ATL也很吃力,以我个人的经验,一些东西刚开始看不太懂就放一放,先去看一些基本的东西,比如不懂COM,先去学下C++ 中的虚函数;不懂C++模板,先去学下STL;不懂Thunk,先去看一下汇编,等有了一定的积累,回头再看,一切就觉得没这么难了。