API回调成员函数 THUNK

API回调成员函数 THUNK

想来想去还是罗嗦一下, 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

你可能感兴趣的:(API回调成员函数 THUNK)