想来想去还是罗嗦一下,
API只能回调全局函数,而我们有时候希望他能回调成员函数。最常用的就是Timmer和窗口回调。要实现这个需求,就会用到THUNK技术。THUNK 我查了一下是:thunk 名词 n. 铮;铛,锵 。跟这个完全没有关系嘛(看来英文太烂是坏处还是挺多的)。学习了一下之后,我理解的意思就是:狸猫换太子。替换原来意图,转调我们需要的地址。
Thunk的原理其实说起来很简单:巧妙的将数据段的几个字节的数据设为特殊的值,然后告诉系统,这几个字节的数据是代码(即将一个函数指针指向这几个字节的第一个字节),让系统来执行。
让API回调成员函数:
直接用成员函数的地址传给作为API的回调函数显然会编译出错的,原因是他们的调用规则不一致,C++编译器不允许这样做。具体可以参考:
http://hi.baidu.com/shongbee2/blog/item/7867de9744e3c26155fb9611.html
而刚好THUNK技术就是让数据段当做代码断用,如果我把回调函数地址用一个数据段给他,然后在数据段中再跳转到成员函数的地址。这样就可以间接的调用成员函数了。不错,我就是学习的这个方法。嘻嘻。。
大致方向知道了,还得了解一下细节,函数调用的规则:
建议看一下http://hi.baidu.com/shongbee2/blog/item/7867de9744e3c26155fb9611.html(也就是上一篇文章啦。)需要注意的:调用者怎么处理栈,被调用者怎么使用栈和处理栈。系统回调函数基本上都是_stdcall的调用方式,成员函数是__thiscall的调用方式。他们的区别为:
关键字
|
堆栈清除
|
参数传递
|
__stdcall
|
被调用者
|
将参数倒序压入堆栈(自右向左)
|
__thiscall
|
被调用者
|
压入堆栈,this指针保存在 ECX 寄存器中
|
发现他们唯一不同的就是__thiscall把this指针保存到了ECX的寄存器中。其他都是一样的。这种情况我们就方便了,我们只需在他调用我们的时候,我们吧this指针保存到ECX,然后跳转到期望的成员函数地址就可以了。
//我认为思路就是这样了。接下来是实现,贴源代码:
#include "stdafx.h"
#include "wtypes.h"
#include <iostream>
using namespace std;
typedef void (*FUNC)(DWORD dwThis);
typedef int (_stdcall *FUNC1)(int a, int b);
#pragma pack(push,1)
typedef struct tagTHUNK
{
BYTE bMovEcx; //MOVE ECX 将this指针移动到ECX的指令
DWORD dwThis; // this this指针的地址
BYTE bJmp; //jmp 跳转指令
DWORD dwRealProc; //proc offset 跳转偏移
void Init(DWORD proc,void* pThis)
{
bMovEcx = 0xB9; //注释见下面说明^_^
dwThis = (DWORD)pThis;
bJmp = 0xE9;
dwRealProc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(THUNK)));
FlushInstructionCache(GetCurrentProcess(),this,sizeof(THUNK));
}
}THUNK;
#pragma pack(pop)
/**************************************************************************************
void Init(DWORD proc,void* pThis)里面的说明:
0xB9 为MOVE ECX的指令, 0xE9 跳转的指令,这段初始化表示:
0013FF54 mov ecx, ptr [this]
0013FF59 jmp dwRealProc
这个单步一下便知。
下面那个API :FlushInstructionCache,查MSDN,表示刷新缓存,
因为我们修改了数据,建议他重新载入一下。
我最不能理解的是jmp的偏移是为什么是那样计算,所以这里也着重说明一下:
jmp跳转的是当前指令地址的偏移,我们参数中proc是实际函数的地址,我们需要
把他转为jmp的偏移: 实际函数地址-jmp指令地址。
实际函数地址就是proc,jmp地址就是((INT_PTR)this+sizeof(THUNK)),所以就得到
dwRealProc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(THUNK)));这行代码
还有一点,我对汇编不了解,下面是YY:为什么不是:
dwRealProc = DWORD((INT_PTR)proc - ((INT_PTR)this+sizeof(THUNK)) - sizeof(dwRealProc))
直观上看jmp地址不是:this + sizeof(bMoveEcx) + sizeof(dwThis) + sizeof(bJmp)吗?
也就是((INT_PTR)this+sizeof(THUNK)) - sizeof(dwRealProc) 啊。可是我看了一下编译的结果,
发现0013FF59 jmp dwRealProc 是一行的,也就是jmp地址实际就是:
((INT_PTR)this+sizeof(THUNK)) 这个地址。经过测试也没有问题,我就认为是这样了,不对的还
忘多指出。嘻嘻。
还有一个容易混淆的,就是我们会传入this指针,在dwRealProc里面和 FlushInstructionCache
里面都用到了this。这里要注意啦:如果你不知道传入的参数this指针和使用的这个this的话,你就该
重新复习一下C++基础了。解释一下:传入的this指针变为参数pThis,使用的this是THUNK的this。^_^
*****************************************************************************************/
template<typename dst_type,typename src_type>
dst_type pointer_cast(src_type src)
{
return *static_cast<dst_type*>( static_cast<void*>(&src) );
}
class Test
{
public:
int m_nFirst;
THUNK m_thunk;
int m_nTest;
//构造函数中初始化为3,仅为测试,以便查看外面的方法JmpedTest是否可以正确取得这个值
Test() : m_nTest(3),m_nFirst(4)
{}
void TestThunk()
{
m_thunk.Init(pointer_cast<int>(&Test::Test2),this);
FUNC1 f = (FUNC1)&m_thunk;
f(1,2);
cout << "Test::TestThunk()" << endl;
}
int Test2(int a, int b)
{
cout << a << " " << b << " " << m_nFirst << " " << m_nTest << endl;
return 0;
}
};
int _tmain(int argc, _TCHAR* argv[])
{
Test t;
t.TestThunk();
system("pause");
return 0;
}
总结:
这个明显是暴力的去强制跳转,直接把指令写入到数据段中,增加了出错的风险,而且可移植性变的很差。所以尽量少用。
要弄清楚函数调用规则和堆栈的平衡。如果你用_cedcl规则的函数调用的话,就会出错啦。
学习代码中只是处理了简单的情况,还有几种方式,例如不是强制跳转,而是用call的方式调用,也可以实现。对于其他的函数规则例如成员函数是_stdcall,他是参数压栈的,这个THUNK的写法也不一样了。。
因为数据段中用到了this,函数回调中会用到它,所以一定要保证这个this有效。特别是窗口回调函数,如果释放了变量,但是窗口没有销毁是很容易出问题的。窗口回调函数也有比较喜欢用一个静态的分配器,通过窗口识别,把他分配到不同的成员处理函数中的方式。
这个只是初学,原因是发现ATL的窗口回调是这样做的。觉得很神奇,所以学习了一下,有不对的地方还望多多指教。嘻嘻。。。
找到的资料:
http://www.vckbase.com/document/viewdoc/?id=1821
http://www.codeproject.com/KB/cpp/GenericThunks.aspx
http://blog.csdn.net/superarhow/archive/2006/07/10/898261.aspx
http://www.cnblogs.com/homeofish/archive/2009/02/20/1395208.html