« 有的源码是不值得现在再去读的 | 返回首页 | 使用 closure 替代 table »

监视单件的调用

我们现在的引擎中,所有的单件由一个管理类来管理。任何一个模块想取到一个单件,都可以通过一个统一的方法从管理类中拿到。

在调试程序的过程中,我遇到了一个奇怪的需求。我需要在一个单件的方法被调用时,程序都会停下来,进入调试器。

诚然,如果每次单件都通过 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 直接指定内存断点好了……
奇怪的需求,奇怪的功能,奇怪的实现
云风在游戏这块算是老人了… 这段代码大概就是实现了一个动态的代理过程吧…aop的功能,apectC++里提供了更完善的功能和配置…建议云风看看AOP,应该这是你更想要的解决方案。
本来看你的BLOG从来不加评论,以学习的态度去看,毕竟是前辈。但这篇实在....也许为当时的目的确实有用,但这个确实有点“妖”了。
和delphi的窗口proc与类成员的连接有点类似。 我也写过类似的,只不过扩展到了所有的callback和类对象的连接,可以作很多事情。这个技巧非常有用。
重载 operator-> 需要重编译代码,它会修改代码的行为,不是2进制的解决方案。
通过重载operator->应该更方便一些,不过也是非常规技巧了。
另外,通过C++ 源码以及 template 技巧来封装 -> 调用也不是不可以。 B. Stroustrup 为 C++ Report 上写过一篇 paper 叫 Wrapping C++ Member Function Calls 可以解决这个问题。 但是我想要的解决方案是2进制的,不用经过代码重编译的而已。
修改 get_instance 有个好处。每个分离编译的模块内部可能只需要调用一次 get_instance 再把 instance 记在一个全局指针中。 我们用这个技巧可以单独对一个模块做调试。比如,内存分配器可能是一个单件。我们可以就可以监视某个指定模块对内存分配的调用。
sorry, 我明白了,你这里修改的是get_instance 明白了,要实现这种古怪的需求,却是没办法只能用这种古怪的写法。 可惜C++的模板不能for each method,只能用运行时的办法来做。 java或者.NET用类型信息来实现泛型,C++用template实现泛型,但是template毕竟不具有足够的编译时类型信息
大概意思好像明白了,但是能力有限,代码还是看不太懂。 to:Atry 看你前两条留言还以为是个“高人”呢,一看第三条留言原来你连文章都没仔细读就开骂……
现在我们的引擎是做2进制复用的。所以对于调用单件的模块和实现单件的模块都不能修改和重新编译。 重载 -> 会修改代码执行的行为,不能二进制复用。(必须修改代码并重新编译) 这篇 blog 主要是介绍一种非常规的技巧,来处理 C++ 的代码。
这个在GOF里面叫做“decorate”
不明白在说什么。
为何不直接重载->
奇技淫巧,不用为妙
哈哈,写这么丑陋的代码还洋洋自得

Post a comment

非这个主题相关的留言请到:留言本