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

监视单件的调用

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

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

诚然,如果每次单件都通过 get_instance 取得,然后调用其方法的话,我们在 get_instance 里设置断点即可。但是,在我们的引擎中,每个模块都是在初始化阶段,拿到单件的指针。然后放在了全局变量中。单件的使用,直接运用这个指针。这样,就对监视这个单件的调用造成了麻烦。

为了解决这个问题,我用了一个很 trick 的方法,下面列出代码。

#include <cstdio>
#include <cstddef>
#include <cstdlib>
#include <cstring>

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<MAX_METHOD;i++) {
        __proxy_virtual_table[i]=__proxy_bridge_code+i;
    }

    __proxy_code temp={
        0xba,0,     // mov edx,index*4
        0xe9,0,     // jmp __proxy_gate
        0x90,0x90,  // nop
    };

    for (i=0;i<MAX_METHOD;i++) {
        temp.index=i*4;
        temp.offset=(char*)__proxy_gate-
            ((char*)__proxy_bridge_code+
            i*sizeof(__proxy_code)+offsetof(__proxy_code,nop1));
        __proxy_bridge_code[i]=temp;
    }
}

template <typename T>
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 叫 <a href="http://www.research.att.com/~bs/wrapper.pdf">Wrapping C++ Member Function Calls</a> 可以解决这个问题。

但是我想要的解决方案是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

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