织梦CMS - 轻松建站从此开始!

罗索实验室

当前位置: 主页 > 基础技术 > C/CPP专题 >

如何让类的成员函数作为回调函数

落鹤生 发布于 2010-10-29 15:10 点击:次 
一个很牛的文章,主要是用汇编完成了类成员作为回调函数,不用static。
TAG:

为什么类(class)的成员函(member function)数不能作为回调函数(callback function)
首先来看看回调函数有怎样的特点。windows中,回调函都显式(explicit)使用CALLBACK修饰符(decorator)修饰(decorated)。实际上CALLBACK就是_stdcall参数传递方式(calling convention)的宏定义。MSDN中对__stdcall做了如下定义:

The __stdcall calling convention is used to call Win32 API functions. The callee cleans the stack, so the compiler makes vararg functions __cdecl. Functions that use this calling convention require a function prototype.

Element Implementation
Argument-passing order Right to left.
Argument-passing convention By value, unless a pointer or reference type is passed.
Stack-maintenance responsibility Called function pops its own arguments from the stack.
Name-decoration convention An underscore () is prefixed to the name. The name is followed by the at sign (@) followed by the number of bytes (in decimal) in the argument list. Therefore, the function declared as int func( int a, double b ) is decorated as follows: _func@12
Case-translation convention None


其中心思想是,__stdcall修饰的函数,参数从右至左依次压入堆栈,被调用者(callee)负责平衡堆栈(clean also called ‘stack unwinding handling’)。

下面来看看类的成员函数有怎样的特点。在VC++中,所有类的成员函数在定义的时候都被隐式(implicit)定义为__thiscall参数传递方式。在MSDN 中对__thiscall做了如下定义:

The __thiscall calling convention is used on member functions and is the default calling convention used by C++ member functions that do not use variable arguments. Under __thiscall, the callee cleans the stack, which is impossible for vararg functions. Arguments are pushed on the stack from right to left, with the this pointer being passed via register ECX, and not on the stack, on the x86 architecture.

其中心思想是,__thiscall 修饰的函数参数从右至左依次压入堆栈,被调用者负责平衡堆栈。之后是与C语言所有参数传递方式均不相同的一点:成员函数所在类的this指针被存入ecx寄存器(这个特性只针对Intel x86架构)。

对比之后,我们发现类成员函数不能作为回调函数的主要原因在于类成员函数使用__thiscal参数传递方式,因此需要调用者(caller)通过ecx寄存器提供类对象的指针。而回调函数使用__stdcall参数传递方式,不具备这个特点。

如何让类成员函数成为回调函数
根据第一节对回调函数与类成员函数各自特点的分析。不难发现,只要能想办法在类成员函数被调用之前设置好ecx寄存器,就能在__stdcall调用的基础上模拟出一个完好的__thiscall调用。

如何提前设置ecx寄存器呢?我们知道函数调用实际是通过汇编指令(oprand)’call 函数地址’完成的。因此我们可以提供一个中间函数。当回调发生时,先调用中间函数,再在中间函数执行过程中设置ecx寄存器,当ecx设置好后jmp到类成员函数去(注意:这里是jmp不是call)。当执行到类的成员函数时,函数上下文(function context)就和__thiscall所产生的完全一样了。

如何制作这个中间函数呢?普通的函数是不行的。主要因为在vc++ debug版本的代码中要使用ecx寄存器做堆栈溢出检测(stack overflow detect),即使是空函数都是如此。其次由于存在栈框(stack frame)效率也不高。

这时就需要使用thunk来达到我们的目的。所谓thunk就是程序自己生成并执行的一小段汇编代码。下面通过代码来理解thunk。


