Thunk 技術的一個改進

來源:互聯網
上載者:User

Thunk 技術的一個改進

作者:南風

下載原始碼

摘要:介紹了 thunk 技術中如何避免直接寫機器碼。

關鍵字:Thunk  機器碼 this指標

  Thunk技術,一般認為是在程式中直接構造出可執行代碼的技術(在正常情況下,這是編譯器的任務)。《深度探索C++物件模型》中對這個詞的來源有過考證(在中文版的162頁),說thunk是knuth的倒拼字。knuth就是大名鼎鼎的電腦經典名著《The Art of Computer Programming》的作者,該書被程式員們稱為“編程聖經”,與牛頓的“自然哲學的數學原理”等一起,被評為“世界曆史上最偉大的十種科學著作”之一(也不知是誰評的,我沒查到,不過反正這本書很牛就是了)。
  一般情況下,使用thunk技術都是事先查好指令的機器碼,然後將數組或結構體賦值為這些機器碼的二進位值,最後再跳轉到數組或結構體的首地址。比如在參考文獻[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; /* 跳轉位移量 */void (*pf)(int/* a*/) = (void (*)(int)) &code[0];pf (6);

  這是一段典型的thunk代碼,其執行結果是“In foo, a = 7”。
可以看到,它定義了一個數組code[9],然後將事先查好的各彙編指令的機器碼直接賦值給數組。然後定義一個函數指標等於數組的首地址,最後通過該函數指標調用thunk代碼。這裡使用了函數指標完成調用,好處是代碼比較清晰易讀。也可以使用彙編代碼jmp或call來完成,這樣就不必額外定義一個函數指標。
  網路上的thunk代碼,基本上都是這個思路。如果你實際寫一段這樣的代碼,一定會發現很麻煩。對著教科書尋找每一個彙編指令的機器碼,相信不會是一件愉快的事情。其實我們回過頭來想想,這件事電腦來做不是最合適嗎,編譯器不就是做這個事情的嗎?
  以上面的代碼為例,讓我們重新考慮一下整個過程。我們的目的是在調用函數foo之前將參數增加1。一般而言,這樣做肯定是沒有foo函數的原始碼或者不允許修改原始碼,否則直接改foo函數的代碼就好了,何必這麼麻煩。為了調用時候的簡單化,定義一個函數指標是比較合適的,否則每次調用都寫彙編代碼jmp 或call太麻煩。這樣一來,函數指標必須指向一個程式碼片段的地址。但是這個程式碼片段必須用機器碼來構造嗎,直接寫彙編代碼也同樣可以做到。
  當然,這裡有一個問題。我們寫彙編指令的時候,必須是一條指令一條指令的寫,不能說指令寫一半,然後讓組譯工具去處理。上面的代碼中,第一條指令inc直接寫彙編語句當然沒問題。但下面的jmp語句,就不能直接寫。因為我們寫彙編語句的時候,jmp跳轉位移量是未知的,必須編譯後才知道。並且我們不能唯寫 jmp而不寫位移量,那是通不過編譯的。
  這個問題可以這樣解決,寫jmp語句的時候,我們寫一個佔位的DWORD,其值設為一個特殊的值,比如0xffff(原理是這樣,實際處理還要迂迴一下,後面有說明)。只要在這段thunk代碼中不出現這個值就好。然後執行的時候,在第一次調用之前,在thunk代碼中尋找該值,將其替換為計算出來的動態值。經過這樣的處理,就可以徹底在thunk代碼中消除機器碼的直接操作。
  更一般化,為了產生正確的機器碼,我們用兩個函數。一個用於產生機器碼的模板,另一個函數用於在機器碼的模板中填入需要動態計算產生的值。下面是一個例子:

void ThunkTemplate(DWORD& addr1,DWORD& addr2)//產生機器碼{int flag = 0;DWORD x1,x2;if(flag){          //注意,這個括弧中的代碼無法直接執行,因為其中可能含有無意義的佔位數。__asm{thunk_begin:                   ;//這裡寫thunk代碼的彙編語句.                    ...thunk_end:   ;}}__asm{mov   x1,offset thunk_begin; //取 Thunk程式碼片段 的位址範圍.mov   x2,offset thunk_end;}    addr1 = x1;    addr2 = x2;}

  上面的函數用於產生thunk的機器碼模板,之所以稱為模板,是因為其中包含了無意義的佔位數,必須將這些佔位數替換為有意義的值之後,才可以執行這些代碼。因此,在函數中thunk代碼模板放在一個if(0)語句中,就是避免調用該函數的時候執行thunk代碼。另外,為了能方便的得到thunk代碼模板的地址,這裡採用一個函數傳出thunk代碼的首尾地址。

至於替換佔位數的功能是很簡單的,直接替換就好。

void ReplaceCodeBuf(BYTE *code,int len, DWORD old,DWORD x)//完成動態值的替換.{int i=0;for(i=0;i<len-4;++i){if(*((DWORD *)&code[i])==old){*((DWORD *)&code[i]) = x;return ;}}}

這樣使用兩個函數:

DWORD addr1,addr2;ThunkTemplate(addr1,addr2);memset(m_thunk,0,100);//m_thunk是一個數組: 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指標的值。

  原理部分到此為止。下面舉一個完整的,有實際意義的例子。在windows中,回呼函數的使用是很常見的。比如視窗過程,又比如定時器回呼函數。這些函數,你寫好代碼,但是卻從不直接調用。相反,你把函數地址傳遞給系統,當系統檢測到某些事件發生的時候,系統來調用這些函數。這樣當然很好,不過如果你想做一個封裝,將所有相關部分寫成一個類,那問題就來了。
  問題是,這些回呼函數的形式事先已經定義好了,你無法讓一個類的成員函數成為一個回呼函數,因為類型不可能匹配。這不能怪微軟,微軟不可能將回呼函數定義為一個類成員函數(該定義為什麼類?),而只能將回呼函數定義為一個全域的函數。並且微軟其實很多時候也提供了補救措施,在回呼函數中增加了一個void *的參數。這個參數一般都用來傳遞類的this指標。這樣一來,可以這樣解決:給系統提供一個全域函數作為回呼函數,在該函數中通過額外的那個void *參數訪問到類的對象,從而直接調用到類成員函數。如此,你的封裝一樣可以完成,不過多了一次函數調用而已。

但是,不是所有的回呼函數都這麼幸運,微軟都給它們提供了一個額外的參數。比如,定時器的回呼函數就沒有。

VOID CALLBACK TimerProc(  HWND hwnd,         // handle to window  UINT uMsg,         // WM_TIMER message  UINT_PTR idEvent,  // timer identifier  DWORD dwTime       // current system time);

四個參數,個個都有用途。沒有地方可以讓你傳遞那個this指標。當然了,你實在要傳也可以做到,比如將hwnd設定為一個結構體的指標,其中包含原來的 hwnd和一個this指標。在定時器回呼函數中取出hwnd後強制轉化為結構體指標,取出原來的hwnd,取出this指標。現在就可以通過this指標自由的調用類成員函數了。不過這種方法不是我想要的,我要的是一個通用,統一的解決方案。通過在參數裡面加塞夾帶的方法,一般也是沒有問題的,不過如果碰到一個回呼函數沒有參數怎麼辦?另外,本來是封裝為一個類的,結果還是要帶著一個全域函數,你難道不覺得有些不爽嗎?
  這正是thunk技術大顯身手的地方了。我們知道,所謂類成員函數,和對應的全域函數,其實就差一個this指標。如果我們在系統調用函數之前正確處理好this指標,那系統就可以正確的調用類成員函數。
  具體的思路是這樣的:當系統需要一個回呼函數地址的時候,我們傳遞一個thunk程式碼片段的地址。這個程式碼片段做兩件事:

1、準備好this指標
2、調用成員函數

關鍵的代碼如下(完整的工程在附件中):

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預留位置,運行時將被替換為this指標.    mov   eax,-2;   //-2預留位置,運行時將被替換為CTimer::CallBcak的地址.    jmp   eax;thiscall_2:  ;}__asm //__stdcall{stdcall_1:push  dword ptr [esp]        ; //儲存(複製)返回地址到當前棧中mov   dword ptr [esp+4], -1  ; //將this指標送入棧中,即原來的返回地址處mov   eax,  -2;        jmp   eax                    ; //跳轉至目標訊息處理函數(類成員函數)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;}

上面的函數有幾個地方需要說明:

1、為了能適應兩種不同的成員函數呼叫慣例,這裡寫了兩份代碼。通過參數calltype決定拷貝哪一份代碼到緩衝區。
2、本來一條jmp xxxx;指令這裡分解為兩條指令:

mov eax,-2;jmp eax;

  這是由組合語言的特點決定的。直接寫jmp -2是通不過的(根據地址的不同,jmp彙編後可能出現好幾種形式。這裡必須出現一個真實的地址以便彙編器決定jmp類型)。
3、如果對this指標的知識不清楚,請參考我在vc知識庫的另外一篇文章《直接調用類成員函數地址》。

設定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替換為成員函數的指標.

  如果你還想和以前一樣直接在數組中賦值機器碼(畢竟這樣看起來很酷,我完全理解)。那也可以這樣,調用ThunkTemplate產生m_thunk後,列印出該數組的值,而後在程式中直接給m_thunk數組賦值,就象網上大部分thunk代碼那樣,當然在調用前要多一個步驟就是替換掉佔位數。不過無論如何,調用這兩個函數產生機器碼應該比手工尋找方便多了,如果你也這樣認為,那就算我這篇文章沒白寫。

參考文獻:基於 Thunk 實現的類成員訊息處理函數

最新評論 [發表評論] [文章投稿] 查看所有評論 推薦給好友 列印
感謝。。好文。。 ( bingdao68 發表於 2011-3-22 22:57:00)
 
絕好的文章! ( wyyayy 發表於 2010-8-23 18:31:00)
 
很巧妙.
看著鬼斧神工,但只是對已存在的函數代碼的調用而已,我對thunk技術究竟有沒有實用價值表懷疑...

( tgl10 發表於 2008-6-13 14:21:00)
 
哦,請忽略我前面的疑問,我錯了,被調用函數被改過可以推平stack. ( jink1025 發表於 2008-3-31 9:44:00)
 
__asm //__stdcall
{
stdcall_1:
push  dword ptr [esp]; //儲存(複製)返回地址到當前棧中
mov   dword ptr [esp+4], -1; //將this指標送入棧中,即原來的返回地址處
mov   eax,  -2;
jmp   eax; //跳轉至目標訊息處理函數(類成員函數)
stdcall_2:;
}

看上去是錯的.
__stdcall 被調用者負責回收stack,你這裡私自push了一次,函數返回後stack會出問題的吧 ( jink1025 發表於 2008-3-31 9:38:00)
 
本來就是這樣的,還用你說呀 ( sheds 發表於 2008-3-27 22:01:00)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.