ATL Thunk機制深刻分析

若是你有SDK的編程經驗,就必定應該知道在建立窗口時須要指定窗口類,窗口類中的一種重要的參數就是窗口過程。任何窗口接收到的消息,都是由該窗口過程來處理。html

在面向對象編程中,若是還須要開發人員來使用原始的窗口過程這種面向過程的開發方式,面向對象就顯得不那麼純粹了。因此,在界面編程的框架中,框架每每會隱藏窗口過程,開發人員看到的都是一個個的類。編程

若是要處理某一個消息,則須要在窗口對應的類中加入響應的message map便可。windows

那麼,框架是如何將窗口過程跟窗口對應的類關聯起來呢? ATL中用的是一個叫thunk的機制。因爲咱們收回來的dump有大量的窗口過程出問題的case,最後發現跟thunk有必定的關係,因此我對ATL的thunk作了 一番研究。app

Thunk的基本原理是分配一段內存,而後將窗口過程設置爲這段內存。這段內存的做用是將窗口過程的第一個參數(窗口句柄)替換成類的This指針,並jump到類的WinProc函數中。這樣就完成了窗口過程到類的成員函數的一個轉換。框架

 

這裏面有幾個點須要重點研究一下:ide

  1. 何時分配thunk這段內存,又在何時將窗口過程設置爲thunk的這段內存。
  2. 內存是怎麼分配的,是一段堆上的內存嗎?
  3. 這段內存究竟是什麼東西?

 

咱們先來看看第一個問題:函數

何時分配thunk這段內存,又在何時將窗口過程設置爲thunk的這段內存。this

ATL在建立窗口時,使用的窗口類是經過一段宏來定義的:DECLARE_WND_CLASS(_T("My Window Class"))atom

這段宏的定義以下:spa

複製代碼
#define DECLARE_WND_CLASS(WndClassName) \
static ATL::CWndClassInfo& GetWndClassInfo() \
{ \
static ATL::CWndClassInfo wc = \
{ \
{ sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, StartWindowProc, \
0, 0, NULL, NULL, NULL, (HBRUSH)(COLOR_WINDOW + 1), NULL, WndClassName, NULL }, \
NULL, NULL, IDC_ARROW, TRUE, 0, _T("") \
}; \
return wc; \
}
複製代碼

 

 

能夠看到,這個宏其實是在定義一個靜態函數,他的功能是返回一個ATL::CWndClassInfo對象,這個對象實際上就是ATL對窗口類的封裝,其中窗口過程被指定爲StartWindowProc。

當用戶調用CWindowImpl::Create來建立窗口時,會調用到CWindowImpl的父類CWindowImplBaseT的create函數:

這個函數以下:

複製代碼
template <class TBase, class TWinTraits>
HWND CWindowImplBaseT< TBase, TWinTraits >::Create(HWND hWndParent, _U_RECT rect, LPCTSTR szWindowName,
DWORD dwStyle, DWORD dwExStyle, _U_MENUorID MenuOrID, ATOM atom, LPVOID lpCreateParam)
{
。。。。

// Allocate the thunk structure here, where we can fail gracefully.
result = m_thunk.Init(NULL,NULL);
      .......
HWND hWnd = ::CreateWindowEx(dwExStyle, MAKEINTATOM(atom), szWindowName,
dwStyle, rect.m_lpRect->left, rect.m_lpRect->top, rect.m_lpRect->right - rect.m_lpRect->left,
rect.m_lpRect->bottom - rect.m_lpRect->top, hWndParent, MenuOrID.m_hMenu,
_AtlBaseModule.GetModuleInstance(), lpCreateParam);
.............
return hWnd;
}
複製代碼


 

能夠看到,咱們首先對thunk用NULL進行了初始化,正如註釋所說的,這這裏初始化是由於若是分配內存失敗了,能夠更好的進行錯誤處理。實際上thunk也徹底能夠在後面處理窗口的第一個消息時進行初始化。

接着調用windows API CreateWindowEx來建立窗口,前面咱們知道,這個窗口的使用的窗口類的窗口過程是StartWindowProc,咱們接着看它的處理代碼。

 

複製代碼
template <class TBase, class TWinTraits>
LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CWindowImplBaseT< TBase, TWinTraits >* pThis = (CWindowImplBaseT< TBase, TWinTraits >*)_AtlWinModule.ExtractCreateWndData();
pThis->m_hWnd = hWnd;

