轉(zhuǎn)載請注明出處:http://www.shnenglu.com/proguru/archive/2008/08/24/59831.html
thunk是什么?查字典只能讓人一頭霧水。thunk是一段插入程序中實(shí)現(xiàn)特定功能的二進(jìn)制代碼,這個定義是我下的,對不對各位看官請自己斟酌,呵呵。
我這里要講的是窗口回調(diào)專用thunk,thunk的核心是調(diào)用棧動態(tài)修改技術(shù)。地球人都知道,windows的窗口回調(diào)函數(shù)是一個全局函數(shù),類成員函數(shù)是不可以作為窗口回調(diào)函數(shù)的,因?yàn)樗衪his指針,這給我們用C++來包裝窗口帶來不小的麻煩。你說什么?用一個全局函數(shù)或類的靜態(tài)成員函數(shù)來做窗口回調(diào)函數(shù)?這肯定沒問題。但是這樣帶來的麻煩也許比你想象的要多,想想我們的GUI Framework不會只有一個類,而是一個類層級結(jié)構(gòu),會有許許多許多、形形色色的widget,每個都是一個窗口。對象與窗口之間的映射可能就是個不小的問題,像MFC那樣搞?太落伍了吧!用thunk就要簡單的多。WTL用了thunk,但是不夠徹底。
廢話少說,先貼出thunk核心代碼。
1 /*
2 * thunk with DEP support
3 *
4 * author:proguru
5 * July 9,2008
6 */
7 #ifndef __KTHUNK_H__
8 #define __KTHUNK_H__
9 #include "windows.h"
10
11 //#define USE_THISCALL_CONVENTION //turn it off for c++ builder compatibility
12
13 #ifdef USE_THISCALL_CONVENTION
14 #define WNDPROC_THUNK_LENGTH 29 //For __thiscall calling convention ONLY,assign m_hWnd by thunk
15 #define GENERAL_THUNK_LENGTH 10
16 #define KCALLBACK //__thiscall is default
17 #else
18 #define WNDPROC_THUNK_LENGTH 22 //__stdcall calling convention ONLY,assign m_hWnd by thunk
19 #define GENERAL_THUNK_LENGTH 16
20 #define KCALLBACK __stdcall
21 #endif
22
23 extern HANDLE g_hHeapExecutable;
24
25 class KThunkBase{
26 public:
27 KThunkBase(SIZE_T size){
28 if(!g_hHeapExecutable){ //first thunk,create the executable heap
29 g_hHeapExecutable=::HeapCreate(HEAP_CREATE_ENABLE_EXECUTE,0,0);
30 //if (!g_hHeapExecutable) abort
31 }
32 m_szMachineCode=(unsigned char*)::HeapAlloc(g_hHeapExecutable,HEAP_ZERO_MEMORY,size);
33 }
34 ~KThunkBase(){
35 if(g_hHeapExecutable)
36 ::HeapFree(g_hHeapExecutable,0,(void*)m_szMachineCode);
37 }
38 inline void* GetThunkedCodePtr(){return &m_szMachineCode[0];}
39 protected:
40 unsigned char* m_szMachineCode;
41 };
42
43 class KWndProcThunk : public KThunkBase{
44 public:
45 KWndProcThunk():KThunkBase(WNDPROC_THUNK_LENGTH){}
46 void Init(INT_PTR pThis, INT_PTR ProcPtr){
47 #ifndef _WIN64
48 #pragma warning(disable: 4311)
49 DWORD dwDistance =(DWORD)(ProcPtr) - (DWORD)(&m_szMachineCode[0]) - WNDPROC_THUNK_LENGTH;
50 #pragma warning(default: 4311)
51
52 #ifdef USE_THISCALL_CONVENTION
53 /*
54 For __thiscall, the default calling convention used by Microsoft VC++, The thing needed is
55 just set ECX with the value of 'this pointer'
56
57 machine code assembly instruction comment
58 --------------------------- ------------------------- ----------
59 B9 ?? ?? ?? ?? mov ecx, pThis ;Load ecx with this pointer
60 50 PUSH EAX
61 8B 44 24 08 MOV EAX, DWORD PTR[ESP+8] ;EAX=hWnd
62 89 01 MOV DWORD PTR [ECX], EAX ;[pThis]=[ECX]=hWnd
63 8B 44 24 04 mov eax,DWORD PTR [ESP+04H] ;eax=(return address)
64 89 44 24 08 mov DWORD PTR [ESP+08h],eax ;hWnd=(return address)
65 58 POP EAX
66 83 C4 04 add ESP,04h
67
68 E9 ?? ?? ?? ?? jmp ProcPtr ;Jump to target message handler
69 */
70 m_szMachineCode[0] = 0xB9;
71 *((DWORD*)&m_szMachineCode[1] ) =(DWORD)pThis;
72 *((DWORD*)&m_szMachineCode[5] ) =0x24448B50;
73 *((DWORD*)&m_szMachineCode[9] ) =0x8B018908;
74 *((DWORD*)&m_szMachineCode[13]) =0x89042444;
75 *((DWORD*)&m_szMachineCode[17]) =0x58082444;
76 *((DWORD*)&m_szMachineCode[21]) =0xE904C483;
77 *((DWORD*)&m_szMachineCode[25]) =dwDistance;
78 #else
79 /*
80 * 01/26/2008 modify
81 For __stdcall calling convention, replace 'HWND' with 'this pointer'
82
83 Stack frame before modify Stack frame after modify
84
85 :
: :
:
86 |---------------| |----------------|
87 | lParam | | lParam |
88 |---------------| |----------------|
89 | wParam | | wParam |
90 |---------------| |----------------|
91 | uMsg | | uMsg |
92 |---------------| |----------------|
93 | hWnd | | <this pointer> |
94 |---------------| |----------------|
95 | (Return Addr) | <- ESP | (Return Addr) | <-ESP
96 |---------------| |----------------|
97 :
: :
|
98
99 machine code assembly instruction comment
100 ------------------- ---------------------------- --------------
101 51 push ecx
102 B8 ?? ?? ?? ?? mov eax,pThis ;eax=this;
103 8B 4C 24 08 mov ecx,dword ptr [esp+08H] ;ecx=hWnd;
104 89 08 mov dword ptr [eax],ecx ;[this]=hWnd,if has vftbl shound [this+4]=hWnd
105 89 44 24 08 mov dword ptr [esp+08H], eax ;Overwite the 'hWnd' with 'this pointer'
106 59 pop ecx
107 E9 ?? ?? ?? ?? jmp ProcPtr ; Jump to target message handler
108 */
109
110 *((WORD *) &m_szMachineCode[ 0]) = 0xB851;
111 *((DWORD *) &m_szMachineCode[ 2]) = (DWORD)pThis;
112 *((DWORD *) &m_szMachineCode[ 6]) = 0x08244C8B;
113 *((DWORD *) &m_szMachineCode[10]) = 0x44890889;
114 *((DWORD *) &m_szMachineCode[14]) = 0xE9590824;
115 *((DWORD *) &m_szMachineCode[18]) = (DWORD)dwDistance;
116 #endif //USE_THISCALL_CONVENTION
117 #else //_WIN64
118 /*
119 For x64 calling convention, RCX hold the 'HWND',copy the 'HWND' to Window object,
120 then insert 'this pointer' into RCX,so perfectly!!!
121
122 Stack frame before modify Stack frame after modify
123
124 :
: :
:
125 |---------------| |----------------|
126 | | <-R9(lParam) | | <-R9(lParam)
127 |---------------| |----------------|
128 | | <-R8(wParam) | | <-R8(wParam)
129 |---------------| |----------------|
130 | | <-RDX(msg) | | <-RDX(msg)
131 |---------------| |----------------|
132 | | <-RCX(hWnd) | | <-RCX(this)
133 |---------------| |----------------|
134 | (Return Addr) | <-RSP | (Return Addr) | <-RSP
135 |---------------| |----------------|
136 :
: :
:
137
138 machine code assembly instruction comment
139 ------------------- ----------------------- ----
140 48B8 ???????????????? mov RAX,pThis
141 4808 mov qword ptr [RAX],RCX ;m_hWnd=[this]=RCX
142 4889C1 mov RCX,RAX ;RCX=pThis
143 48B8 ???????????????? mov RAX,ProcPtr
144 FFE0 jmp RAX
145 */
146 *((WORD *)&m_szMachineCode[0] ) =0xB848;
147 *((INT_PTR*)&m_szMachineCode[2] ) =pThis;
148 *((DWORD *)&m_szMachineCode[10]) =0x89480848;
149 *((DWORD *)&m_szMachineCode[14]) =0x00B848C1;
150 *((INT_PTR*)&m_szMachineCode[17]) =ProcPtr;
151 *((WORD *)&m_szMachineCode[25]) =0xE0FF;
152 #endif
153 }
154 };
是不是有些頭暈?且待我慢慢分解。
類成員函數(shù)有兩種調(diào)用約定,MS VC++默認(rèn)采用thiscall調(diào)用約定,而Borland C++默認(rèn)采用stdcall調(diào)用約定。thiscall采用ECX寄存器來傳遞this指針,而stdcall則通過棧來傳遞this指針,this指針是成員函數(shù)隱藏的第一個參數(shù)。而到了x64平臺,則問題有了新的變化。為了充分利用寄存器,提高效率,函數(shù)的前四個參數(shù)默認(rèn)用寄存器傳遞,分別是RCX,RDX,R8和R9。對于成員函數(shù),其this指針通過RCX傳遞。x64 thunk代碼我并未測試過,因?yàn)橐恢蔽词褂脁64平臺,不過應(yīng)該不會有太大問題。
在這里,我只分析x86平臺上使用stdcall調(diào)用習(xí)慣的thunk代碼。因?yàn)檫@段代碼將窗口回調(diào)函數(shù)調(diào)用棧上的HWND直接修改this指針,所以有兩個問題需要提前了解一下。
第一、我將回調(diào)函數(shù)的signature修改為如下形式:
LRESULT KCALLBACK KWndProc(UINT uMsg, WPARAM wParam, LPARAM lParam) ;
請注意這是個成員函數(shù),而且沒有HWND hWnd這個參數(shù)。
第二、窗口類的第一個數(shù)據(jù)成員必須是窗口句柄變量,我的是HWND m_hWnd.至于為什么要這樣,后面會有提及。
現(xiàn)在請看代碼第85行開始的圖形,前一個是修改前windows調(diào)用我們提供的回調(diào)函數(shù)的棧結(jié)構(gòu),后一個則是為了適應(yīng)我們的需求修改過后的調(diào)用棧。首先,我們的回調(diào)函數(shù)需要一個this指針,而且要放到棧上第一個參數(shù)的位置上,這是通過第46行的thunk初始化函數(shù)Init傳
遞進(jìn)來的。其次我們的窗口對象必須要得到自己所對應(yīng)的窗口句柄,不然一切都是空談。
那么我們可以用thunk來修改調(diào)用棧。首先用初始棧上的第一個參數(shù),也就是實(shí)際的窗口句柄,傳遞給窗口對象。如何傳遞呢?因?yàn)閙_hWnd成員是對象的第一個數(shù)據(jù)成員,那么很簡單,如果沒有虛函數(shù)的存在,那么這個m_hWnd就靜靜地待在對象的最開始處,就是this指針?biāo)赶虻奈恢谩H绻刑摵瘮?shù)的存在,那么事情也不是太復(fù)雜,對象的起始處現(xiàn)在是VPTR,m_hWnd緊隨其后,代碼略作調(diào)整即可。其次用this指針覆蓋棧上的第一個參數(shù),也就是窗口句柄HWND。下面是逐條注釋的匯編格式指令:
1 push ecx ;保護(hù)ecx,后面會用到
2 mov eax,pThis ;傳送this指針到eax. eax=this;
3 mov ecx,dword ptr [esp+08H] ;把調(diào)用棧上的第一個參數(shù)送ecx. ecx=hWnd
4 mov dword ptr [eax],ecx ;把窗口句柄賦予窗口對象數(shù)據(jù)成員m_hWnd.
5 ;[this]=hWnd,if has vftbl shound [this+4]=hWnd
6 mov dword ptr [esp+08H], eax ;用this指針覆蓋調(diào)用棧上的第一個參數(shù)即窗口句柄
7 ;Overwite the 'hWnd' with 'this pointer'
8 pop ecx ;彈出先前ecx
9 jmp ProcPtr ;跳轉(zhuǎn)到消息處理函數(shù).Jump to target message handler
這樣就把窗口(句柄)和窗口對象完美的綁定到一起,不需要一個對應(yīng)查找表,不使用任何全局或靜態(tài)的數(shù)據(jù),滿足thread safe。
至于匯編格式指令翻譯到機(jī)器碼的問題,下載intel的指令手冊,查查表就可以了。
下面的代碼展示了thunk的使用(刪除了不相干的代碼):
1 template <typename T,typename TBase=KWindow>
2 class KWindowRoot : public TBase{
3 public:
4 KWindowRoot():TBase(){
5 T* pT=static_cast<T*>(this);
6 m_thunk.Init((INT_PTR)pT, pT->GetMessageProcPtr());
7 }
8
9 INT_PTR GetMessageProcPtr(){
10 typedef LRESULT (KCALLBACK T::*KWndProc_t)(UINT,WPARAM,LPARAM);
11 union{
12 KWndProc_t wndproc;
13 INT_PTR dwProcAddr;
14 }u;
15 u.wndproc=&T::KWndProc;
16 return u.dwProcAddr;
17 }
18
19 LRESULT KCALLBACK KWndProc(UINT uMsg, WPARAM wParam, LPARAM lParam){
20 T* pT=static_cast<T*>(this);
21 return pT->ProcessWindowMessage(uMsg,wParam,lParam);
22 }
23
24
25 protected:
26 KWndProcThunk m_thunk;
27 inline INT_PTR GetThunkedProcPtr(){return (INT_PTR)m_thunk.GetThunkedCodePtr();}
28 };
在基類KWindow中HWND m_hWnd是其第一個數(shù)據(jù)成員。因?yàn)槭褂昧四0宓撵o態(tài)多態(tài)特性,故對象沒有VPTR指針。
到了這里事情還沒有結(jié)束。既然使用thunk就不得不面對DEP。DEP會阻止沒有執(zhí)行權(quán)限的內(nèi)存執(zhí)行代碼。如果我們的thunk分配在棧上或new出來的堆上,則會被DEP阻止,程序執(zhí)行失敗。因此可以申請一個具有執(zhí)行權(quán)限的堆來解決這個問題:
1 KThunkBase(SIZE_T size){
2 if(!g_hHeapExecutable){ //first thunk,create the executable heap
3 g_hHeapExecutable=::HeapCreate(HEAP_CREATE_ENABLE_EXECUTE,0,0);
4 //if (!g_hHeapExecutable) abort
5 }
6 m_szMachineCode=(unsigned char*)::HeapAlloc(g_hHeapExecutable,HEAP_ZERO_MEMORY,size);
7 }
總的來講thunk的空間和時(shí)間開銷都是足夠小的,甚至可以忽略不計(jì)。但是卻帶來了極大的便利。
thunk只是開了一個頭。