一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

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

 ShaneWu 2009-11-18

  Thunk技術(shù),一般認(rèn)為是在程序中直接構(gòu)造出可執(zhí)行代碼的技術(shù)(在正常情況下,這是編譯器的任務(wù))。《深度探索C++對(duì)象模型》中對(duì)這個(gè)詞的來源有過考證(在中文版的162頁(yè)),說thunk是knuth的倒拼字。knuth就是大名鼎鼎的計(jì)算機(jī)經(jīng)典名著 《The Art of Computer Programming》的作者,該書被程序員們稱為“編程圣經(jīng)”,與牛頓的“自然哲學(xué)的數(shù)學(xué)原理”等一起,被評(píng)為“世界歷史上最偉大的十種科學(xué)著作”之一(也不知是誰(shuí)評(píng)的,我沒查到,不過反正這本書很牛就是了)。
一般情況下,使用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ù)組的首地址,最后通過該函數(shù) 指針調(diào)用thunk代碼。這里使用了函數(shù)指針完成調(diào)用,好處是代碼比較清晰易讀。也可以使用匯編代碼jmp或call來完成,這樣就不必額外定義一個(gè)函數(shù) 指針。
網(wǎng)絡(luò)上的thunk代碼,基本上都是這個(gè)思路。如果你實(shí)際寫一段這樣的代碼,一定會(huì)發(fā)現(xiàn)很麻煩。對(duì)著教科書查找每一個(gè)匯編指令的機(jī)器碼,相信不會(huì)是一件愉快的事情。其實(shí)我們回過頭來想想,這件事計(jì)算機(jī)來做不是最合適嗎,編譯器不就是做這個(gè)事情的嗎?
以上面的代碼為例,讓我們重新考慮一下整個(gè)過程。我們的目的是在調(diào)用函數(shù)foo之前將參數(shù)增加1。一般而言,這樣做肯定是沒有foo函數(shù)的源代碼或者不允 許修改源代碼,否則直接改foo函數(shù)的代碼就好了,何必這么麻煩。為了調(diào)用時(shí)候的簡(jiǎn)單化,定義一個(gè)函數(shù)指針是比較合適的,否則每次調(diào)用都寫匯編代碼jmp 或call太麻煩。這樣一來,函數(shù)指針必須指向一個(gè)代碼段的地址。但是這個(gè)代碼段必須用機(jī)器碼來構(gòu)造嗎,直接寫匯編代碼也同樣可以做到。
當(dāng)然,這里有一個(gè)問題。我們寫匯編指令的時(shí)候,必須是一條指令一條指令的寫,不能說指令寫一半,然后讓匯編程序去處理。上面的代碼中,第一條指令inc直 接寫匯編語(yǔ)句當(dāng)然沒問題。但下面的jmp語(yǔ)句,就不能直接寫。因?yàn)槲覀儗憛R編語(yǔ)句的時(shí)候,jmp跳轉(zhuǎn)偏移量是未知的,必須編譯后才知道。并且我們不能只寫 jmp而不寫偏移量,那是通不過編譯的。
這個(gè)問題可以這樣解決,寫jmp語(yǔ)句的時(shí)候,我們寫一個(gè)占位的DWORD,其值設(shè)為一個(gè)特殊的值,比如0xffff(原理是這樣,實(shí)際處理還要迂回一下, 后面有說明)。只要在這段thunk代碼中不出現(xiàn)這個(gè)值就好。然后執(zhí)行的時(shí)候,在第一次調(diào)用之前,在thunk代碼中查找該值,將其替換為計(jì)算出來的動(dòng)態(tài) 值。經(jīng)過這樣的處理,就可以徹底在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)中的代碼無法直接執(zhí)行,因?yàn)槠渲锌赡芎袩o意義的占位數(shù)。
__asm
{
thunk_begin:
;//這里寫thunk代碼的匯編語(yǔ)句.
...
thunk_end:   ;
}
}
__asm
{
mov   x1,offset thunk_begin; //取 Thunk代碼段 的地址范圍.
mov   x2,offset thunk_end;
}
addr1 = x1;
addr2 = x2;
}

上面的函數(shù)用于生成thunk的機(jī)器碼模板,之所以稱為模板,是因?yàn)槠渲邪藷o意義的占位數(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ù)的使用是很常見的。比如窗口過程,又比如定時(shí)器回調(diào)函數(shù)。這些函 數(shù),你寫好代碼,但是卻從不直接調(diào)用。相反,你把函數(shù)地址傳遞給系統(tǒng),當(dāng)系統(tǒng)檢測(cè)到某些事件發(fā)生的時(shí)候,系統(tǒng)來調(diào)用這些函數(shù)。這樣當(dāng)然很好,不過如果你想 做一個(gè)封裝,將所有相關(guān)部分寫成一個(gè)類,那問題就來了。
問題是,這些回調(diào)函數(shù)的形式事先已經(jīng)定義好了,你無法讓一個(gè)類的成員函數(shù)成為一個(gè)回調(diào)函數(shù),因?yàn)轭愋筒豢赡芷ヅ?。這不能怪微軟,微軟不可能將回調(diào)函數(shù)定義 為一個(gè)類成員函數(shù)(該定義為什么類?),而只能將回調(diào)函數(shù)定義為一個(gè)全局的函數(shù)。并且微軟其實(shí)很多時(shí)候也提供了補(bǔ)救措施,在回調(diào)函數(shù)中增加了一個(gè)void *的參數(shù)。這個(gè)參數(shù)一般都用來傳遞類的this指針。這樣一來,可以這樣解決:給系統(tǒng)提供一個(gè)全局函數(shù)作為回調(diào)函數(shù),在該函數(shù)中通過額外的那個(gè)void *參數(shù)訪問到類的對(duì)象,從而直接調(diào)用到類成員函數(shù)。如此,你的封裝一樣可以完成,不過多了一次函數(shù)調(diào)用而已。