// Initialize the thunk. This is allocated in CWindowImplBaseT::Create,
// so failure is unexpected here.
pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);
WNDPROC pProc = pThis->m_thunk.GetWNDPROC();
WNDPROC pOldProc = (WNDPROC)::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)pProc);
return pProc(hWnd, uMsg, wParam, lParam);
}
複製代碼

 

這是類的一個靜態函數,因此能夠做爲窗口過程直接使用。咱們能夠看到,他的參數正是窗口過程的四個參數。

函數首先拿到This指針,將傳遞進來的窗口句柄賦給this的成員變量m_hWnd。而後m_thunk.Init來從新初始化thunk,這個時候傳遞進去的再也不是兩個NULL,而是類的一個成員函數和This指針。

這個初始化具體作什麼咱們後面再具體分析。

初始化完成以後,咱們就能夠拿到這個thunk的地址(m_thunk.GetWNDPROC就是獲取thunk的地址),而後調用SetWindowLongPtr將窗口過程設置成thunk的地址。下次窗口有消息來時就直接跑到thunk裏面去了

最後直接調用thunk的地址,是爲了將StartWindowProc正在處理的這個消息也傳遞給thunk處理,以避免丟失了窗口的第一個消息。

 

這段內存究竟是什麼東西?

咱們下面來重點分析m_thunk.Init是完成什麼功能。

CDynamicStdCallThunk.Init的代碼以下:

複製代碼
         BOOL Init(DWORD_PTR proc, void *pThis)
{
if (pThunk == NULL)
{
pThunk = new _stdcallthunk;
if (pThunk == NULL)
{
return FALSE;
}
}
return pThunk->Init(proc, pThis);
}
複製代碼

 

代碼很簡單,分配一段結構(_stdcallthunk),而後繼續調用這個結構的init函數。(提早說一下的是,這裏override了new這個operator,真正作的事情不是簡單的從堆上分配內存,後面會詳細介紹)

結構

複製代碼
struct _stdcallthunk
{
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
BOOL Init(DWORD_PTR proc, void* pThis)
{
m_mov = 0x042444C7; //C7 44 24 0C
m_this = PtrToUlong(pThis);
m_jmp = 0xe9;
m_relproc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(_stdcallthunk)));
// write block from data cache and
// flush from instruction cache
FlushInstructionCache(GetCurrentProcess(), this, sizeof(_stdcallthunk));
return TRUE;
}
。。。。
複製代碼

 

Thunk有四個成員,第一個成員是m_mov,被賦值爲0x042444C7,第二個成員是m_this,被賦值爲窗口對應類的地址。

這兩個DWORD實際上組成了一條彙編語句:

mov dword ptr [esp+0x4], pThis

經過前面咱們知道,窗口過程已經被設置成這段內存的起始地址,也就是說窗口過程的第一行代碼就是這行代碼。

Esp是指向棧頂的指針,esp+0x4則是窗口過程的第一個參數hWnd,這段代碼的意思就是說將this指針覆蓋掉窗口過程的第一個參數hWnd。

咱們知道,類成員函數的第一個參數都是this指針,有了this指針,類成員函數就能夠調用了。

下面的事情就是準備jump到成員函數中:m_jmp賦值爲0xe9,一個相對跳轉指令,m_relproc被賦值爲相對成員函數相對thunk的地址,這兩個成員變量也組成了一條彙編語句:

jmp WndProc

回到前面看看傳遞進來的成員函數的原型:

pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);

 

GetWindowProc實際上就是返回成員函數WindowProc,原型以下:

template <class TBase, class TWinTraits>

LRESULT CALLBACK CWindowImplBaseT< TBase, TWinTraits >::WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

能夠看到,成員函數只有三個參數,窗口過程的第一個參數被修改爲了this指針,因此達到了巧妙將窗口過程修改爲類的成員函數的目的。

Thunk的基本原理到這裏已經結束了,還剩下一個問題就是前面提到的對new的override,這就牽涉到關於ATL thunk的最後一個問題:

 

內存是怎麼分配的,是一段堆上的內存嗎?

