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

通用Thunk

編輯:關於VC++

背景

許多庫需要我們提供一個函數作為回調,這使得使用 “面向對象編程”(OOP) 出現了麻煩。因為普通的C函數沒有成員函數需要的this指針。Thunk技術是一種快速但是平台相關的解決此問題的方法。我最近研究過許多有關thunk技術的文章,我認為許多解決方案都是針對於特定問題的。我設計了一組類,來提供一種通用的解決方案。

環境

開發環境 : IA32,Windows Xp SP2,Visual Studio 2005

用法

源代碼提供了5(實際上4)個類(全都在 Thunk 名字空間中)。它們的每一個對象都有2個屬性,對象和方法。它們可以動態的創建一些機器碼。執行這些機器碼將在邏輯上和調用 Obj.Method(…); 舉例來說,如果我們想要設計一個類來進行窗口子類化的工作,我們可以按下面5個步驟使用通用Thunk

class CSubClassing {
private:
  Thunk::ThisToStd m_thunk;
//1.選擇一個合適的Thunk類
// ThisToStd 類使一個使用__thiscall 約定的成員函數 (LRESULT SubProc(…) )
//成為一個使用_stdcall 約定的回調函數WNDPROC)
//2.實例化一個對象.
public:
CSubClassing() {
  m_thunk.Attach(this);
//3.附加到想要回調的對象上
  m_thunk.AttachMethod(&CSubClassing::SubProc);
// 4.附加成員函數
  // to do
}
void Attach(HWND hWnd) {
    m_oldProc = (WNDPROC)SetWindowLong(hWnd,GWL_PROC
      ,m_thunk.MakeCallback<LONG>());
// 5.轉化到回調函數指針
//SetWindowLong函數使用一個LONG值來表示WNDPROC
    // to do
}
private:
  //這個非靜態成員函數將被Windows回調
  LRESULT SubProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam) {
    if (msg!=WE_NEEDED)
      return CallWndProc(m_oldProc,hWnd,msg,wParam,lParam);
    // to do
  }
WNDPROC m_oldProc;
}
這5個類(class)都有相同的界面和使用方式。一旦你依據成員函數與回調函數的調用約定選定好了一個Thunk類,就可以按照上面的步驟做一些有用的事情 : 如WNDPROC,THREADPROC,hooking,等等

更多詳細信息 見 Thunk.h和 示例(sample)工程(project)

示例工程包含5個程序的源代碼,但是沒有可執行文件,否則會太龐大。工程可以在Visual Studio 2005上順利編譯,只要工程的目錄結構維持原樣。5個程序使用一份相同的測試代碼——TestClass.h TestClass.cpp main.cpp。不同之處在預處理器的定義。這樣,它們分別測試了 ThisToStd,ThisToCdecl,StdToStd,StdToCdecl和CdeclToCdecl的功能。除了這些,你還可以從中得知使用一個Thunk類,需要包含和加入到工程中的最少文件。(只包含Thunk.h 並把Thunk.cpp 加入工程中也能工作,但不是最好方法)

原理

原理中最重要的是函數的調用約定(Calling Convention) ,調用者和被調者之間的約定。普通C函數通常使用3種調用約定 : “__cdecl” “__stdcall” “__fastcall” 成員函數通常使用 “__thiscall””__stdcall” “__cdecl”

我們需要著重關注以下3點:

調用者(普通C函數)怎麼准備參數和返回地址?

被調用者(成員函數)希望並且要求的參數和返回地址是什麼?它如何取得它們?

平衡堆棧是誰的責任?

調用者准備的參數和返回地址總不是被調用者所期待的那樣,因為被調用者還需要一個this指針。平衡堆棧的方式也許也會不同。我們的工作就是以被調用者期望的方式,准備好this指針,同時彌補2者在平衡堆棧上的差異。

為了簡單起見,我們以 “ void func(int); void C::func(int); ”為例,首先,我們來看看當使用__stdcall 約定的func被調用的時候,會發生什麼。

func(1212); 編譯器會像這樣准備參數和返回地址 :

PUSH 1212 ; 使得堆棧增加4

CALL func; 使得堆棧也增加4(因為返回地址也被壓入堆棧)

0x50000:...;被調用者返回這裡,我們假設這裡的地址是0x50000

調用者希望被調用者使用 RET 4 (使得堆棧減少8:參數1212使用4,返回地址0x50000也使用4)來平衡堆棧,所以在這之後沒有多余的機器碼。所以,在這之後,堆棧是這個樣子:

...
1212
0x50000 <- ESP

然後,我們來看看使用__thiscall 的被調用者所希望的參數和返回地址。一個真正的成員函數被調用時。

C obj;
obj.func(1212);

編譯器以這樣的方式准備參數:

PUSH 1212;
MOV ECX,obj;
CALL C::func

所以,在這之後,堆棧是這個樣子:


1212
0x50000 <- ESP

ECX 保存著 this 指針。

這也就是被調用者(void __thiscall C::func(int); ) 需要的形式。

第3,我們看看被調用者如何返回。

事實上,它使用 RET 4 來返回到0x50000

所以,我們唯一需要做的就是准備好this指針,然後跳轉到成員函數。(不需要更多的工作,參數和返回值已在正確位置,堆棧也將被正確的平衡。)

設計 ThisToStd

在我們設計第1個,也是最簡單的類 ThisToStd 之前,我們還需要3種信息。

1、我們需要一種得到函數地址的方法。

對於數據指針,我們可以轉化(cast)它到一個 int 值

void *p = &someValue;
int address = reinterpret_cast<int>(p);
/* 如果檢查對64位機的可移植性,將會得到一個警告。不過可以忽略它,因為這個thunk只用在32位機上^_^*/
不同於數據指針,函數指針有更多的限制。void __stdcall fun(int) { … }
void C::fun(int) {}
//int address = (int)fun; // 不允許
//int address = (int)&C::fun; // 同樣錯誤
有2種方法來進行一個強力的轉化template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src) {
  return *static_cast<dst_type*>( static_cast<void*>(&src) );
}
template<typename dst_type,typename src_type>
dst_type union_cast(src_type src) {
  union {
    src_type src;
    dst_type dst;
  } u = {src};
  return u.dst;
}
所以,我們可以實現一個方法template<typename Pointer>
int PointerToInt32(Pointer pointer)
{
 return pointer_cast<int>(pointer); // or union_cast<int>(pointer);
}
int address = PointerToInt32(&fun); // 可以
int address = (int)&C::fun; // 也可以
更多詳細信息見 ThunkBase.h

2.轉移指令的目的地

許多轉移指令的目的地使用“到源的偏移量”來表示

比如:當CPU 執行到0xFF000000 處的指令時, 指令像這個樣子:0xFF000000 : 0xE9 0x33 0x55 0x77 0x99
0xFF000005 : ...

0xE9 是一個 JMP 指令,緊接著的4字節將被解釋為偏移

offset = 0x99775533 (在Intel x86 機器上,低字節存儲在低地址上) = -1720232653

源 (src) = 0xFF000000 (JMP指令的地址) = 4278190080

目的地 (dst) = src+offset+5 (JMP占1字節,偏移占4字節) = 4278190080 – 1720232653 +5 = 2557957432 = 0x98775538

所以在指令 “ JMP -1720232653 “ 之後,下一條被執行的指令將在

0x98775538 : ...

基於這點,我們可以實現2個方法:

void SetTransterDST(
void *src  /* the address of transfer instruction*/
,int dst   /* the destination*/ ) {
  unsigned char *op = static_cast<unsigned char *>(src);
  switch (*op++) {
  case 0xE8:  // CALL offset (dword)
  case 0xE9:  // JMP offset (dword)
    {
      int *offset = reinterpret<int*>(op);
      *offset = dst – reinterpret<int>(src) - sizeof(*op)*1 – sizeof(int);
    }
    break;
  case 0xEB:   // JMP offset (byte)
    ...
    break;
  case ...:
    ...
    break;
  default :
    assert(!”not complete!”);
  }
}
int GetTransnferDST(const void *src) {
  const unsigned char *op = static_cast< const unsigned char *>(src);
  switch (*op++) {
  case 0xE8:   //CALL offset (dword)
  case 0xE9:   //JMP offset (dword)
    {
      const int *offset = reinterpret_cast<const int*>(op);
      return *offset + PointerToInt32(src) + sizeof(*op) +sizeof(int);
    }
    break;
  case 0xEB:   // JMP offset(byte)
      ...
    break;
  case ...:
      ...
    break;
  default:
    assert(!”not complete!”);
    break;
  }
  return 0;
}

更多詳細信息 見 ThunkBase.cpp 3.棧的生長在Win32平台下,棧朝著低地址生長。也就是說,當棧增加N ESP就減少N,反之亦然。我們來設計這個類

class ThisToStd
{
public:
ThisToStd(const void *Obj = 0,int memFunc = 0);
const void *Attach(const void *newObj);
int Attach(int newMemFunc);
private:
#pragma pack( push , 1) // 強制編譯器使用1字節長度對齊結構
unsigned char MOV_ECX;
const void *m_this;
unsigned char JMP;
const int m_memFunc;
#pragma pack( pop ) // 恢復對齊
};
ThisToStd:: ThisToStd(const void *Obj,int memFunc)
: MOV_ECX(0xB9),JMP(0xE9) {
 Attach(Obj); // 設置this指針
Attach(memFunc); // 設置成員函數地址(使用偏移)
}
const void* ThisToStd::Attach(const void *newObj) {
 const void *oldObj = m_this;
 m_this = newObj;
 return oldObj;
}
int ThisToStd::Attach(int newMemFunc) {
 int oldMemFunc = GetTransferDST(&JMP);
SetTransferDST(&JMP,newMemFunc);
return oldMemFunc;
}
我們以如下方式使用這個類 :typedef void ( __stdcall * fun1)(int);
class C { public : void __thiscall fun1(int){} };
C obj;
ThisToStd thunk;
thunk.Attach(&obj); // 假設 &obj = OBJ_ADD
int memFunc = PointerToInt32(&C::fun1); //假設memFunc = MF_ADD
thunk.Attach(memFunc); // thunk.m_memFunc 將被設置為MF_ADD – (&t.JMP)-5
fun1 fun = reinterpret_cast<fun1>(&thunk); //假設 &thunk = T_ADD
fun(1212); // 與 obj.fun(1212) 有同樣效果
它是如何工作的,當CPU執行到 fun(1212); 機器碼如下:PUSH 1212;
CALL DWORD PTR [fun];
0x50000 : … ; 假設 RET_ADD = 0x50000
// CALL DOWRD PTR [fun] 與CALL(0xE8) offset(dword) 不同
//我們只需要知道: 它將RET_ADD壓棧,然後跳轉到T_ADD

執行完這2條指令後,棧是這個樣子 :


1212
RET_ADD &lt;- ESP

下一條被執行的指令,是在thunk 的地址處 (T_ADD)

thunk的第1字節是 “const unsigned char MOV_ECX” –被初始化為0xB9.

緊接著的4字節是 “const void *m_this”

在 thunk.Attach(&obj); 後,m_this = OBJ_ADD

這5字節組成一條合法的指令

T_ADD : MOV ECX,OBJ_ADD

thunk的第6字節是 “const unsigned char JMP” –被初始化為0xE9.

緊接著的4字節是 “const int m_memFunc”

將被 thunk.Attach(memFunc) 修改

這5字節又組成一條合法指令

T_ADD+5 : JMP offset

offset = MF_ADD - &thunk.JMP – 5 ( 由 thunk.Attach() 和SetTransferDST 設置)

所以,這條指令執行後,下一條被執行指令將在這裡:

MF_ADD : …

現在,this指正已經准備好,(參數和返回地址也由fun(1212)准備好,而且 C::fun1 將會使用RET 4 返回到 RET_ADD,並正確的平衡堆棧。

所以,它成功了!

設計 StdToStd

讓我們由以下3步分析:

1. 調用者如何准備參數和返回地址?

一般的說,一個使用__stdcall 的普通C函數會將參數從右向左依次壓棧。我們假設它使得棧增長了 N。注意:N並不總等於參數數目×4!

CALL 指令將返回地址壓棧,使得棧再增長4

參數 m &lt;-ESP +4 +N

參數 m-1

參數 1 &lt;- ESP + 4

返回地址 &lt;- ESP

它將平衡堆棧的工作交給被調用者。(使用RET N)

2. 被調用者如何得到參數與返回地址?(它希望何種方式?)

一個和上述普通C函數具有相同參數列表,使用__stdcall的成員函數,希望參數,返回地址和this指針像這樣准備 :

參數 m &lt;- ESP + 8 + N

參數 m-1

參數 1 &lt; -ESP + 8

this &lt; -ESP +4

返回地址 &lt;-ESP

3. 被調用者如何返回?

它使用 RET N+4 返回。

所以我們的工作是在參數1和返回地址之間插入this指針,然後跳轉到成員函數。

(我們插入了一個this指針使得棧增加了4,所以被調用者使用 RET N+4 是正確的)

在設計 StdToStd 之前,讓我們定義一些有用的宏。

相信我,這將使得源代碼更加容易閱讀和改進。

MachineCodeMacro.h
#undef CONST
#undef CODE
#undef CODE_FIRST
#ifndef THUNK_MACHINE_CODE_IMPLEMENT
#define CONST const
#define CODE(type,name,value) type name;
#define CODE_FIRST(type,name,value) type name;
#else
#define CONST
#define CODE(type,name,value) ,name(value)
#define CODE_FIRST(type,name,value) :name(value)
#endif
ThunkBase.h
#include “MachineCodeMacro.h”
namespace Thunk {
	typedef unsigned char byte;
	typedef unsigend short word;
	typedef int dword;
	typedef const void* dword_ptr;
}
StdToStd.h
#include <ThunkBase.h>
#define STD_TO_STD_CODES()					\
/*	POP EAX	*/							\
CONST	CODE_FIRST(byte,POP_EAX,0x58)			\
/*	PUSH m_this	*/						\
CONST	CODE(byte,PUSH,0x68)				\
		CODE(dword_ptr,m_this,0)				\
/*	PUSH EAX	*/							\
CONST	CODE(byte,PUSH_EAX,0x50)				\
/*	JMP m_memFunc(offset)	*/				\
CONST	CODE(byte,JMP,0xE9)					\
CONST	CODE(dword,m_memFunc,0)
namespace Thunk {
	class StdToStd {
	public:
		StdToStd(const void *Obj = 0,int memFunc = 0);
		StdToStd(const StdToStd &src);
		const void* Attach(const void *newObj);
		int Attach(int newMemFunc);
	private:
#pragma pack( push ,1 )
	STD_TO_STD_CODES()
#pragma pack( pop )
};
StdToStd.cpp
#include <StdToStd.h>
#define THUNK_MACHINE_CODE_IMPLEMENT
#include <MachineCodeMacro.h>
namespace Thunk {
	StdToStd::StdToStd(dword_ptr Obj,dword memFunc)
		STD_TO_STD_CODES()
	{
		Attach(Obj);
		Attach(memFunc);
	}
	StdToStd::StdToStd(const StdToStd &src)
		STD_TO_STD_CODES()
	{
		Attach(src.m_this);
		Attach( GetTransferDST(&src.JMP) ); 
	}
	dwrod_ptr StdToStd::Attach(dword_ptr newObj) {
		dword_ptr oldObj = m_this;
		m_this = newObj;
		return oldObj;
	}
	dword StdToStd::Attach(dword newMemFunc) {
		dword oldMemFunc = GetTransferDST(&JMP);
		SetTransferDST(&JMP,newMemFunc);
		return oldMemFunc;
	}
}

宏 CONST CODE_FIRST(byte,POP_EAX,0x58) 在StdToStd.h 中,將被替換成: “const byte POP_EAX;”

(宏THUNK_MACHINE_CODE_IMPLEMENT沒有定義)

在StdToStd.cpp 中,將被替換成: “:POP_EAX(0x58)”

(宏THUNK_MACHINE_CODE_IMPLEMENT 被定義)

在StdToStd.cpp中,宏 CODE_FIRST 於CODE 的不同之處在於 CODE 被替換為 “, 某某” 而不是 “: 某某” .使得初始化列表合法。

宏(macro) STD_TO_STD_CODES() 的注釋(comment) 詳細說明了這個類是如何工作的。

設計 ThisToCdecl

讓我們還是依照那3個步驟分析:

1、當一個使用__cdecl 的普通C函數調用時,編譯器從右向左壓入參數,我們假設這使得棧增加N。CALL指令將返回地址壓棧,使得棧再增加4。

堆棧就像這樣:

參數 m &lt;- ESP + 4 + N

參數 m-1

參數 1 &lt;- ESP + 4

返回地址 &lt;- ESP

它使用 ADD ESP,N 平衡堆棧。

2、當一個和上述C普通函數有同樣參數列表,使用__thiscall 的成員函數將要被調用時,它希望參數已經被從右向左壓入,而且ECX保存著this指針。

參數 m &lt;- ESP + 4 + N

參數 m-1

參數 1 &lt;- ESP + 4

返回地址 &lt;- ESP

ECX : this

3、當被調用者返回

它使用 RET N !

所以,我們的工作如下:

在調用成員函數之前,將this指針放入ECX

在成員函數返回後,將ESP設置成一個正確的值

返回到調用者。所以,這個正確的值應該是當調用者執行完ADD ESP,N之後,ESP剛好是被調用者調用前的值。

因為參數數量×4不總是等於N,所以我們不能使用SUB ESP,N來設置ESP(比如參數列表含有double)

我們也不能修改返回地址,使它跨過“ADD ESP,N”的指令,因為這條指令並不總是緊接著CALL指令(調用caller 的CALL指令)

(比如 返回類型是double的情況)

一個可能的實現是在某個地方保存ESP,在被調用者返回後將它傳送回ESP。

讓我們來看看第1個實現:

ThisToCdecl 36.h
#define __THIS_TO__CDECL_CODES()					\
/*	MOV DWORD PTR [old_esp],ESP	*/				\
CONST	CODE_FIRST(word,MOV_ESP_TO,0x2589)		\
CONST	CODE(dword_ptr,pold_esp,&old_esp)			\
											\
/*	POP	ECX	*/								\
CONST	CODE(byte,POP_ECX,0x59)					\
											\
/*	MOV DWORD PTR [old_return],ECX	*/			\
CONST	CODE(word,MOV_POLD_R,0x0D89)			\
CONST	CODE(dword_ptr,p_old_return,&old_return)		\
											\
/*	MOV ECX,this	*/							\
CONST	CODE(byte,MOV_ECX,0xB9)					\
		CODE(dword_ptr,m_this,0)					\
											\
/*	CALL memFunc	*/							\
CONST	CODE(byte,CALL,0xE8)						\
		CODE(dword,m_memFunc,0)				\
											\
/*	MOV ESP,old_esp	*/						\
CONST	CODE(byte,MOV_ESP,0xBC)					\
CONST	CODE(dword,old_esp,0)					\
/*	MOV DWORD PTR [ESP],old_retrun	*/			\
CONST	CODE(word,MOV_P,0x04C7)					\
CONST	CODE(byte,_ESP,0x24)						\
CONST	CODE(dword,old_return,0)					\
/*	RET	*/									\
CONST	CODE(byte,RET,0xC3)

1、我們將ESP保存到old_esp中。

2、然後,彈出返回地址(返回到調用者的地址),並將其保存到old_return 中,

3、在ECX中准備好this指針。

4、調用成員函數(我們彈出調用者的返回地址,而CALL指令會壓入一個新的返回地址——棧現在適合被調用者。被調用者將返回到thunk 代碼的剩下部分。)

5、恢復ESP和返回地址,然後返回調用者

優化

sizeof(ThisToCdecl)==36 , 我認為這是不可接受的。

如果我們使用PUSH old_return 來代替 MOV DWORD PTR[ESP],old_return,可以節省2字節(因此,我們必須在保存old_esp之前彈棧),於此同時,也增加了一個額外的堆棧操作。(見 ThisToCdecl 34.h)

在這種情況下,相對於時間上的優化,我更加傾向空間上的優化。所以第3個實現如下:

我們可以使用一個叫做Hook的函數來准備this指針,保存old_esp和返回地址,設置被調用者的返回地址,然後跳轉到被調用者。這樣,thunk對象將包含更少的指令,而變的更小。(23字節)

ThisToCdecl.h
#define THIS_TO_CDECL_CODES()				\
/*	CALL Hook	*/						\
CONST	CODE_FIRST(byte,CALL,0xE8)		\
CONST	CODE(dword,HOOK,0)				\
									\
/*	this and member function	*/			\
		CODE(dword,m_memFunc,0)		\
		CODE(dword_ptr,m_this,0)			\
									\
/*	member function return here!	*/		\
/*	MOV ESP,oldESP	*/					\
CONST	CODE(byte,MOV_ESP,0xBC)			\
CONST	CODE(dword,oldESP,0)				\
									\
/*	JMP oldRet	*/						\
CONST	CODE(byte,JMP,0xE9)				\
CONST	CODE(dword,oldRet,0)

這些機器碼首先調用“Hook”函數,這個函數做如下工作:

1. 保存 the oldESP 和 oldRet。

2. 將被調用者的返回地址設置到 “member function return here!”。

3. 將ECX設置為this指針。

4. 跳轉到成員函數

當成員函數返回後,剩下的thunk代碼將修改ESP然後返回到調用者。

Hook函數被實現為:void __declspec( naked ) ThisToCdecl::Hook() {
  _asm {
    POP EAX      //1
    // p=&m_memFunc; &m_this=p+4; &oldESP=p+9; &oldRet=p+14

    // Save ESP
    MOV DWORD PTR [EAX+9],ESP  //3
    ADD DWORD PTR [EAX+9],4    //4
    // Save CallerReturn(by offset)
    //src=&JMP=p+13,dst=CallerReturn,offset=CallerReturn-p-13-5
    MOV ECX,DWORD PTR [ESP]    //3
    SUB ECX,EAX          //2
    SUB ECX,18          //3
    MOV DWORD PTR [EAX+14],ECX  //3
    // Set CalleeReturn
    MOV DWORD PTR [ESP],EAX    //3
    ADD DWORD PTR [ESP],8    //4
    // Set m_this
    MOV ECX,DWORD PTR [EAX+4]  //3
    // Jump to m_memFunc
    JMP DWORD PTR[EAX ]      //2
  }
}

我們使用 CALL offset(dword) 跳轉到Hook,這個指令會將返回地址壓棧。所以,CALL HOOK之後,堆棧如下 :

參數 m

參數m-1

參數1

調用者返回地址

Hook返回地址 &lt;- ESP

Hook 返回地址剛好是緊接著“CALL HOOK”的指令,—— &m_memFunc

Hook 使用 __declspec( naked ) 強制編譯器不生成額外指令。(兼容性:VC8支持。VC6,7不確定,g++不支持)

第1條指令POP EAX 將使堆棧減少4並且得到thunk對象的地址。

參數1

調用者返回地址 &lt;- ESP

EAX : p //p=&m_method; &m_this=p+4; &oldESP=p+9; &oldRet=p+14

現在,還有3件事情值得我們注意:

1. thunk對象使用 CALL(0xE8)轉移到 Hook。這是一個相對轉移

2. thunk對象使用 JMP offset 跳轉到調用者,offset將被Hook計算。

3. Hook 使用 JMP DWORD PTR[EAX],這是一個絕對跳轉,所以m_memFunc不能使用 SetTransferDST,m_memFunc = PointerToInt32(&C::Fun); 才是正確的。

更詳細實現見 ThisToCdecl.h 和 ThisToCdecl.cpp

設計 CdeclToCdecl

1、使用__cdecl 的普通C函數前面已經討論過

2、一個使用__cdecl 的成員函數希望棧像這個樣子:

參數 m &lt;-ESP + 8 + N

參數m-1

參數1 &lt;-ESP + 8

this &lt;-ESP + 4

返回地址 &lt;- ESP

3、使用__cdecl 的成員函數使用 RET 返回

CdeclToCdecl類與ThisToCdecl十分相似:

thunk對象調用一個 Hook函數來准備this指針,保存old_esp,返回地址,然後跳轉到被調用者。

被調用者返回之後,thunk代碼修改ESP,然後跳轉到調用者。

不同之處在Hook函數,它將this指針插入到參數1與返回值之間,而不是將它傳送到ECX。

更詳細的實現見 CdeclToCdecl.h 和CdeclToCdecl.cpp

設計 StdToCdecl

讓我們拿它和CdeclToCdecl做比較。

唯一不同的是,成員函數使用RET N+4而不是 RET。

當被調用者返回後,不管是RET N+4,還是RET,ESP都將被恢復。

因此,CdeclToCdecl可以勝任StdToCdecl

所以,StdToCdecl 只是一個 typedef “typedef CdeclToCdecl StdToCdecl;” ^_^

設計 CdeclToStd

使用__stdcall 的調用者將堆棧平衡工作交給被調用者。

使用__cdecl 的被調用者使用RET返回到調用者。

而關於ESP的信息在這之中丟失了!

非常不幸,我沒辦法設計出一個通用的thunk類。 -_-

關於 __fastcall 和更進一步的工作

__fastcall調用約定將小於或等於dword的頭2個參數用ECX和EDX傳遞。

所以設計出一個通用的thunk類似乎是不可能的。(因為和參數相關)

但是特殊的解決方案是存在的。

我認為Thunk的理論比實現更重要。

在你打算解決一個特定的問題 (比如為了特定參數的 __fastcall 和 CdeclToStd ),在另一平台上實現,或者想繼續優化這份實現的時候,如果這篇文章能對你有所幫助,我非常高興 ^_^

源代碼可以任意使用,作者不會為此承擔任何責任 ^_^。

關於FlushInstructionCache

這些類通常是按如下方式被使用:class CNeedCallback {
private:
CThunk m_thunk;
public:
CNeedCallback() :m_thunk(this,Thunk::Helper::PointerToInt32(&CNeedCallback::Callback)) {}
private:
returnType Callback(….) {}
}

所以,每個thunk對象的Obj和Method屬性在構造後就不再改變。我不知道在這種情況下FlushInstructionCache是否有必要。如果你認為有,請在 ThunkBase.cpp中定義 THUNK_FLUSHINSTRUCTIONCACHE ,或者簡單的去掉第4行注釋。

特別感謝

Illidan_Ne 和Sean Ewington ^_^.

本文配套源碼

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