但是,不是所有的回調(diào)函數(shù)都這么幸運(yùn),微軟都給它們提供了一個(gè)額外的參數(shù)。比如,定時(shí)器的回調(diào)函數(shù)就沒有。

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è)都有用途。沒有地方可以讓你傳遞那個(gè)this指針。當(dāng)然了,你實(shí)在要傳也可以做到,比如將hwnd設(shè)置為一個(gè)結(jié)構(gòu)體的指針,其中包含原來的 hwnd和一個(gè)this指針。在定時(shí)器回調(diào)函數(shù)中取出hwnd后強(qiáng)制轉(zhuǎn)化為結(jié)構(gòu)體指針,取出原來的hwnd,取出this指針?,F(xiàn)在就可以通過this指 針自由的調(diào)用類成員函數(shù)了。不過這種方法不是我想要的,我要的是一個(gè)通用,統(tǒng)一的解決方法。通過在參數(shù)里面加塞夾帶的方法,一般也是沒有問題的,不過如果 碰到一個(gè)回調(diào)函數(shù)沒有參數(shù)怎么辦?另外,本來是封裝為一個(gè)類的,結(jié)果還是要帶著一個(gè)全局函數(shù),你難道不覺得有些不爽嗎?
這正是thunk技術(shù)大顯身手的地方了。我們知道,所謂類成員函數(shù),和對(duì)應(yīng)的全局函數(shù),其實(shí)就差一個(gè)this指針。如果我們?cè)谙到y(tǒng)調(diào)用函數(shù)之前正確處理好this指針,那系統(tǒng)就可以正確的調(diào)用類成員函數(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?,即原來的返回地址?mov   eax,  -2;
jmp   eax                    ; //跳轉(zhuǎn)至目標(biāo)消息處理函數(shù)(類成員函數(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è)地方需要說明:

1、為了能適應(yīng)兩種不同的成員函數(shù)調(diào)用約定,這里寫了兩份代碼。通過參數(shù)calltype決定拷貝哪一份代碼到緩沖區(qū)。
2、本來一條jmp xxxx;指令這里分解為兩條指令:

mov eax,-2;
jmp eax;

  這是由匯編語(yǔ)言的特點(diǎn)決定的。直接寫jmp -2是通不過的(根據(jù)地址的不同,jmp匯編后可能出現(xiàn)好幾種形式。這里必須出現(xiàn)一個(gè)真實(shí)的地址以便匯編器決定jmp類型)。
3、如果對(duì)this指針的知識(shí)不清楚,請(qǐng)參考我在vc知識(shí)庫(kù)的另外一篇文章《直接調(diào)用類成員函數(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ī)器碼(畢竟這樣看起來很酷,我完全理解)。那也可以這樣,調(diào)用ThunkTemplate生成m_thunk后,打印出該數(shù)組的值,而后在程序中直接給m_thunk數(shù)組賦值,就象網(wǎng)上大部分thunk代碼那樣 ,當(dāng)然在調(diào)用前要多一個(gè)步驟就是替換掉占位數(shù)。不過無論如何,調(diào)用這兩個(gè)函數(shù)生成機(jī)器碼應(yīng)該比手工查找方便多了,如果你也這樣認(rèn)為,那就算我這篇文章沒白寫。

參考文獻(xiàn):基于 Thunk 實(shí)現(xiàn)的類成員消息處理函數(shù)

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(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)遵守用戶 評(píng)論公約

    類似文章 更多

    中文字幕精品人妻一区| 一区二区三区日本高清| 老熟女露脸一二三四区| 国产一区二区在线免费| 国产精品免费视频专区| 色丁香一区二区黑人巨大| 精品少妇一区二区视频| 99久久免费看国产精品| 99久久精品免费精品国产| 色婷婷丁香激情五月天| 欧美三级不卡在线观线看| 久久国产亚洲精品赲碰热| 国产又大又猛又粗又长又爽| 无套内射美女视频免费在线观看 | 中文字幕久久精品亚洲乱码| 91熟女大屁股偷偷对白| 人人爽夜夜爽夜夜爽精品视频| 欧美激情床戏一区二区三| 国产视频一区二区三区四区| 国产欧美日韩一级小黄片| 午夜精品福利视频观看| 中文字幕亚洲精品乱码加勒比| 亚洲国产黄色精品在线观看| 中文字幕人妻一区二区免费| 人妻少妇久久中文字幕久久| 国产精品国三级国产专不卡| 亚洲一区二区三区国产| 日韩欧美国产高清在线| 日韩在线中文字幕不卡| 亚洲熟妇熟女久久精品| 国产成人在线一区二区三区| 丰满少妇被猛烈撞击在线视频| 熟妇久久人妻中文字幕| 久久成人国产欧美精品一区二区| 亚洲午夜福利不卡片在线| 黄色美女日本的美女日人| 欧美亚洲91在线视频| 丝袜视频日本成人午夜视频| 国产男女激情在线视频| 精品国产亚洲区久久露脸| 人妻中文一区二区三区|