我们现在的引擎中,所有的单件由一个管理类来管理。任何一个模块想取到一个单件,都可以通过一个统一的方法从管理类中拿到。
在调试程序的过程中,我遇到了一个奇怪的需求。我需要在一个单件的方法被调用时,程序都会停下来,进入调试器。
诚然,如果每次单件都通过 get_instance
取得,然后调用其方法的话,我们在 get_instance
里设置断点即可。但是,在我们的引擎中,每个模块都是在初始化阶段,拿到单件的指针。然后放在了全局变量中。单件的使用,直接运用这个指针。这样,就对监视这个单件的调用造成了麻烦。
为了解决这个问题,我用了一个很 trick 的方法,下面列出代码。
#include
#include
#include
#include
struct i_foobar {
virtual void foo(int arg)=0;
virtual void bar()=0;
};
class foobar : public i_foobar {
public:
virtual void foo(int arg) {
printf("%d",arg);
}
virtual void bar() {
printf("bar");
}
};
#define MAX_METHOD 64
#pragma pack(push,1)
struct __proxy_code {
unsigned char mov_edx;
unsigned long index;
unsigned char jmp;
unsigned long offset;
unsigned char nop1;
unsigned char nop2;
};
#pragma pack(pop)
static const void* __proxy_virtual_table[MAX_METHOD];
static __proxy_code __proxy_bridge_code[MAX_METHOD];
__declspec(naked) void __proxy_gate()
{
__asm {
int 3 // 断点
mov ecx,[ecx-4]
mov eax,[ecx]
add eax,edx
mov eax,[eax]
jmp eax
}
}
struct __proxy_t {
void *__instance;
void *__vtbl;
};
void init_proxy()
{
int i;
for (i=0;i
T* create_proxy(T* instance)
{
__proxy_t *p=new __proxy_t;
p->__instance=instance;
p->__vtbl=__proxy_virtual_table;
return (T*)&(p->__vtbl);
}
void main()
{
init_proxy();
i_foobar *foo=new foobar;
i_foobar *proxy=create_proxy(foo);
proxy->foo(100);
proxy->bar();
}
我们给出了一个简单的例子,这个程序有很多问题,比如单件 foo 在程序开始构造出来,却没有在最后释放。我们只是用它说明这个技巧。这里我们拥有一个单件的接口 i_foobar
,它有两个方法 foo 和 bar 。下面有一个简单的实现 foobar 。接下来我不希望对 i_foorbar
以及 foobar 的实现做任何修改了。
通过两个数组,__proxy_virtual_table , __proxy_code __proxy_bridge_code
我们得到了一个假的 foobar 对象。实际上它可以伪装任何一个接口类(有虚函数上限64个的限制)。有了这个东西,我们可以为 foobar 这个单件创建一个 proxy 了。proxy 这个名字可能不太恰当,姑且如此了。 这里的 proxy 的行为和 foo 完全一致,唯一不同的是,每次调用都会产生一个 int 3 :)
实际上,这个技巧可以被更广泛的使用,比如对特定函数做钩子,或者有更灵活的断点设置方法等。
Comments
如果用VS IDE
直接指定内存断点好了……
Posted by: 凄临雨 | (17) April 23, 2012 12:47 AM
奇怪的需求,奇怪的功能,奇怪的实现
Posted by: yongzhi.wu | (16) September 20, 2007 02:16 PM
云风在游戏这块算是老人了…
这段代码大概就是实现了一个动态的代理过程吧…aop的功能,apectC++里提供了更完善的功能和配置…建议云风看看AOP,应该这是你更想要的解决方案。
Posted by: jiffwan | (15) October 30, 2006 11:37 AM
本来看你的BLOG从来不加评论,以学习的态度去看,毕竟是前辈。但这篇实在....也许为当时的目的确实有用,但这个确实有点“妖”了。
Posted by: chloe | (14) May 7, 2006 05:08 PM
和delphi的窗口proc与类成员的连接有点类似。
我也写过类似的,只不过扩展到了所有的callback和类对象的连接,可以作很多事情。这个技巧非常有用。
Posted by: Dragon | (13) March 31, 2006 01:26 PM
重载 operator-> 需要重编译代码,它会修改代码的行为,不是2进制的解决方案。
Posted by: Cloud
| (12)
March 23, 2006 07:14 PM
通过重载operator->应该更方便一些,不过也是非常规技巧了。
Posted by: 林悠雨 | (11) March 23, 2006 04:28 PM
另外,通过C++ 源码以及 template 技巧来封装 -> 调用也不是不可以。
B. Stroustrup 为 C++ Report 上写过一篇 paper 叫 <a href="http://www.research.att.com/~bs/wrapper.pdf">Wrapping C++ Member Function Calls</a> 可以解决这个问题。
但是我想要的解决方案是2进制的,不用经过代码重编译的而已。
Posted by: Cloud
| (10)
March 14, 2006 03:40 PM
修改 get_instance 有个好处。每个分离编译的模块内部可能只需要调用一次 get_instance 再把 instance 记在一个全局指针中。
我们用这个技巧可以单独对一个模块做调试。比如,内存分配器可能是一个单件。我们可以就可以监视某个指定模块对内存分配的调用。
Posted by: Cloud
| (9)
March 14, 2006 03:34 PM
sorry, 我明白了,你这里修改的是get_instance
明白了,要实现这种古怪的需求,却是没办法只能用这种古怪的写法。
可惜C++的模板不能for each method,只能用运行时的办法来做。
java或者.NET用类型信息来实现泛型,C++用template实现泛型,但是template毕竟不具有足够的编译时类型信息
Posted by: Atry | (8) March 14, 2006 03:13 PM
大概意思好像明白了,但是能力有限,代码还是看不太懂。
to:Atry
看你前两条留言还以为是个“高人”呢,一看第三条留言原来你连文章都没仔细读就开骂……
Posted by: 不空 | (7) March 14, 2006 09:43 AM
现在我们的引擎是做2进制复用的。所以对于调用单件的模块和实现单件的模块都不能修改和重新编译。
重载 -> 会修改代码执行的行为,不能二进制复用。(必须修改代码并重新编译)
这篇 blog 主要是介绍一种非常规的技巧,来处理 C++ 的代码。
Posted by: Cloud
| (6)
March 13, 2006 01:09 PM
这个在GOF里面叫做“decorate”
Posted by: John Fractal | (5) March 13, 2006 11:15 AM
不明白在说什么。
Posted by: mouse | (4) March 13, 2006 09:50 AM
为何不直接重载->
Posted by: Atry | (3) March 13, 2006 09:33 AM
奇技淫巧,不用为妙
Posted by: Atry | (2) March 13, 2006 09:27 AM
哈哈,写这么丑陋的代码还洋洋自得
Posted by: Atry | (1) March 13, 2006 09:23 AM