本文同时发表在 codeproject 网站:参见:Generic Thunk with 5 combinations of Calling Conventions
介绍
这篇文章提出了一种基于Thunk技术,让一个成员函数成为一个回调函数的通用方法。文章主要讨论原理,同时也提供了一份实现和示例。
背景
许多库需要我们提供一个函数作为回调,这使得使用 “面向对象编程”(OOP) 出现了麻烦。因为普通的C函数没有成员函数需要的this指针。Thunk技术是一种快速但是平台相关的解决此问题的方法。我最近研究过许多有关thunk技术的文章,我认为许多解决方案都是针对于特定问题的。我设计了一组类,来提供一种通用的解决方案。
环境
开发环境 : IA32,Windows Xp SP2,Visual Studio 2005
用法
源代码提供了5(实际上4)个类(全都在 Thunk 名字空间中)。它们的每一个对象都有2个属性,对象和方法。它们可以动态的创建一些机器码。执行这些机器码将在逻辑上和调用 Obj.Method(…); 举例来说,如果我们想要设计一个类来进行窗口子类化的工作,我们可以按下面5个步骤使用通用Thunk
01.
class
CSubClassing {
02.
private
:
03.
Thunk::ThisToStd m_thunk;
04.
//1.选择一个合适的Thunk类
05.
// ThisToStd 类使一个使用__thiscall 约定的成员函数 (LRESULT SubProc(…) )
06.
//成为一个使用_stdcall 约定的回调函数WNDPROC)
07.
//2.实例化一个对象.
08.
09.
public
:
10.
CSubClassing() {
11.
m_thunk.Attach(
this
);
12.
//3.附加到想要回调的对象上
13.
m_thunk.AttachMethod(&CSubClassing::SubProc);
14.
// 4.附加成员函数
15.
// to do
16.
}
17.
void
Attach(
HWND
hWnd) {
18.
m_oldProc = (WNDPROC)SetWindowLong(hWnd,GWL_PROC
19.
,m_thunk.MakeCallback());
20.
// 5.转化到回调函数指针
21.
//SetWindowLong函数使用一个LONG值来表示WNDPROC
22.
// to do
23.
}
24.
private
:
25.
//这个非静态成员函数将被Windows回调
26.
LRESULT
SubProc(
HWND
hWnd,
UINT
msg,
WPARAM
wParam,
LPARAM
lParam) {
27.
if
(msg!=WE_NEEDED)
28.
return
CallWndProc(m_oldProc,hWnd,msg,wParam,lParam);
29.
// to do
30.
}
31.
WNDPROC m_oldProc;
32.
}
这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点:
1.调用者(普通C函数)怎么准备参数和返回地址?
2.被调用者(成员函数)希望并且要求的参数和返回地址是什么?它如何取得它们?
3.平衡堆栈是谁的责任?
调用者准备的参数和返回地址总不是被调用者所期待的那样,因为被调用者还需要一个this指针。平衡堆栈的方式也许也会不同。我们的工作就是以被调用者期望的方式,准备好this指针,同时弥补2者在平衡堆栈上的差异。
为了简单起见,我们以 “ void func(int); void C::func(int); ”为例,首先,我们来看看当使用__stdcall 约定的func被调用的时候,会发生什么。
1.
func(1212); 编译器会像这样准备参数和返回地址 :
2.
PUSH 1212 ; 使得堆栈增加4
3.
CALL func; 使得堆栈也增加4(因为返回地址也被压入堆栈)
4.
0x50000:...;被调用者返回这里,我们假设这里的地址是0x50000
调用者希望被调用者使用 RET 4 (使得堆栈减少8:参数1212使用4,返回地址0x50000也使用4)来平衡堆栈,所以在这之后没有多余的机器码。所以,在这之后,堆栈是这个样子:
1.
...
2.
1212
3.
0x50000 <- ESP
然后,我们来看看使用__thiscall 的被调用者所希望的参数和返回地址。一个真正的成员函数被调用时。
1.
C obj;
2.
obj.func(1212);
编译器以这样的方式准备参数:
1.
PUSH 1212;
2.
MOV ECX,obj;
3.
CALL C::func
所以,在这之后,堆栈是这个样子:
1.
…
2.
1212
3.
0x50000 <- ESP
ECX 保存着 this 指针。
这也就是被调用者(void __thiscall C::func(int); ) 需要的形式。
第3,我们看看被调用者如何返回。
事实上,它使用 RET 4 来返回到0x50000
所以,我们唯一需要做的就是准备好this指针,然后跳转到成员函数。(不需要更多的工作,参数和返回值已在正确位置,堆栈也将被正确的平衡。)
设计 ThisToStd
在我们设计第1个,也是最简单的类 ThisToStd 之前,我们还需要3种信息。
1、我们需要一种得到函数地址的方法。
对于数据指针,我们可以转化(cast)它到一个 int 值
1.
void
*p = &someValue;
2.
int
address =
reinterpret_cast
(p);
3.
/* 如果检查对64位机的可移植性,将会得到一个警告。不过可以忽略它,因为这个thunk只用在32位机上^_^*/
不同于数据指针,函数指针有更多的限制。
1.
void
__stdcall fun(
int
) { … }
2.
void
C::fun(
int
) {}
3.
4.
//int address = (int)fun; // 不允许
5.
//int address = (int)&C::fun; // 同样错误
有2种方法来进行一个强力的转化
01.
template
<
typename
dst_type,
typename
src_type >
02.
dst_type pointer_cast(src_type src) {
03.
return
*
static_cast
(
static_cast
(&src) );
04.
}
05.
06.
template
07.
dst_type union_cast(src_type src) {
08.
union
{
09.
src_type src;
10.
dst_type dst;
11.
} u = {src};
12.
return
u.dst;
13.
}
所以,我们可以实现一个方法
1.
template
<
typename
Pointer >
2.
int
PointerToInt32(Pointer pointer)
3.
{
4.
return
pointer_cast(pointer);
// or union_cast(pointer);
5.
}
6.
7.
int
address = PointerToInt32(&fun);
// 可以
8.
int
address = (
int
)&C::fun;
// 也可以
更多详细信息见 ThunkBase.h
2.转移指令的目的地
许多转移指令的目的地使用“到源的偏移量”来表示
比如:当CPU 执行到0xFF000000 处的指令时, 指令像这个样子:
1.
0xFF000000 : 0xE9 0x33 0x55 0x77 0x99
2.
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个方法:
01.
void
SetTransterDST(
02.
void
*src
/* the address of transfer instruction*/
03.
,
int
dst
/* the destination*/
) {
04.
unsigned
char
*op =
static_cast
(src);
05.
switch
(*op++) {
06.
case
0xE8:
// CALL offset (dword)
07.
case
0xE9:
// JMP offset (dword)
08.
{
09.
int
*offset = reinterpret(op);
10.
*offset = dst – reinterpret(src) -
sizeof
(*op)*1 –
sizeof
(
int
);
11.
}
12.
break
;
13.
case
0xEB:
// JMP offset (byte)
14.
...
15.
break
;
16.
case
...:
17.
...
18.
break
;
19.
default
:
20.
assert
(!”not complete!”);
21.
}
22.
}
23.
24.
int
GetTransnferDST(
const
void
*src) {
25.
const
unsigned
char
*op =
static_cast
<
const
unsigned
char
*>(src);
26.
switch
(*op++) {
27.
case
0xE8:
//CALL offset (dword)
28.
case
0xE9:
//JMP offset (dword)
29.
{
30.
const
int
*offset =
reinterpret_cast
(op);
31.
return
*offset + PointerToInt32(src) +
sizeof
(*op) +
sizeof
(
int
);
32.
}
33.
break
;
34.
case
0xEB:
// JMP offset(byte)
35.
...
36.
break
;
37.
case
...:
38.
...
39.
break
;
40.
default
:
41.
assert
(!”not complete!”);
42.
break
;
43.
}
44.
return
0;
45.
}
更多详细信息 见 ThunkBase.cpp 3.栈的生长在Win32平台下,栈朝着低地址生长。也就是说,当栈增加N ESP就减少N,反之亦我们来设计这个类
01.
class
ThisToStd
02.
{
03.
public
:
04.
ThisToStd(
const
void
*Obj = 0,
int
memFunc = 0);
05.
const
void
*Attach(
const
void
*newObj);
06.
int
Attach(
int
newMemFunc);
07.
08.
private
:
09.
10.
#pragma pack( push , 1) // 强制编译器使用1字节长度对齐结构
11.
unsigned
char
MOV_ECX;
12.
const
void
*m_this;
13.
unsigned
char
JMP;
14.
const
int
m_memFunc;
15.
#pragma pack( pop ) // 恢复对齐
16.
};
17.
18.
ThisToStd:: ThisToStd(
const
void
*Obj,
int
memFunc)
19.
: MOV_ECX(0xB9),JMP(0xE9) {
20.
Attach(Obj);
// 设置this指针
21.
Attach(memFunc);
// 设置成员函数地址(使用偏移)
22.
}
23.
24.
const
void
* ThisToStd::Attach(
const
void
*newObj) {
25.
const
void
*oldObj = m_this;
26.
m_this = newObj;
27.
return
oldObj;
28.
}
29.
30.
int
ThisToStd::Attach(
int
newMemFunc) {
31.
int
oldMemFunc = GetTransferDST(&JMP);
32.
SetTransferDST(&JMP,newMemFunc);
33.
return
oldMemFunc;
34.
}
我们以如下方式使用这个类 :
01.
typedef
void
( __stdcall * fun1)(
int
);
02.
class
C {
public
:
void
__thiscall fun1(
int
){} };
03.
04.
C obj;
05.
ThisToStd thunk;
06.
thunk.Attach(&obj);
// 假设 &obj = OBJ_ADD
07.
int
memFunc = PointerToInt32(&C::fun1);
//假设memFunc = MF_ADD
08.
thunk.Attach(memFunc);
// thunk.m_memFunc 将被设置为MF_ADD – (&t.JMP)-5
09.
fun1 fun =
reinterpret_cast
< fun1 >(&thunk);
//假设 &thunk = T_ADD
10.
fun(1212);
// 与 obj.fun(1212) 有同样效果
它是如何工作的,当CPU执行到 fun(1212); 机器码如下:
1.
PUSH 1212;
2.
CALL
DWORD
PTR [fun];
3.
0x50000 : … ; 假设 RET_ADD = 0x50000
4.
// CALL DOWRD PTR [fun] 与CALL(0xE8) offset(dword) 不同
5.
//我们只需要知道: 它将RET_ADD压栈,然后跳转到T_ADD
执行完这2条指令后,栈是这个样子 :
1.
…
2.
1212
3.
RET_ADD <- 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 <-ESP +4 +N
参数 m-1
…
参数 1 <- ESP + 4
返回地址 <- ESP
它将平衡堆栈的工作交给被调用者。(使用RET N)
2. 被调用者如何得到参数与返回地址?(它希望何种方式?)
一个和上述普通C函数具有相同参数列表,使用__stdcall的成员函数,希望参数,返回地址和this指针像这样准备 :
参数 m <- ESP + 8 + N
参数 m-1
…
参数 1 < -ESP + 8
this < -ESP +4
返回地址 <-ESP
3. 被调用者如何返回?
它使用 RET N+4 返回。
所以我们的工作是在参数1和返回地址之间插入this指针,然后跳转到成员函数。
(我们插入了一个this指针使得栈增加了4,所以被调用者使用 RET N+4 是正确的)
在设计 StdToStd 之前,让我们定义一些有用的宏。
相信我,这将使得源代码更加容易阅读和改进。
01.
MachineCodeMacro.h
02.
03.
#undef CONST
04.
#undef CODE
05.
#undef CODE_FIRST
06.
07.
#ifndef THUNK_MACHINE_CODE_IMPLEMENT
08.
#define CONST const
09.
#define CODE(type,name,value) type name;
10.
#define CODE_FIRST(type,name,value) type name;
11.
12.
#else
13.
#define CONST
14.
#define CODE(type,name,value) ,name(value)
15.
#define CODE_FIRST(type,name,value) :name(value)
16.
17.
#endif
18.
19.
ThunkBase.h
20.
21.
#include “MachineCodeMacro.h”
22.
23.
namespace
Thunk {
24.
typedef
unsigned
char
byte;
25.
typedef
unsigend
short
word;
26.
typedef
int
dword;
27.
typedef
const
void
* dword_ptr;
28.
}
29.
30.
31.
StdToStd.h
32.
33.
#include < ThunkBase.h >
34.
35.
#define STD_TO_STD_CODES() \
36.
/* POP EAX */
\
37.
CONST CODE_FIRST(byte,POP_EAX,0x58) \
38.
/* PUSH m_this */
\
39.
CONST CODE(byte,PUSH,0x68) \
40.
CODE(dword_ptr,m_this,0) \
41.
/* PUSH EAX */
\
42.
CONST CODE(byte,PUSH_EAX,0x50) \
43.
/* JMP m_memFunc(offset) */
\
44.
CONST CODE(byte,JMP,0xE9) \
45.
CONST CODE(dword,m_memFunc,0)
46.
47.
namespace
Thunk {
48.
class
StdToStd {
49.
public
:
50.
StdToStd(
const
void
*Obj = 0,
int
memFunc = 0);
51.
StdToStd(
const
StdToStd &src);
52.
const
void
* Attach(
const
void
*newObj);
53.
int
Attach(
int
newMemFunc);
54.
private
:
55.
56.
#pragma pack( push ,1 )
57.
STD_TO_STD_CODES()
58.
#pragma pack( pop )
59.
};
60.
61.
StdToStd.cpp
62.
63.
#include < StdToStd.h >
64.
#define THUNK_MACHINE_CODE_IMPLEMENT
65.
#include < MachineCodeMacro.h >
66.
67.
namespace
Thunk {
68.
StdToStd::StdToStd(dword_ptr Obj,dword memFunc)
69.
STD_TO_STD_CODES()
70.
{
71.
Attach(Obj);
72.
Attach(memFunc);
73.
}
74.
StdToStd::StdToStd(
const
StdToStd &src)
75.
STD_TO_STD_CODES()
76.
{
77.
Attach(src.m_this);
78.
Attach( GetTransferDST(&src.JMP) );
79.
}
80.
dwrod_ptr StdToStd::Attach(dword_ptr newObj) {
81.
dword_ptr oldObj = m_this;
82.
m_this = newObj;
83.
return
oldObj;
84.
}
85.
dword StdToStd::Attach(dword newMemFunc) {
86.
dword oldMemFunc = GetTransferDST(&JMP);
87.
SetTransferDST(&JMP,newMemFunc);
88.
return
oldMemFunc;
89.
}
90.
}
宏 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 <- ESP + 4 + N
参数 m-1
…
参数 1 <- ESP + 4
返回地址 <- ESP
它使用 ADD ESP,N 平衡堆栈。
2、当一个和上述C普通函数有同样参数列表,使用__thiscall 的成员函数将要被调用时,它希望参数已经被从右向左压入,而且ECX保存着this指针。
…
参数 m <- ESP + 4 + N
参数 m-1
…
参数 1 <- ESP + 4
返回地址 <- ESP
ECX : this
3、当被调用者返回
它使用 RET N !
所以,我们的工作如下:
1.在调用成员函数之前,将this指针放入ECX
2.在成员函数返回后,将ESP设置成一个正确的值
3.返回到调用者。所以,这个正确的值应该是当调用者执行完ADD ESP,N之后,ESP刚好是被调用者调用前的值。
因为参数数量×4不总是等于N,所以我们不能使用SUB ESP,N来设置ESP(比如参数列表含有double)
我们也不能修改返回地址,使它跨过“ADD ESP,N”的指令,因为这条指令并不总是紧接着CALL指令(调用caller 的CALL指令)
(比如 返回类型是double的情况)
一个可能的实现是在某个地方保存ESP,在被调用者返回后将它传送回ESP。
让我们来看看第1个实现:
01.
ThisToCdecl 36.h
02.
03.
#define __THIS_TO__CDECL_CODES() \
04.
/* MOV DWORD PTR [old_esp],ESP */
\
05.
CONST CODE_FIRST(word,MOV_ESP_TO,0x2589) \
06.
CONST CODE(dword_ptr,pold_esp,&old_esp) \
07.
\
08.
/* POP ECX */
\
09.
CONST CODE(byte,POP_ECX,0x59) \
10.
\
11.
/* MOV DWORD PTR [old_return],ECX */
\
12.
CONST CODE(word,MOV_POLD_R,0x0D89) \
13.
CONST CODE(dword_ptr,p_old_return,&old_return) \
14.
\
15.
/* MOV ECX,this */
\
16.
CONST CODE(byte,MOV_ECX,0xB9) \
17.
CODE(dword_ptr,m_this,0) \
18.
\
19.
/* CALL memFunc */
\
20.
CONST CODE(byte,CALL,0xE8) \
21.
CODE(dword,m_memFunc,0) \
22.
\
23.
/* MOV ESP,old_esp */
\
24.
CONST CODE(byte,MOV_ESP,0xBC) \
25.
CONST CODE(dword,old_esp,0) \
26.
/* MOV DWORD PTR [ESP],old_retrun */
\
27.
CONST CODE(word,MOV_P,0x04C7) \
28.
CONST CODE(byte,_ESP,0x24) \
29.
CONST CODE(dword,old_return,0) \
30.
/* RET */
\
31.
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字节)
01.
ThisToCdecl.h
02.
03.
#define THIS_TO_CDECL_CODES() \
04.
/* CALL Hook */
\
05.
CONST CODE_FIRST(byte,CALL,0xE8) \
06.
CONST CODE(dword,HOOK,0) \
07.
\
08.
/* this and member function */
\
09.
CODE(dword,m_memFunc,0) \
10.
CODE(dword_ptr,m_this,0) \
11.
\
12.
/* member function return here! */
\
13.
/* MOV ESP,oldESP */
\
14.
CONST CODE(byte,MOV_ESP,0xBC) \
15.
CONST CODE(dword,oldESP,0) \
16.
\
17.
/* JMP oldRet */
\
18.
CONST CODE(byte,JMP,0xE9) \
19.
CONST CODE(dword,oldRet,0)
这些机器码首先调用“Hook”函数,这个函数做如下工作:
1. 保存 the oldESP 和 oldRet。
2. 将被调用者的返回地址设置到 “member function return here!”。
3. 将ECX设置为this指针。
4. 跳转到成员函数
当成员函数返回后,剩下的thunk代码将修改ESP然后返回到调用者。
Hook函数被实现为:
01.
void
__declspec
(
naked
) ThisToCdecl::Hook() {
02.
_asm {
03.
POP EAX
//1
04.
// p=&m_memFunc; &m_this=p+4; &oldESP=p+9; &oldRet=p+14
05.
06.
// Save ESP
07.
MOV
DWORD
PTR [EAX+9],ESP
//3
08.
ADD
DWORD
PTR [EAX+9],4
//4
09.
10.
// Save CallerReturn(by offset)
11.
//src=&JMP=p+13,dst=CallerReturn,offset=CallerReturn-p-13-5
12.
MOV ECX,
DWORD
PTR [ESP]
//3
13.
SUB ECX,EAX
//2
14.
SUB ECX,18
//3
15.
MOV
DWORD
PTR [EAX+14],ECX
//3
16.
17.
// Set CalleeReturn
18.
MOV
DWORD
PTR [ESP],EAX
//3
19.
ADD
DWORD
PTR [ESP],8
//4
20.
21.
// Set m_this
22.
MOV ECX,
DWORD
PTR [EAX+4]
//3
23.
24.
// Jump to m_memFunc
25.
JMP
DWORD
PTR[EAX ]
//2
26.
}
27.
}
我们使用 CALL offset(dword) 跳转到Hook,这个指令会将返回地址压栈。所以,CALL HOOK之后,堆栈如下 :
…
参数 m
参数m-1
…
参数1
调用者返回地址
Hook返回地址 <- ESP
Hook 返回地址刚好是紧接着“CALL HOOK”的指令,—— &m_memFunc
Hook 使用 __declspec( naked ) 强制编译器不生成额外指令。(兼容性:VC8支持。VC6,7不确定,g++不支持)
第1条指令POP EAX 将使堆栈减少4并且得到thunk对象的地址。
…
参数1
调用者返回地址 <- 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 <-ESP + 8 + N
参数m-1
…
参数1 <-ESP + 8
this <-ESP + 4
返回地址 <- 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
这些类通常是按如下方式被使用:
1.
class
CNeedCallback {
2.
private
:
3.
CThunk m_thunk;
4.
public
:
5.
CNeedCallback() :m_thunk(
this
,Thunk::Helper::PointerToInt32(&CNeedCallback::Callback)) {}
6.
private
:
7.
returnType Callback(….) {}
8.
}
所以,每个thunk对象的Obj和Method属性在构造后就不再改变。我不知道在这种情况下FlushInstructionCache是否有必要。如果你认为有,请在 ThunkBase.cpp中定义 THUNK_FLUSHINSTRUCTIONCACHE ,或者简单的去掉第4行注释。
特别感谢
Illidan_Ne 和Sean Ewington ^_^.
联系客服