• 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) ", *((int*)pB));
        delete pB;
        printf("addr(%d) ", *((int*)pB));

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

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

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

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


    PS: 没头绪下班前,我干了三件事情:
                    在前C++项目群里描述问题,询问“有谁碰到过中途调纯虚函数,服务器宕机的情况”;
                    在加入的技术群里问;
                    在知乎提问,邀请轮子哥、R大
            次天来就看到有人回复:子类析构掉的话,虚表会被改写成iobj的虚表,析构过的指针,可以调iobj的虚函数,调其它虚函数则会挂
            即使自己没能想到“析构过程可能被编译器优化掉”,也能在他们的指导之下找到问题的。
            利用别人的经验哈 b( ̄▽ ̄)d
  • 相关阅读:
    使用vs2010编译 Python SIP PyQt4
    谷歌编程指南
    【转】微策略面经相关资料
    KMP 算法
    C++ 拷贝构造函数
    虚继承 虚表 定义一个不能被继承的类
    cache的工作原理
    背包问题
    【转】C/C++ 内存对齐
    【转】 Linux/Unix 进程间通信的各种方式及其比较
  • 原文地址:https://www.cnblogs.com/3workman/p/6341393.html
Copyright © 2020-2023  润新知