65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.78/5 (8投票s)

2008年4月13日

CPOL

12分钟阅读

viewsIcon

34618

downloadIcon

432

利用Thunk技术,将成员函数转换为回调函数的简单通用解决方案。

引言

本文介绍了一种使用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)*1sizeof(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 ^\_^。

© . All rights reserved.