爲何要對new進行override?由於windows xp sp2以後,爲了對付層出不窮的緩衝區溢出攻擊,windows推出了一個新的feature叫Data execution prevention。若是這個feature被啓用,那麼堆上和棧上的數據是不能夠執行的,若是thunk是位於new出來的代碼,那麼一執行就會crash。

爲了解決這個問題,ATL override了new和delete運算符。

Override後的new最終會調用到函數__AllocStdCallThunk_cmn:

 

複製代碼
PVOID __AllocStdCallThunk_cmn ( VOID )
{
PATL_THUNK_ENTRY lastThunkEntry;
PATL_THUNK_ENTRY thunkEntry;
PVOID thunkPage;

if (__AtlThunkPool == NULL) {
if (__InitializeThunkPool() == FALSE) {
}
}

if (ATLTHUNK_USE_HEAP()) {
// On a non-NX capable platform, use the standard heap.
thunkEntry = (PATL_THUNK_ENTRY)HeapAlloc(GetProcessHeap(), 0, sizeof(ATL::_stdcallthunk));
return thunkEntry;
}
thunkPage = (PATL_THUNK_ENTRY)VirtualAlloc(NULL, PAGE_SIZE, MEM_COMMIT,PAGE_EXECUTE_READWRITE);

// Create an array of thunk structures on the page and insert all but
// the last into the free thunk list.

// The last is kept out of the list and represents the thunk allocation.
thunkEntry = (PATL_THUNK_ENTRY)thunkPage;
lastThunkEntry = thunkEntry + ATL_THUNKS_PER_PAGE - 1;
do {
__AtlInterlockedPushEntrySList(__AtlThunkPool,&thunkEntry->SListEntry);
thunkEntry += 1;
} while (thunkEntry < lastThunkEntry);

return thunkEntry;
}
複製代碼

 

函數首先判斷是否是第一次被調用,若是第一次被調用,則調用__InitializeThunkPool來進行初始化(後面會詳細介紹他)。初始化主要是用來判斷Data execution prevention功能是否啓用了。

若是沒有啓用,則簡單多了,直接調用HeapAlloc來分配內存。

若是啓用了則複雜多了。ATL會調用VirtualAlloc來分配一段PAGE_EXECUTE_READWRITE屬性的內存,這段內存是能夠被執行的,爲了節省內存,將這段內存分紅不少塊,每一塊大小就是一個thunk的大小。

而後將這些塊壓入到一個list當中,須要的時候則從中取出,釋放的時候又將塊壓入到list中。

 

因爲即便只建立一個窗口也須要分配一個頁面的大小,若是這個進程中有多個dll,每一個dll都建立一個ATL的窗口,那麼就會佔用到不少頁面空間,浪費內存。爲了節省內存的使用,windows在進程的一個重要結構PEB偏移0x34的地方加入了一個域:

0:007> dt ntdll!_PEB

   +0x000 InheritedAddressSpace : UChar

   +0x001 ReadImageFileExecOptions : UChar

   。。。。。

   +0x030 SystemReserved   : [1] Uint4B

   +0x034 AtlThunkSListPtr32 : Uint4B

   +0x038 ApiSetMap        : Ptr32 Void

。。。。。。

在前面的初始化函數__InitializeThunkPool中,會嘗試從這個位置獲取Thunk的list的head,若是發現是空的,纔會調用VirtualAlloc來建立新的頁面:

 

複製代碼
BOOL static DECLSPEC_NOINLINE __InitializeThunkPool ( VOID )
{
#define PEB_POINTER_OFFSET 0x34

PSLIST_HEADER *atlThunkPoolPtr;
PSLIST_HEADER atlThunkPool;

result = IsProcessorFeaturePresent( 12 /*PF_NX_ENABLED*/ );
if (result == FALSE) {
// NX execution is not happening on this machine.
// Indicate that the regular heap should be used by setting
// __AtlThunkPool to a special value.
__AtlThunkPool = ATLTHUNK_USE_HEAP_VALUE;
return TRUE;
}

atlThunkPoolPtr = (PSLIST_HEADER *)((PCHAR)(Atl_NtCurrentTeb()->ProcessEnvironmentBlock) + PEB_POINTER_OFFSET);
atlThunkPool = *atlThunkPoolPtr;
__AtlThunkPool = atlThunkPool;
return TRUE;
}
複製代碼

 

https://www.cnblogs.com/georgepei/archive/2012/03/30/2425472.html

相關文章
相關標籤/搜索