程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> Thunk技術的一個改進

Thunk技術的一個改進

編輯:關於VC++

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代碼那樣,當然在調用前要多一個步驟就是替換掉占位數。不過無 論如何,調用這兩個函數生成機器碼應該比手工查找方便多了,如果你也這樣認 為,那就算我這篇文章沒白寫。

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved