通用 Thunk 具有 5 种调用约定组合






2.78/5 (8投票s)
利用Thunk技术,将成员函数转换为回调函数的简单通用解决方案。
-
"GenericThunks/GenericThunk_source.zip">Download GenericThunk_source - 15.17 KB
- 下载 GenericThunk_source_sample - 31.54 KB
- 下载 GenericThunk_source_sample_executable - 456.79 KB
引言
本文介绍了一种使用Thunk技术将成员函数转换为回调函数的通用方法。
主要讨论理论。同时也提供了实现和示例。
背景
许多库需要我们提供一个函数作为回调,这使得面向对象变得困难。
这是因为成员函数(非静态)需要纯C函数没有的“this”指针。
Thunk技术是一种快速(但依赖平台)的解决此问题的方法。
我最近阅读了许多关于Thunk技术的文章,我认为其中许多解决方案是针对特定问题的。
我设计了一套Thunk类来提供一个通用的解决方案。
环境
开发环境:IA32, Windows XP SP2, Visual Studio 2005。
用法
有5个(实际上是4个)类(都在Thunk命名空间下)。
这些类的每个对象都有两个属性:**Obj** 和 **Method**(非静态)。
它们可以动态生成一些机器码。
执行这些机器码在逻辑上等同于调用 **Obj.Method**(...);
例如,如果我们想设计一个用于子类化工作的类,可以按照以下**5个步骤**进行:
class CSubClassing {
private:
Thunk::ThisToStd m_thunk;
//ONE.choose a correct Thunk class.
/* this ThisToStd class makes a __thiscall method (LRESULT SubProc(…) )
become a __stdcall callback function. (Win32 WNDPROC) */
//TWO.Instantiate a thunk object.
public:
CSubClassing() {
m_thunk.Attach(this);
//THREE.attach the object
m_thunk.AttachMethod(&CSubClassing::SubProc);
//FOUR.attach the method
// to do
}
void Attach(HWND hWnd) {
m_oldProc = (WNDPROC)SetWindowLong(hWnd,GWL_PROC
,m_thunk.MakeCallback<LONG>());
//FIVE.convert it to callback function
/* SetWindowLong function specifys WNDPROC by LONG value */
// to do
}
private:
// this non-static member function now will be callbacked by 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个类都具有相同的接口和用法。
一旦您**根据成员函数和回调函数的调用约定**选择一个Thunk类,您就可以借助它完成一些有用的事情,例如:WNDPROC、THREADPROC、挂钩等。
请参阅**Thunk.h**、**示例项目**以获取更多详细信息。
示例项目包含5个程序的源代码。
没有提供可执行文件,因为它们太大了。
该项目可以使用Microsoft Visual Studio 2005正常编译,无需更改目录结构。
这5个程序共享一个测试代码——**TestClass.h**、**TestClass.cpp**和**main.cpp**。
区别在于**预处理器**的定义。
然后,它们分别测试**ThisToStd**、**ThisToCdecl**、**StdToStd**、**StdToCdecl**和**CdeclToCdecl**。
此外,您还可以获取关于在使用Thunk类时需要包含和添加到项目中的文件的信息。
(包含Thunk.h并将Thunk.cpp添加到项目中可以使用,但不是最佳方式。)
理论
关于理论,我们应该知道的最重要的事情是**调用约定**——调用者和被调用者之间的约定。
纯C函数通常使用以下三种调用约定之一:
"**__cdecl**"、"**__stdcall**" 或 "**__fastcall**"。
而成员函数通常使用:
"**__thiscall**"、"**__stdcall**" 或 "__cdecl"。
我们必须关注以下**3个要点**:
1.调用者(纯C函数)如何准备参数和返回地址?
2.对于被调用者(成员函数),期望和需要的参数和返回地址是什么?被调用者如何获取它们?
3.谁负责平衡堆栈?
调用者准备的参数和返回地址与被调用者期望的**不总是相同**,因为需要“this”指针。并且堆栈平衡的方式也可能不同。
我们的工作是在被调用者期望的正确位置准备“this”指针,并弥补堆栈平衡的差异。
为了简单起见,我们以"**void func(int);**"和"**void C::func(int);**"作为示例。
**首先**,让我们看看当func使用**__stdcall**约定调用时会发生什么。
func(1212); the compiler prepares the arguments and return address like this:
PUSH 1212 ; lead stack increases by 4
CALL func ; lead stack increases by 4,too (because the return address is pushed)
0x50000 :... ; the callee return here. And we suppose the address here is 0x50000
调用者期望被调用者使用**RET 4**(堆栈向下减少8:4用于参数1212,另外4用于返回地址0x50000)来平衡堆栈,因此没有额外的机器码。
所以,在此之后,堆栈如下:
....
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);**)所期望的。
**第三**,让我们看看被调用者是如何返回的。
实际上,它会使用**RET 4**返回到0x50000。
因此,我们的工作只是准备“this”指针,然后跳转到成员函数。
(无需额外工作,参数和返回地址都在正确位置,堆栈也会正确平衡。)
设计ThisToStd
在我们设计第一个也是最简单的类——**ThisToStd**之前,还需要另外三种信息。
1.我们需要一种方法来获取函数的地址。
与可以转换为int值的函数指针不同,
void *p = &someValue;
int address = reinterpret_cast<int>(p);
/* a warning if checking the portability for 64-bit machine
it can be ignored because this thunk is only used on 32-bit machine ^_^ */
函数指针由于限制更多而不能。
void __stdcall fun(int) { ... }
void C::fun(int) {}
//int address = (int)fun; // not allow!
//int address = (int)&C::fun; // error,too
有两种方法可以进行强大的转换:
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); // works!
int address = (int)&C::fun; // works,too!
有关更多详细信息,请参阅**ThunkBase.h**。
2.跳转指令的目标。
许多跳转指令的目标由**偏移量**指定,从源地址算起。
例如:
当CPU执行地址0xFF000000处的指令时,指令如下:
0xFF000000 : 0xE9 0x33 0x55 0x77 0x99
0xFF000005 : ...;
**0xE9**是JMP指令,接下来的4个字节将被解释为**偏移量**。
offset = 0x99775533 (在Intel x86上,低字节存储在低地址) = -1720232653
source (src) = 0xFF000000 (JMP指令的地址) = 4278190080
destination (dst) = src+offset+5 (1字节,JMP,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) /* this will force the compiler to align following structure with 1 byte size */
unsigned char MOV_ECX;
const void *m_this;
unsigned char JMP;
const int m_memFunc;
#pragma pack( pop , 1) // restore the alignment
};
ThisToStd:: ThisToStd(const void *Obj,int memFunc)
: MOV_ECX(0xB9),JMP(0xE9) {
Attach(Obj); // set this pointer
Attach(memFunc); // set member function address (by offset)
}
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); // suppose &obj = OBJ_ADD
int memFunc = PointerToInt32(&C::fun1); // suppose memFunc = MF_ADD
thunk.Attach(memFunc); /* thunk.m_memFunc will set to MF_ADD – (&t.JMP)-5 */
fun1 fun = reinterpret_cast<fun1>(&thunk); // suppose &thunk = T_ADD
fun(1212); // the same as obj.fun(1212);
工作原理:
当CPU执行fun(1212)时,机器码是:
PUSH 1212;
CALL DWORD PTR [fun];
0x50000 : ... ; suppose RET_ADD = 0x50000
// CALL DOWRD PTR [fun] different to CALL(0xE8) offset(dword)
// the only thing we need to know is : it push RET_ADD and JMP to T_ADD
在这2条指令之后,堆栈将是:
....
1212
RET_ADD <- ESP
下一个要执行的指令的地址是thunk(**T_ADD**)的地址。
thunk的第一个字节是“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。
Arg m <- ESP + 4 + N
Arg m-1
…
Arg 1 <- ESP + 4
Return Address <- ESP
它将堆栈平衡的工作交给被调用者(使用**RET N**)。
**2**。被调用者如何获取参数和返回地址?(期望是什么?)
一个使用__stdcall的成员函数(**具有相同的参数列表**)期望参数、返回地址和this指针如下:
Arg m <- ESP + 8 + N
Arg m-1
…
Arg 1 <- ESP + 8
this <- ESP + 4
Return Address <- ESP
**3**。被调用者如何返回?
它使用**RET N+4**返回。
因此,我们的工作是**插入**this指针到Arg1和返回地址之间,然后跳转到成员函数。
(我们插入一个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在此文件中定义)
CODE_FIRST和CODE的区别在于StdToStd.cpp。
CODE被替换为“,something”而不是“:something”,因此初始化列表是合法的。
**STD_TO_STD_CODES()**的注释解释了它是如何工作的。
有关更多详细信息,请参阅**StdToStd.h**和**StdToStd.cpp**。
设计ThisToCdecl
让我们用以下3个步骤进行分析:
**1**。当一个纯C函数(带__cdecl)调用时。
编译器会**从右到左**推送参数,导致堆栈增加**N**。
CALL指令会推送返回地址,并使堆栈再次增加4。
堆栈将如下所示:
…
Arg m <- ESP + 4 + N
Arg m-1
…
Arg 1 <- ESP + 4
Return Address <- ESP
它使用**ADD ESP,N**来平衡堆栈。
**2**。当一个成员函数(具有与__thiscall相同的参数列表)即将被调用时。
它期望参数从右到左推送,并且ECX存储this指针。
Arg m <- ESP + 4 + N
Arg m-1
…
Arg 1 <- ESP + 4
Return Address <- ESP
ECX : this
**3**。当被调用者返回时。
它使用RET N!
那么,我们的工作是:
**1**。在调用成员函数之前,将this指针存储在ECX中。
**2**。在成员函数返回后,将ESP设置为正确的值。
**3**。返回给调用者。因此,正确的值应该等于调用者执行ADD ESP,N之前调用者的旧EPS值。
/*
dword oldESP = ESP;
... ; prerare arguments
CALL ThunkAddress
...;
ADD ESP,N
assert(ESP==oldESP);
*/
参数的数量×4不总是等于N,所以我们不能使用SUB ESP,N来设置ESP值。
(例如,参数列表包含double)
我们不能修改返回地址,让它跨越“ADD ESP,N”指令,因为这条指令不总是后面跟着CALL(调用调用者)。
(例如,返回类型是double)
一个可能的实现是将其保存到某个地方,然后在被调用者返回后将其移动到ESP。
让我们看看第一个实现。
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)
首先,我们将ESP保存到一个值old_esp。
其次,弹出返回地址(返回给调用者),保存到一个值old_return。
第三,在ECX中准备this指针。
第四,调用成员函数(我们弹出调用者的返回地址,CALL会推送一个返回地址——堆栈适合被调用者。被调用者将返回到thunk代码的其余部分)。
第五,恢复ESP,返回地址,并返回给调用者。
优化
sizeof(ThisToCdecl)==36,我认为这是不可接受的。
如果我们使用**PUSH old_return**而不是**MOV DWORD PTR[ESP],old_return**,可以节省2个字节(因此,我们必须在保存old_esp之前POP),并且增加了一个堆栈操作。
(参见**ThisToCdecl 34.h**)
在这种情况下,我更倾向于空间优化而不是时间优化。那么,第三个实现是:
我们可以使用一个名为**Hook**的函数来准备this指针,保存old_esp和old_return,设置被调用者的返回地址并跳转到被调用者。
这样,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**”的函数,Hook函数执行以下工作:
**1**。保存oldESP和oldRet。
**2**。将被调用者的返回地址设置为“**此处成员函数返回!**”。
**3**。设置ECX为this。
**4**。JMP到成员函数。
被调用者返回后,其余的thunk代码修改ESP并返回给调用者。
Hook函数实现如下:
void __declspec( naked ) ThisToCdecl::Hook() { _asm { POP EAX // p=&m_memFunc; &m_this=p+4; &oldESP=p+9; &oldRet=p+14 // Save ESP MOV DWORD PTR [EAX+9],ESP ADD DWORD PTR [EAX+9],4 // Save CallerReturn(by offset) //src=&JMP=p+13,dst=CallerReturn,offset=CallerReturn-p-13-5 MOV ECX,DWORD PTR [ESP] SUB ECX,EAX SUB ECX,18 MOV DWORD PTR [EAX+14],ECX // Set CalleeReturn MOV DWORD PTR [ESP],EAX ADD DWORD PTR [ESP],8 // Set m_this MOV ECX,DWORD PTR [EAX+4] // Jump to m_memFunc JMP DWORD PTR[EAX] } }
我们使用CALL offset(dword)跳转到Hook,这条指令会推送返回地址。
因此,CALL HOOK后的堆栈如下:
…
Arg m
Arg m-1
…
Arg1
caller Return Address
Hook Return Address <- ESP
;//Hook Return Address is just followed by instruction “CALL HOOK” — &m_method
Hook使用**\_\_declspec( naked )**强制编译器不生成额外的指令。
(兼容性:VC8支持。VC6、7不确定。g++不支持)
第一条指令POP EAX会使堆栈减少4,并获取thunk对象的
地址(指向成员函数“m_memFunc”的偏移量)。
…
Arg1
caller Return Address <- ESP
EAX : p //p=&m_memFunc; &m_this=p+4; &oldESP=p+9; &oldRet=p+14
现在还有三件事应该注意:
**1**。thunk对象使用CALL(**0xE8**)进行跳转。这是一个相对跳转。
偏移量可以通过SetTransferDST(&CALL,&Hook)计算。
**2**。thunk对象使用JMP offset跳转到调用者,偏移量由Hook计算。
**3**。Hook使用**JMP DOWRD PTR [EAX]**,这是一个绝对跳转。
因此,m_memFunc不应使用SetTransferDST,m_memFunc = PointerToInt32(&C::Fun);是正确的。
有关更多详细信息,请参阅**ThisToCdecl.h**和**ThisToCdecl.cpp**。
设计CdeclToCdecl
**1**。我们已经讨论过带__cdecl的纯C函数。
**2**。带__cdecl的成员函数期望堆栈如下:
…
Arg m <- ESP + 8 + N
Arg m-1
…
Arg 1 <- ESP + 8
this <- ESP + 4
Return Address <- ESP
**3**。带__cdecl的成员函数使用**RET**返回。
CdeclToCdecl类几乎与ThisToCdecl类相同。
thunk对象调用一个**Hook**函数来准备this指针,保存old_esp和old_return,然后跳转到被调用者。
被调用者返回后,thunk对象修改ESP并跳转到调用者。
区别在于Hook函数。
它在Arg1和返回地址之间**插入**this指针,而不是将this指针移动到ECX。
有关更多详细信息,请参阅**CdeclToCdecl.h**和**CdeclToCdecl.cpp**。
设计StdToCdecl
让我们将其与**CdeclToCdecl**进行比较。
唯一的区别是成员函数使用**RET N+4**而不是**RET**。
在被调用者返回到thunk对象后,无论使用RET N+4还是RET,ESP都会恢复。
因此,CdeclToCdecl可以胜任StdToCdecl。
所以,StdToCdecl仅仅是一个**typedef**:“typedef CdeclToCdecl StdToCdecl;” ^_^
设计CdeclToStd
带__stdcall的调用者将堆栈平衡的工作交给被调用者。
带__cdecl的被调用者使用RET返回给调用者。
ESP的信息丢失了!
不幸的是,我对如何设计一个通用的thunk类没有想法。-_-
关于__fastcall和未来工作
__fastcall调用约定将前两个DWORD或更小参数传递给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**中#define **THUNK_FLUSHINSTRUCTIONCACHE**,或者直接删除第4行的注释。
特别感谢
特别感谢Illidan\_Ne和Sean Ewington ^\_^。