理解ATL中的一些彙編程式碼
阿新 • • 發佈:2018-12-27
我們知道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 <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;
}
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);
}
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 0Cthunk.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 cacheFlushInstructionCache(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 valueMSG 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 processLRESULT lRes;BOOL bRet = pThis->m_pObject->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, pThis->m_dwMsgMapID);// restore saved value for the current messageATLASSERT(pThis->m_pCurrentMsg == &msg);pThis->m_pCurrentMsg = pOldMsg;// do the default processing if message was not handledif(!bRet){if(uMsg != WM_NCDESTROY)lRes = pThis->DefWindowProc(uMsg, wParam, lParam);else{// unsubclass, if neededLONG 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 handlepThis->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跟蹤介面引用計數的過程,同樣先貼程式碼:
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);
}
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 identityif (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\
}
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\ //跳轉到該函式地址
}
可以看到,通過上面的彙編程式碼,將原來是針對QIThunk的呼叫又轉回到了我們原始的介面中。呵呵, 實際上應該是ATL攔截了我們原始的介面呼叫,轉到了QIThunk中,而QIThunk最終又通過Thunk機制轉回了原始的介面呼叫。
通過上面一些介紹,希望可以幫助你理解ATL, 我們可以看到Thunk本質上只是通過彙編實現引數的修改和指令的跳轉。
以前我看ATL也很吃力,以我個人的經驗,一些東西剛開始看不太懂就放一放,先去看一些基本的東西,比如不懂COM,先去學下C++ 中的虛擬函式;不懂C++模板,先去學下STL;不懂Thunk,先去看一下彙編,等有了一定的積累,回頭再看,一切就覺得沒這麼難了。 posted on 2012-10-23 00:23 Richard Wei 閱讀(2626) 評論(0) 編輯 收藏 引用 所屬分類: 彙編
下面我們主要分析一下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;
}
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);
}
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 0Cthunk.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 cacheFlushInstructionCache(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 valueMSG 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 processLRESULT lRes;BOOL bRet = pThis->m_pObject->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, pThis->m_dwMsgMapID);// restore saved value for the current messageATLASSERT(pThis->m_pCurrentMsg == &msg);pThis->m_pCurrentMsg = pOldMsg;// do the default processing if message was not handledif(!bRet){if(uMsg != WM_NCDESTROY)lRes = pThis->DefWindowProc(uMsg, wParam, lParam);else{// unsubclass, if neededLONG 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 handlepThis->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跟蹤介面引用計數的過程,同樣先貼程式碼:
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);
}
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 identityif (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\
}
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\ //跳轉到該函式地址
}
可以看到,通過上面的彙編程式碼,將原來是針對QIThunk的呼叫又轉回到了我們原始的介面中。呵呵, 實際上應該是ATL攔截了我們原始的介面呼叫,轉到了QIThunk中,而QIThunk最終又通過Thunk機制轉回了原始的介面呼叫。
通過上面一些介紹,希望可以幫助你理解ATL, 我們可以看到Thunk本質上只是通過彙編實現引數的修改和指令的跳轉。
以前我看ATL也很吃力,以我個人的經驗,一些東西剛開始看不太懂就放一放,先去看一些基本的東西,比如不懂COM,先去學下C++ 中的虛擬函式;不懂C++模板,先去學下STL;不懂Thunk,先去看一下彙編,等有了一定的積累,回頭再看,一切就覺得沒這麼難了。 posted on 2012-10-23 00:23 Richard Wei 閱讀(2626) 評論(0) 編輯 收藏 引用 所屬分類: 彙編