午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

Thunk 技術(shù)的一個(gè)改進(jìn)

 ShaneWu 2009-11-18

  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ū)很牛就是了)。
一般情況下,使用thunk技術(shù)都是事先查好指令的機(jī)器碼,然后將數(shù)組或結(jié)構(gòu)體賦值為這些機(jī)器碼的二進(jìn)制值,最后再跳轉(zhuǎn)到數(shù)組或結(jié)構(gòu)體的首地址。比如在參考文獻(xiàn)[1]中的代碼:

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”。
可以看到,它定義了一個(gè)數(shù)組code[9],然后將事先查好的各匯編指令的機(jī)器碼直接賦值給數(shù)組。然后定義一個(gè)函數(shù)指針等于數(shù)組的首地址,最后通過(guò)該函數(shù) 指針調(diào)用thunk代碼。這里使用了函數(shù)指針完成調(diào)用,好處是代碼比較清晰易讀。也可以使用匯編代碼jmp或call來(lái)完成,這樣就不必額外定義一個(gè)函數(shù) 指針。
網(wǎng)絡(luò)上的thunk代碼,基本上都是這個(gè)思路。如果你實(shí)際寫(xiě)一段這樣的代碼,一定會(huì)發(fā)現(xiàn)很麻煩。對(duì)著教科書(shū)查找每一個(gè)匯編指令的機(jī)器碼,相信不會(huì)是一件愉快的事情。其實(shí)我們回過(guò)頭來(lái)想想,這件事計(jì)算機(jī)來(lái)做不是最合適嗎,編譯器不就是做這個(gè)事情的嗎?
以上面的代碼為例,讓我們重新考慮一下整個(gè)過(guò)程。我們的目的是在調(diào)用函數(shù)foo之前將參數(shù)增加1。一般而言,這樣做肯定是沒(méi)有foo函數(shù)的源代碼或者不允 許修改源代碼,否則直接改foo函數(shù)的代碼就好了,何必這么麻煩。為了調(diào)用時(shí)候的簡(jiǎn)單化,定義一個(gè)函數(shù)指針是比較合適的,否則每次調(diào)用都寫(xiě)匯編代碼jmp 或call太麻煩。這樣一來(lái),函數(shù)指針必須指向一個(gè)代碼段的地址。但是這個(gè)代碼段必須用機(jī)器碼來(lái)構(gòu)造嗎,直接寫(xiě)匯編代碼也同樣可以做到。
當(dāng)然,這里有一個(gè)問(wèn)題。我們寫(xiě)匯編指令的時(shí)候,必須是一條指令一條指令的寫(xiě),不能說(shuō)指令寫(xiě)一半,然后讓匯編程序去處理。上面的代碼中,第一條指令inc直 接寫(xiě)匯編語(yǔ)句當(dāng)然沒(méi)問(wèn)題。但下面的jmp語(yǔ)句,就不能直接寫(xiě)。因?yàn)槲覀儗?xiě)匯編語(yǔ)句的時(shí)候,jmp跳轉(zhuǎn)偏移量是未知的,必須編譯后才知道。并且我們不能只寫(xiě) jmp而不寫(xiě)偏移量,那是通不過(guò)編譯的。
這個(gè)問(wèn)題可以這樣解決,寫(xiě)jmp語(yǔ)句的時(shí)候,我們寫(xiě)一個(gè)占位的DWORD,其值設(shè)為一個(gè)特殊的值,比如0xffff(原理是這樣,實(shí)際處理還要迂回一下, 后面有說(shuō)明)。只要在這段thunk代碼中不出現(xiàn)這個(gè)值就好。然后執(zhí)行的時(shí)候,在第一次調(diào)用之前,在thunk代碼中查找該值,將其替換為計(jì)算出來(lái)的動(dòng)態(tài) 值。經(jīng)過(guò)這樣的處理,就可以徹底在thunk代碼中消除機(jī)器碼的直接操作。
更一般化,為了生成正確的機(jī)器碼,我們用兩個(gè)函數(shù)。一個(gè)用于生成機(jī)器碼的模板,另一個(gè)函數(shù)用于在機(jī)器碼的模板中填入需要?jiǎng)討B(tài)計(jì)算產(chǎn)生的值。下面是一個(gè)例子:

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)了。
問(wèn)題是,這些回調(diào)函數(shù)的形式事先已經(jīng)定義好了,你無(wú)法讓一個(gè)類(lèi)的成員函數(shù)成為一個(gè)回調(diào)函數(shù),因?yàn)轭?lèi)型不可能匹配。這不能怪微軟,微軟不可能將回調(diào)函數(shù)定義 為一個(gè)類(lèi)成員函數(shù)(該定義為什么類(lèi)?),而只能將回調(diào)函數(shù)定義為一個(gè)全局的函數(shù)。并且微軟其實(shí)很多時(shí)候也提供了補(bǔ)救措施,在回調(diào)函數(shù)中增加了一個(gè)void *的參數(shù)。這個(gè)參數(shù)一般都用來(lái)傳遞類(lèi)的this指針。這樣一來(lái),可以這樣解決:給系統(tǒng)提供一個(gè)全局函數(shù)作為回調(diào)函數(shù),在該函數(shù)中通過(guò)額外的那個(gè)void *參數(shù)訪問(wèn)到類(lèi)的對(duì)象,從而直接調(diào)用到類(lèi)成員函數(shù)。如此,你的封裝一樣可以完成,不過(guò)多了一次函數(shù)調(diào)用而已。

但是,不是所有的回調(diào)函數(shù)都這么幸運(yùn),微軟都給它們提供了一個(gè)額外的參數(shù)。比如,定時(shí)器的回調(diào)函數(shù)就沒(mé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é)得有些不爽嗎?
這正是thunk技術(shù)大顯身手的地方了。我們知道,所謂類(lèi)成員函數(shù),和對(duì)應(yīng)的全局函數(shù),其實(shí)就差一個(gè)this指針。如果我們?cè)谙到y(tǒng)調(diào)用函數(shù)之前正確處理好this指針,那系統(tǒng)就可以正確的調(diào)用類(lèi)成員函數(shù)。
具體的思路是這樣的:當(dāng)系統(tǒng)需要一個(gè)回調(diào)函數(shù)地址的時(shí)候,我們傳遞一個(gè)thunk代碼段的地址。這個(gè)代碼段做兩件事:

1、準(zhǔn)備好this指針
2、調(diào)用成員函數(shù)

關(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ū)。
2、本來(lái)一條jmp xxxx;指令這里分解為兩條指令:

mov eax,-2;
jmp eax;

  這是由匯編語(yǔ)言的特點(diǎn)決定的。直接寫(xiě)jmp -2是通不過(guò)的(根據(jù)地址的不同,jmp匯編后可能出現(xiàn)好幾種形式。這里必須出現(xiàn)一個(gè)真實(shí)的地址以便匯編器決定jmp類(lèi)型)。
3、如果對(duì)this指針的知識(shí)不清楚,請(qǐng)參考我在vc知識(shí)庫(kù)的另外一篇文章《直接調(diào)用類(lèi)成員函數(shù)地址》。

設(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ù)

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多