为什么类(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.
其中心思想是,__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。
1#include "windows.h"
2#include "stdio.h"
3#include "stdlib.h"
4#include "assert.h"
5#include "stdafx.h"
6/**/////////////////////////////////////////////////////////////////////////// 7// 回调函数类型定义
8typedef int (CALLBACK pfaCallBack)(int, long, char);
9/**/////////////////////////////////////////////////////////////////////////// 10// thunk 结构定义
11// 由于thunk 要被当作代码来执行,因此thunk 结构必须是字节对齐的,这里使用
12// VC++ 的修饰符号#pragma pack(push, 1) 来定义一个字节对齐的结构体
13// 之后通过#pragma(pop) 恢复默认对齐模式
14#pragma pack(push, 1)
15struct Thunk
16{
17 BYTE op_movecx;
18 DWORD_PTR val_ecx;
19 BYTE op_call;
20 DWORD_PTR val_address;
21};
22#pragma pack(pop)
23/**/////////////////////////////////////////////////////////////////////////// 24// 一个类的定义,就这样平静的开始了
25class Dummy {
26// 一个成员变量
27private:
28 int m_id ;
29// 定义一个thunk
30private:
31 Thunk m_thunk;
32// 定义构造函数,在构造函数中设置m_id值
33public:
34 Dummy(int id):m_id(id)
35 {
36 }
37/**///////////////////////////////////////////////////////////////////////////
38// 定义一个回调函数,另外他还是个类的成员函数呢
39public:
40 int memberCallback(int intVal, long longVal, char charVal)
41 {
42 // 做自己想做的事情
43 printf("\nI am a member function of class Dummy""(Dummy::memberCallback),ID = %d.""\nI got the value 0x%08x 0x%08x \'%c\'"
44 , m_id, intVal, longVal, charVal);
45 return m_id;
46 }
47/**///////////////////////////////////////////////////////////////////////////
48// 初始化thunk 的数据,这里是关键
49public:
50 void InitThunk()
51 {
52 // 0xB9是‘mov ecx, 数值’的机器码,xB9之后的个字节(32位)指定了要
53 // 给ecx的数值.
54 m_thunk.op_movecx = 0xB9;
55 // 填写要给ecx的数值为this(类对象的指针)
56 m_thunk.val_ecx = (DWORD_PTR)this;
57 // 0xE9是‘jmp 相对地址’的机器码。相对地址由xE9之后的个字节(32位)
58 // 给出。
59 m_thunk.op_call = 0xE9;
60 // 获得Dummy::memberCallback的具体地址。关于成员函数与类对象的关系
61 // 请参阅Stan Lippman 的<<Inside C++ Object Model>>
62 // 用汇编获得地址省去了用C++带来的难看的语法
63 DWORD_PTR off = 0;
64 _asm
65 {
66 mov eax, Dummy::memberCallback
67 mov DWORD PTR [off], eax
68 }
69 // jmp后面是相对地址,因此要求出这个地址
70 // 相对地址=成员函数地址-跳转点下一指令地址
71 // 正负号不要紧,jmp自己能根据正负判断如何跳。
72 m_thunk.val_address =
73 off - ( (DWORD_PTR)(&m_thunk.val_address) + sizeof(DWORD_PTR) );
74 }
75/**///////////////////////////////////////////////////////////////////////////
76// 返回thunk的地址给要回调他的函数。
77// 那个函数还以为thunk是一个函数地址呢。根本不知道thunk是我们自己构造的
78// 数据
79public:
80 pfaCallBack GetStaticEntry()
81 {
82 return (pfaCallBack)&m_thunk;
83 }
84};
85/**/////////////////////////////////////////////////////////////////////////// 86// 一个调用回调函数的函数
87void Trigger(pfaCallBack callback)
88{
89 assert(callback);
90 int intVal = 0x1234;
91 int longVal = 0x5678ABCD;
92 int charVal = 'D';
93 // 函数内部
94 int r;
95 // 开始回调
96 r = callback(intVal, longVal, charVal);
97 printf("\n Return value = %d\n", r);
98}
99/**///////////////////////////////////////////////////////////////////////////100// 传说中的主函数。VC++工程里生成的就叫_tmain不叫main。
101int _tmain(int argc, _TCHAR argv[])
102{
103 //生成一个对象
104 Dummy *dummy1 = new Dummy(9);
105 //初始化thunk
106 dummy1->InitThunk();
107 //取得thunk地址
108 pfaCallBack pCallback1 = dummy1->GetStaticEntry();
109 //给需要回调函数的函数传递thunk
110 Trigger(pCallback1);
111 // 按任意键继续
112 system("pause");
113 return 0;
114}
115
posted on 2008-07-06 03:58
幽幽 阅读(809)
评论(0) 编辑 收藏 引用 所属分类:
Windows