#include "windows.h"
#include "stdio.h"
#include "stdlib.h"
#include "assert.h"
#include "stdafx.h"
//////////////////////////////////////////////////////////////////////////
// 回调函数类型定义
typedef int (CALLBACK pfaCallBack)(int, long, char);
//////////////////////////////////////////////////////////////////////////
// thunk 结构定义
// 由于thunk 要被当作代码来执行,因此thunk 结构必须是字节对齐的,这里使用
// VC++ 的修饰符号#pragma pack(push, 1) 来定义一个字节对齐的结构体
// 之后通过#pragma(pop) 恢复默认对齐模式
#pragma pack(push, 1)
struct Thunk
{
     BYTE op_movecx;
     DWORD_PTR val_ecx;
     BYTE op_call;
     DWORD_PTR val_address;
};
#pragma pack(pop)
//////////////////////////////////////////////////////////////////////////
// 一个类的定义,就这样平静的开始了
class Dummy {
// 一个成员变量
private:
     int m_id ;
// 定义一个thunk
private:
     Thunk m_thunk;
// 定义构造函数,在构造函数中设置m_id值
public:
     Dummy(int id):m_id(id)
     {
     }
//////////////////////////////////////////////////////////////////////////
// 定义一个回调函数,另外他还是个类的成员函数呢
public:
     int memberCallback(int intVal, long longVal, char charVal)
     {
         // 做自己想做的事情
         printf("\nI am a member function of class Dummy""(Dummy::memberCallback),ID = %d.""\nI got the value 0x%08x 0x%08x \'%c\'"
             , m_id, intVal, longVal, charVal);
         return m_id;
     }
//////////////////////////////////////////////////////////////////////////
// 初始化thunk 的数据,这里是关键
public:
     void InitThunk()
     {
         // 0xB9是‘mov ecx, 数值’的机器码,xB9之后的个字节(32位)指定了要
         // 给ecx的数值.
         m_thunk.op_movecx = 0xB9;
         // 填写要给ecx的数值为this(类对象的指针)
         m_thunk.val_ecx = (DWORD_PTR)this;
         // 0xE9是‘jmp 相对地址’的机器码。相对地址由xE9之后的个字节(32位)
         // 给出。
         m_thunk.op_call = 0xE9;
         // 获得Dummy::memberCallback的具体地址。关于成员函数与类对象的关系
         // 请参阅Stan Lippman 的<<Inside C++ Object Model>>
         // 用汇编获得地址省去了用C++带来的难看的语法
         DWORD_PTR off = 0;
        _asm
         {
                 mov eax, Dummy::memberCallback          
                 mov DWORD PTR [off], eax
         }
         // jmp后面是相对地址,因此要求出这个地址
         // 相对地址=成员函数地址-跳转点下一指令地址
         // 正负号不要紧,jmp自己能根据正负判断如何跳。
         m_thunk.val_address =
             off - ( (DWORD_PTR)(&m_thunk.val_address) + sizeof(DWORD_PTR) );
     }
//////////////////////////////////////////////////////////////////////////
// 返回thunk的地址给要回调他的函数。
// 那个函数还以为thunk是一个函数地址呢。根本不知道thunk是我们自己构造的
// 数据
public:
     pfaCallBack GetStaticEntry()
     {
         return (pfaCallBack)&m_thunk;
     }
};
//////////////////////////////////////////////////////////////////////////
// 一个调用回调函数的函数
void Trigger(pfaCallBack callback)
{
     assert(callback);
     int intVal = 0x1234;
     int longVal = 0x5678ABCD;
     int charVal = 'D';
     // 函数内部
     int r;
     // 开始回调
     r = callback(intVal, longVal, charVal);
     printf("\n Return value = %d\n", r);
}
//////////////////////////////////////////////////////////////////////////
// 传说中的主函数。VC++工程里生成的就叫_tmain不叫main。
int _tmain(int argc, _TCHAR argv[])
{
     //生成一个对象
     Dummy *dummy1 = new Dummy(9);
     //初始化thunk
     dummy1->InitThunk();
     //取得thunk地址
     pfaCallBack pCallback1 = dummy1->GetStaticEntry();
     //给需要回调函数的函数传递thunk
     Trigger(pCallback1);
     // 按任意键继续...
     system("pause");
     return 0;
}

(dtzw)
本站文章除注明转载外,均为本站原创或编译欢迎任何形式的转载,但请务必注明出处,尊重他人劳动,同学习共成长。转载请注明:文章转载自:罗索实验室 [http://www1.rosoo.net/a/201010/10376.html]
本文出处:百度博客 作者:dtzw
顶一下
(0)
0%
踩一下
(0)
0%
------分隔线----------------------------
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
用户名: 验证码:点击我更换图片
栏目列表
将本文分享到微信
织梦二维码生成器
推荐内容