“纯虚函数调用”崩溃从何而来?

Posted

技术标签:

【中文标题】“纯虚函数调用”崩溃从何而来?【英文标题】:Where do "pure virtual function call" crashes come from? 【发布时间】:2010-09-11 02:28:43 【问题描述】:

我有时会注意到程序在我的计算机上崩溃并显示错误:“纯虚函数调用”。

当无法从抽象类创建对象时,这些程序如何编译?

【问题讨论】:

【参考方案1】:

如果您尝试从构造函数或析构函数调用虚函数,则可能会出现这种情况。由于您不能从构造函数或析构函数调用虚函数(派生类对象尚未构造或已被销毁),因此它调用基类版本,在纯虚函数的情况下,不会'不存在。

(见现场演示here)

class Base

public:
    Base()  doIt();   // DON'T DO THIS
    virtual void doIt() = 0;
;

void Base::doIt()

    std::cout<<"Is it fine to call pure virtual function from constructor?";


class Derived : public Base

    void doIt() 
;

int main(void)

    Derived d;  // This will cause "pure virtual function call" error

【讨论】:

一般情况下编译器无法捕捉到这一点的任何原因? GCC 只给我一个警告:test.cpp: In constructor 'Base::Base()': test.cpp:4: warning: abstract virtual 'virtual void Base::doIt()'从构造函数调用但它在链接时失败。 在一般情况下无法捕捉到它,因为来自ctor的流可以去任何地方,任何地方都可以调用纯虚函数。这是停止问题 101。 回答略有错误:纯虚函数仍可能被定义,详见***。正确的表述:可能不存在 我觉得这个例子太简单了:构造函数中的doIt()调用很容易被去虚拟化并被静态分派到Base::doIt(),这只会导致链接器错误。我们真正需要的是动态类型在动态调度期间是抽象基类型的情况。【参考方案2】:

除了从具有纯虚函数的对象的构造函数或析构函数调用虚函数的标准情况外,如果您在对象之后调用虚函数,您还可以获得纯虚函数调用(至少在 MSVC 上)已被摧毁。显然,这是一件非常糟糕的事情,但如果您将抽象类作为接口使用并且您搞砸了,那么您可能会看到它。如果您使用引用的计数接口并且您有一个引用计数错误,或者如果您在多线程程序中存在对象使用/对象销毁竞争条件,则可能更有可能......关于这些类型的纯调用的事情是通常不太容易弄清楚发生了什么,因为检查 ctor 和 dtor 中虚拟呼叫的“通常嫌疑人”会变得干净。

为了帮助调试这些类型的问题,您可以在各种版本的 MSVC 中替换运行时库的 purecall 处理程序。您可以通过使用此签名提供您自己的函数来做到这一点:

int __cdecl _purecall(void)

并在链接运行时库之前链接它。这使您可以控制检测到纯调用时发生的情况。一旦你有了控制权,你就可以做一些比标准处理程序更有用的事情。我有一个处理程序,可以提供纯调用发生位置的堆栈跟踪;请参阅此处:http://www.lenholgate.com/blog/2006/01/purecall.html 了解更多详情。

(请注意,您也可以调用 _set_purecall_handler() 在某些版本的 MSVC 中安装您的处理程序)。

【讨论】:

感谢有关在已删除实例上获取 _purecall() 调用的指针;我没有意识到这一点,只是用一些测试代码向自己证明了这一点。查看 WinDbg 中的事后转储,我以为我正在处理一场竞赛,其中另一个线程在完全构造之前尝试使用派生对象,但这为这个问题带来了新的曙光,并且似乎更适合证据。 我要补充的另一件事:如果基类已声明为__declspec(novtable) 优化(Microsoft 特定)。这样,完全有可能在对象被删除后调用重写的虚拟方法,这可能会掩盖问题,直到它以其他形式咬你。 _purecall() 陷阱是你的朋友! 了解 Dave 很有用,我最近看到了一些我认为应该获得纯调用但没有获得纯调用的情况。也许我对那个优化犯规了。 @LenHolgate:非常有价值的答案。这正是我们的问题案例(由竞争条件引起的错误引用计数)。非常感谢您为我们指明了正确的方向(我们怀疑 v-table 损坏,并疯狂地试图找到罪魁祸首)【参考方案3】:

通常当你通过一个悬空指针调用一个虚函数时——很可能实例已经被销毁了。

也可能有更多“创造性”的原因:也许您已经设法切掉了实现虚函数的对象部分。但通常只是实例已经被销毁。

【讨论】:

【参考方案4】:

我遇到了纯虚函数因为对象被破坏而被调用的场景,Len Holgate 已经有一个很好的answer,我想要 用一个例子添加一些颜色:

    创建了一个派生对象,并且指针(作为基类)是 保存在某处 Derived 对象已删除,但指针不知何故 仍被引用 指向已删除 Derived 的指针 对象被调用

Derived 类析构函数将 vptr 指向 Base 类 vtable,它有纯虚函数,所以当我们调用虚函数时,它实际上调用的是纯虚函数。

