首页 > 代码库 > Bug:C++运行时调用纯虚函数

Bug:C++运行时调用纯虚函数

    昨天服务器宕机,打印出的日志非常诡异,宕在纯虚函数调用处。
    日志显示,战斗对象的虚函数调用,前几次正常,某个时刻过后“丧失多态”了,直接调到父类虚函数处,引发纯虚函数宕机。
    且win平台下运行正常,上linux必跪,老项目linux工具不全,debug版本都编不出来,只有Log;windows下还复现不出来。

    找这个bug的过程还是蛮有意思的。记录下(*^__^*)     

    以往没碰到过这种Bug,起初当然毫无头绪。
    首先想到,c++中经常的内存改写,但能正常调用到普通虚函数,应该不是memset这样的东西把对象写坏。
    进一步分析,要能正常调到父类虚函数,那对象虚指针一定指向了正确的父类虚表。

    回忆c++构造函数流程:
        1、假设CBase里有几个纯虚函数 CObj 继承它
        2 、CObj 的构造顺序:先构造 CBase 的部分,此时对象首地址的虚指针指向了 CBase的虚表……再接着构造 CObj 新增的部分,改写对象首地址的虚指针,指向ClassObj的虚表

    如果析构函数按对应顺序反过来,容器里保存的 CBase* 指针,经过析构后,指向的对象,它的首地址就被改写为指向 CBase 虚表了。

    这样就会出现日志看到的情况。

    但我不确定析构函数是不是会改虚指针,按照构造、析构对称的思路预计是会的。
    网上也没查到资料,决定写代码实验~~结果不会

    …………没啥线索了

    晚上想起编译器对拷贝构造函数的优化,默认生成的拷贝构造函数其实不会被调用(没有副作用),直接优化为字节拷贝即可。
    写的测试代码里没显式声明析构函数,会不会也被编译器跳过了。所以 delete 后,首地址的vptr还是没变。

    今天来立马改了测试代码,在父类里加上析构函数声明、实现……果然,析构后对象首地址的内容被改写了
    Obj* pB = new Obj();
    printf("addr(%d) \n", *((int*)pB));
    delete pB;
    printf("addr(%d) \n", *((int*)pB));

    至此,可以肯定服务器宕机,就是因为战斗对象被析构,虚指针被改写为指向父类虚表,业务层再拿来用时就跪了。
    (因为用到内存池,所以没出现悬垂指针的问题) 

    剩下的就好查了,delete对象时某业务模块仍持有其指针,没清理。搜搜战斗对象的引用关系,几分钟便找到问题所在。
    
    战斗城池里有个守卫列表,npc进入时会把自己指针放入这个列表,死亡时没去清。
    别人再来打这个城池时,跑战斗流程就调了纯虚函数,宕机。

    
尾声:
    觉得这个bug挺有深度的,能扣的地方很多。
    比如,为什么在win下不会宕机呢?项目里的战斗对象也是没显式析构函数的,应该是被vs编译器优化掉了,而Linux没有。
    再比如,如果没有内存池,那两边应该都会出现悬垂指针,直接宕机……提前暴露问题所在,反而更好分析定位Bug。
    还有,win环境下,即便免去了纯虚函数的宕机问题,但确将Bug隐藏的更深了。后面业务逻辑再从内存池取指针,拿到那个旧的,胡乱一改,再出问题时候,你看到的就是一坨shit了,鬼知道到底是哪改坏的 ( ̄﹁ ̄)~

    还是我们老大说的好:
        内存池如果是新项目,我估计不会使用,会直接用TCMALLOC之类的。我还是想能工程化就工程化,C++开发还是要往库的思维走。不然老挖坑填坑。


PS: 没头绪下班前,我干了三件事情:
                在前C++项目群里描述问题,询问“有谁碰到过中途调纯虚函数,服务器宕机的情况”;
                在加入的技术群里问;
                在知乎提问,邀请轮子哥、R大
        次天来就看到有人回复:子类析构掉的话,虚表会被改写成iobj的虚表,析构过的指针,可以调iobj的虚函数,调其它虚函数则会挂
        即使自己没能想到“析构过程可能被编译器优化掉”,也能在他们的指导之下找到问题的。
        利用别人的经验哈 b( ̄▽ ̄)d

Bug:C++运行时调用纯虚函数