Thunk技術(shù),一般認(rèn)為是在程序中直接構(gòu)造出可執(zhí)行代碼的技術(shù)(在正常情況下,這是編譯器的任務(wù))。《深度探索C++對(duì)象模型》中對(duì)這個(gè)詞的來(lái)源有過(guò)考證(在中文版的162頁(yè)),說(shuō)thunk是knuth的倒拼字。knuth就是大名鼎鼎的計(jì)算機(jī)經(jīng)典名著
《The Art of Computer Programming》的作者,該書(shū)被程序員們稱(chēng)為“編程圣經(jīng)”,與牛頓的“自然哲學(xué)的數(shù)學(xué)原理”等一起,被評(píng)為“世界歷史上最偉大的十種科學(xué)著作”之一(也不知是誰(shuí)評(píng)的,我沒(méi)查到,不過(guò)反正這本書(shū)很牛就是了)。 void foo(int a) { printf ("In foo, a = %d\n", a); } unsigned char code[9]; * ((DWORD *) &code[0]) = 0x042444FF; /* inc dword ptr [esp+4] */ code[4] = 0xe9; /* JMP */ * ((DWORD *) &code[5]) = (DWORD) &foo - (DWORD) &code[0] - 9; /* 跳轉(zhuǎn)偏移量 */ void (*pf)(int/* a*/) = (void (*)(int)) &code[0]; pf (6); 這是一段典型的thunk代碼,其執(zhí)行結(jié)果是“In foo, a = 7”。 void ThunkTemplate(DWORD& addr1,DWORD& addr2)//生成機(jī)器碼 { int flag = 0; DWORD x1,x2; if(flag) { //注意,這個(gè)括號(hào)中的代碼無(wú)法直接執(zhí)行,因?yàn)槠渲锌赡芎袩o(wú)意義的占位數(shù)。 __asm { thunk_begin: ;//這里寫(xiě)thunk代碼的匯編語(yǔ)句. ... thunk_end: ; } } __asm { mov x1,offset thunk_begin; //取 Thunk代碼段 的地址范圍. mov x2,offset thunk_end; } addr1 = x1; addr2 = x2; } 上面的函數(shù)用于生成thunk的機(jī)器碼模板,之所以稱(chēng)為模板,是因?yàn)槠渲邪藷o(wú)意義的占位數(shù),必須將這些占位數(shù)替換為有意義的值之后,才可以執(zhí)行這些代 碼。因此,在函數(shù)中thunk代碼模板放在一個(gè)if(0)語(yǔ)句中,就是避免調(diào)用該函數(shù)的時(shí)候執(zhí)行thunk代碼。另外,為了能方便的得到thunk代碼模 板的地址,這里采用一個(gè)函數(shù)傳出thunk代碼的首尾地址。 至于替換占位數(shù)的功能是很簡(jiǎn)單的,直接替換就好。 void ReplaceCodeBuf(BYTE *code,int len, DWORD old,DWORD x)//完成動(dòng)態(tài)值的替換. { int i=0; for(i=0;i<len-4;++i) { if(*((DWORD *)&code[i])==old) { *((DWORD *)&code[i]) = x; return ; } } }這樣使用兩個(gè)函數(shù): DWORD addr1,addr2; ThunkTemplate(addr1,addr2); memset(m_thunk,0,100);//m_thunk是一個(gè)數(shù)組: char m_thunk[100]; memcpy(m_thunk,(void*)addr1,addr2-addr1);//將代碼拷貝到m_thunk中。 ReplaceCodeBuf(m_thunk,addr2-addr1,-1,(DWORD)((void*)this));//將m_thunk中的-1替換為this指針的值。
原理部分到此為止。下面舉一個(gè)完整的,有實(shí)際意義的例子。在windows中,回調(diào)函數(shù)的使用是很常見(jiàn)的。比如窗口過(guò)程,又比如定時(shí)器回調(diào)函數(shù)。這些函
數(shù),你寫(xiě)好代碼,但是卻從不直接調(diào)用。相反,你把函數(shù)地址傳遞給系統(tǒng),當(dāng)系統(tǒng)檢測(cè)到某些事件發(fā)生的時(shí)候,系統(tǒng)來(lái)調(diào)用這些函數(shù)。這樣當(dāng)然很好,不過(guò)如果你想
做一個(gè)封裝,將所有相關(guān)部分寫(xiě)成一個(gè)類(lèi),那問(wèn)題就來(lái)了。 VOID CALLBACK TimerProc( HWND hwnd, // handle to window UINT uMsg, // WM_TIMER message UINT_PTR idEvent, // timer identifier DWORD dwTime // current system time );
四個(gè)參數(shù),個(gè)個(gè)都有用途。沒(méi)有地方可以讓你傳遞那個(gè)this指針。當(dāng)然了,你實(shí)在要傳也可以做到,比如將hwnd設(shè)置為一個(gè)結(jié)構(gòu)體的指針,其中包含原來(lái)的
hwnd和一個(gè)this指針。在定時(shí)器回調(diào)函數(shù)中取出hwnd后強(qiáng)制轉(zhuǎn)化為結(jié)構(gòu)體指針,取出原來(lái)的hwnd,取出this指針?,F(xiàn)在就可以通過(guò)this指
針自由的調(diào)用類(lèi)成員函數(shù)了。不過(guò)這種方法不是我想要的,我要的是一個(gè)通用,統(tǒng)一的解決方法。通過(guò)在參數(shù)里面加塞夾帶的方法,一般也是沒(méi)有問(wèn)題的,不過(guò)如果
碰到一個(gè)回調(diào)函數(shù)沒(méi)有參數(shù)怎么辦?另外,本來(lái)是封裝為一個(gè)類(lèi)的,結(jié)果還是要帶著一個(gè)全局函數(shù),你難道不覺(jué)得有些不爽嗎?
1、準(zhǔn)備好this指針 關(guān)鍵的代碼如下(完整的工程在附件中): void ThunkTemplate(DWORD& addr1,DWORD& addr2,int calltype=0) { int flag = 0; DWORD x1,x2; if(flag) { __asm //__thiscall { thiscall_1: mov ecx,-1; //-1占位符,運(yùn)行時(shí)將被替換為this指針. mov eax,-2; //-2占位符,運(yùn)行時(shí)將被替換為CTimer::CallBcak的地址. jmp eax; thiscall_2: ; } __asm //__stdcall { stdcall_1: push dword ptr [esp] ; //保存(復(fù)制)返回地址到當(dāng)前棧中 mov dword ptr [esp+4], -1 ; //將this指針?biāo)腿霔V?,即原?lái)的返回地址處 mov eax, -2; jmp eax ; //跳轉(zhuǎn)至目標(biāo)消息處理函數(shù)(類(lèi)成員函數(shù)) stdcall_2: ; } } if(calltype==0)//this_call { __asm { mov x1,offset thiscall_1; //取 Thunk代碼段 的地址范圍. mov x2,offset thiscall_2 ; } } else { __asm { mov x1,offset stdcall_1; mov x2,offset stdcall_2 ; } } addr1 = x1; addr2 = x2; } 上面的函數(shù)有幾個(gè)地方需要說(shuō)明:
1、為了能適應(yīng)兩種不同的成員函數(shù)調(diào)用約定,這里寫(xiě)了兩份代碼。通過(guò)參數(shù)calltype決定拷貝哪一份代碼到緩沖區(qū)。 mov eax,-2; jmp eax; 這是由匯編語(yǔ)言的特點(diǎn)決定的。直接寫(xiě)jmp -2是通不過(guò)的(根據(jù)地址的不同,jmp匯編后可能出現(xiàn)好幾種形式。這里必須出現(xiàn)一個(gè)真實(shí)的地址以便匯編器決定jmp類(lèi)型)。 設(shè)置thunk代碼的完整代碼如下: DWORD FuncAddr; GetMemberFuncAddr_VC6(FuncAddr,&CTimer::CallBcak); DWORD addr1,addr2; ThunkTemplate(addr1,addr2,0); memset(m_thunk,0,100); memcpy(m_thunk,(void*)addr1,addr2-addr1); ReplaceCodeBuf(m_thunk,addr2-addr1,-1,(DWORD)((void*)this)); //將-1替換為this指針. ReplaceCodeBuf(m_thunk,addr2-addr1,-2,FuncAddr); //將-2替換為成員函數(shù)的指針. 如果你還想和以前一樣直接在數(shù)組中賦值機(jī)器碼(畢竟這樣看起來(lái)很酷,我完全理解)。那也可以這樣,調(diào)用ThunkTemplate生成m_thunk后,打印出該數(shù)組的值,而后在程序中直接給m_thunk數(shù)組賦值,就象網(wǎng)上大部分thunk代碼那樣 ,當(dāng)然在調(diào)用前要多一個(gè)步驟就是替換掉占位數(shù)。不過(guò)無(wú)論如何,調(diào)用這兩個(gè)函數(shù)生成機(jī)器碼應(yīng)該比手工查找方便多了,如果你也這樣認(rèn)為,那就算我這篇文章沒(méi)白寫(xiě)。 參考文獻(xiàn):基于 Thunk 實(shí)現(xiàn)的類(lèi)成員消息處理函數(shù) |
|