这可能是由于明显的代码错误或多线程环境中竞争条件的复杂情况而发生的。

这是一个简单的例子(g++ 编译关闭优化 - 一个简单的程序可以很容易地被优化掉):

 #include <iostream>
 using namespace std;

 char pool[256];

 struct Base
 
     virtual void foo() = 0;
     virtual ~Base();
 ;

 struct Derived: public Base
 
     virtual void foo() override  cout <<"Derived::foo()" << endl;
 ;

 int main()
 
     auto* pd = new (pool) Derived();
     Base* pb = pd;
     pd->~Derived();
     pb->foo();
 

堆栈跟踪看起来像:

#0  0x00007ffff7499428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007ffff749b02a in __GI_abort () at abort.c:89
#2  0x00007ffff7ad78f7 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007ffff7adda46 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007ffff7adda81 in std::terminate() () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007ffff7ade84f in __cxa_pure_virtual () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x0000000000400f82 in main () at purev.C:22

重点:

如果对象被完全删除,这意味着析构函数被调用,并且内存被回收,我们可能会简单地得到一个Segmentation fault,因为内存已经返回给操作系统,程序就无法访问它。所以这种“纯虚函数调用”的场景通常发生在对象被分配到内存池中,而对象被删除时,底层内存实际上并没有被操作系统回收,它仍然可以被进程访问。

【讨论】:

【参考方案5】:

我猜出于某种内部原因(可能需要某种运行时类型信息)为抽象类创建了一个 vtbl,并且出现了问题并且一个真实的对象得到了它。这是一个错误。仅此一点就应该说是不可能发生的事情。

纯属猜测

edit: 看起来我在所讨论的情况下错了。 OTOH IIRC 某些语言确实允许从构造函数析构函数中调用 vtbl。

【讨论】:

这不是编译器中的错误,如果这是您的意思。 您的怀疑是正确的——C# 和 Java 允许这样做。在这些语言中,正在构建的对象确实有其最终类型。在 C++ 中,对象在构造过程中会改变类型,这就是为什么以及何时可以拥有具有抽象类型的对象。 ALL 抽象类和从它们派生的真实对象需要一个 vtbl(虚拟函数表),列出应该在其上调用哪些虚拟函数。在 C++ 中,对象负责创建自己的成员,包括虚函数表。构造函数是从基类调用到派生的,析构函数是从派生到基类调用的,所以在抽象基类中还没有虚函数表。【参考方案6】:

我使用 VS2010,每当我尝试直接从公共方法调用析构函数时,我都会在运行时收到“纯虚函数调用”错误。

template <typename T>
class Foo 
public:
  Foo<T>() ;
  ~Foo<T>() ;

public:
  void SomeMethod1()  this->~Foo(); ; /* ERROR */
;

所以我把 ~Foo() 里面的东西移动到单独的私有方法中,然后它就像一个魅力。

template <typename T>
class Foo 
public:
  Foo<T>() ;
  ~Foo<T>() ;

public:
  void _MethodThatDestructs() ;
  void SomeMethod1()  this->_MethodThatDestructs(); ; /* OK */
;

【讨论】:

【参考方案7】:

如果你使用 Borland/CodeGear/Embarcadero/Idera C++ Builder,你可以直接实现

extern "C" void _RTLENTRY _pure_error_()

    //_ErrorExit("Pure virtual function called");
    throw Exception("Pure virtual function called");

在调试时在代码中放置一个断点并在 IDE 中查看调用堆栈,否则,如果您有相应的工具,则将调用堆栈记录在您的异常处理程序(或该函数)中。我个人为此使用 MadExcept。

附言。原函数调用在[C++ Builder]\source\cpprtl\Source\misc\pureerr.cpp

【讨论】:

【参考方案8】:

这是一种偷偷摸摸的方式。我今天基本上发生了这种情况。

class A

  A *pThis;
  public:
  A()
   : pThis(this)
  
  

  void callFoo()
  
    pThis->foo(); // call through the pThis ptr which was initialized in the constructor
  

  virtual void foo() = 0;
;

class B : public A

public:
  virtual void foo()
  
  
;

B b();
b.callFoo();

【讨论】:

至少它不能在我的vc2008上重现,vptr在A的构造函数中第一次初始化时确实指向A的vtable,但是当B完全初始化时,vptr改为指向B的vtable,没关系 不能用 vs2010/12 重现它 I had this essentially happen to me today 显然不正确,因为完全错误:只有在构造函数(或析构函数)中调用 callFoo() 时才会调用纯虚函数,因为此时对象仍然(或已经)处于 A 阶段的时间。 Here is a running version 的代码没有 B b(); 中的语法错误 - 括号使它成为函数声明,你想要一个对象。

以上是关于“纯虚函数调用”崩溃从何而来?的主要内容,如果未能解决你的问题,请参考以下文章

纯虚函数调用

在 C# 中处理纯虚函数调用

抑制消息框 R6025 纯虚函数调用

Linux系统启动过程的打印信息从何而来?

模板继承中的变量从何而来?

神经网络从何而来?