Thunk 技術的乙個改進

2021-09-06 03:45:07 字數 3470 閱讀 4390

thunk 技術的乙個改進

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

關鍵字:thunk  機器碼 this指標

thunk技術,一般認為是在程式中直接構造出可執行**的技術(在正常情況下,這是編譯器的任務)。《深度探索c++物件模型》中對這個詞的**有過考證(在中文版的162頁),說thunk是knuth的倒拼字。knuth就是大名鼎鼎的計算機經典名著《the art of computer programming》的作者,該書被程式設計師們稱為「程式設計聖經」,與牛頓的「自然哲學的數學原理」等一起,被評為「世界歷史上最偉大的十種科學著作」之一(也不知是誰評的,我沒查到,不過反正這本書很牛就是了)。

一般情況下,使用thunk技術都是事先查好指令的機器碼,然後將陣列或結構體賦值為這些機器碼的二進位制值,最後再跳轉到陣列或結構體的首位址。比如在參考文獻[1]中的**:

void foo(int 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)//生成機器碼

} __asm

addr1 = x1;

addr2 = x2;

}

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

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

void replacecodebuf(byte *code,int len, dword old,dword x)//完成動態值的替換.

__asm //__stdcall

}if(calltype==0)//this_call }

else }

addr1 = x1;

addr2 = x2;

}

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

1、為了能適應兩種不同的成員函式呼叫約定,這裡寫了兩份**。通過引數calltype決定拷貝哪乙份**到緩衝區。

2、本來一條jmp ***x;指令這裡分解為兩條指令:

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 實現的類成員訊息處理函式

[文章投稿]

看著鬼斧神工,但只是對已存在的函式**的呼叫而已,我對thunk技術究竟有沒有實用價值表懷疑...

( tgl10 發表於 2008-6-13 14:21:00)

看上去是錯的.

__stdcall 被呼叫者負責**stack,你這裡私自push了一次,函式返回後stack會出問題的吧 ( jink1025 發表於 2008-3-31 9:38:00)

Thunk 技術的乙個改進

thunk 技術的乙個改進 摘要 介紹了 thunk 技術中如何避免直接寫機器碼。關鍵字 thunk 機器碼 this指標 thunk技術,一般認為是在程式中直接構造出可執行 的技術 在正常情況下,這是編譯器的任務 深度探索c 物件模型 中對這個詞的 有過考證 在中文版的162頁 說thunk是kn...

thunk技術實現視窗類的封裝

當前位置 文章中心 技術開發 程式開發 文章內容 減小字型 增大字型 mfc功能已經非常強大,自己做介面庫也許沒什麼意思,但是這個過程中卻能學到很多東西。比如說 視窗類的封裝,從全域性視窗訊息處理到視窗物件訊息處理的對映方法 對介面進行封裝,一般都是乙個視窗乙個類,比如實現乙個最基本的視窗類cmyw...

乙個緊急查詢的改進思路

今天下午有乙個緊急需求,是輔助業務部門做乙個緊急查詢,既然說緊急查詢,那麼肯定業務上需要加急處理,那麼很快就需要找到我們dba來幫忙了。需求的情況是,需要根據某乙個使用者的標識 比如手機號 來定位對應使用者的id,可能同乙個身份證號,可以註冊多個不同的id。根據資料的情況,在系統中已經做了分庫分表。...