【问题标题】:Under what circumstances can a vtable pointer be null (or 0x1)?在什么情况下 vtable 指针可以为空(或 0x1)?
【发布时间】:2011-01-05 13:08:33
【问题描述】:

我目前正在调试崩溃日志。发生崩溃是因为 (c++-) 对象的 vtable 指针是 0x1,而据我从崩溃日志中可以看出,该对象的其余部分似乎没问题。

程序在尝试调用虚方法时崩溃。

我的问题:vtable 指针在什么情况下可以变为空? operator delete 是否将 vtable 指针设置为 null?

这发生在使用 gcc 4.0.1(Apple Inc. build 5493)的 OS X 上。

【问题讨论】:

  • 你能澄清一下 vtable 中的函数指针是 0 还是 this 指针为空?还是对象中指向当前vtable的指针?
  • vtable 指针为 1。在尝试执行 call *0x1c(%eax)(在 at&t 语法中)时发生崩溃,并且 eax 的值为 1(不是我说的不正确的零)。
  • 在 gdb 中运行,在地址设置观察点,看看写了什么。
  • 要么你在其他地方有一个错误,它正在践踏记忆。或者您的对象没有完全形成(您可以在构造或销毁期间调用虚拟方法)。您的问题没有明确的答案,因为标准对 v-tables 无话可说,而且每个编译器都完全不同。
  • Nikolai:“我目前正在调试崩溃日志。” Martin:当然,代码中的某个地方存在错误,而且绝非微不足道的错误。它与 c++ 标准无关。这是关于使用 gcc 4.0.1(Apple Inc. build 5493)找出可能导致此特定行为的错误。

标签: c++ macos gcc crash vtable


【解决方案1】:

可能是记忆的践踏——错误地覆盖了vtable。在 C++ 中,几乎有无数种方法可以“实现”这一目标。例如缓冲区溢出。

【讨论】:

    【解决方案2】:

    您的任何未定义行为都可能导致这种情况。例如:

    • 指针算术或其他导致程序写入无效内存的错误。
    • 未初始化的变量、无效的类型转换...
    • 以多态方式处理数组可能会导致这种情况成为次要影响。
    • 尝试在删除后使用对象。

    另请参阅问题What’s the worst example of undefined behaviour actually possible?What are all the common undefined behaviour that a C++ programmer should know about?

    最好的办法是使用边界和内存检查器,以帮助进行大量调试。

    【讨论】:

      【解决方案3】:

      一个很常见的情况:试图从构造函数中调用一个纯虚方法...

      构造函数

      struct Interface
      {
        Interface();
        virtual void logInit() const = 0;
      };
      
      struct Concrete: Interface()
      {
        virtual void logInit() const { std::cout << "Concrete" << std::endl; }
      };
      

      现在,假设Interface()的以下实现

      Interface::Interface() {}
      

      然后一切都很好:

      Concrete myConcrete;
      myConcrete.pure();    // outputs "Concrete"
      

      在构造函数之后调用pure好痛苦,还是分解代码更好吧?

      Interface::Interface() { this->logInit(); } // DON'T DO THAT, REALLY ;)
      

      那我们一行就可以搞定了!!

      Concrete myConcrete;  // CRASHES VIOLENTLY
      

      为什么?

      因为对象是自下而上构建的。来看看吧。

      构建Concrete 类的说明(粗略)

      1. 为 _vtable 分配足够的内存(当然)和足够的内存(每个虚函数 1 个函数指针,通常按照它们的声明顺序,从最左边的基数开始)

      2. 调用Concrete构造函数(你看不到的代码)

        a> 调用Interface 构造函数,它使用指针初始化_vtable

        b> 调用 Interface 构造函数的主体(你写的)

        c> 为这些方法覆盖 _vtable 中的指针具体覆盖

        d> 调用 Concrete 构造函数的主体(你写的)

      那么问题是什么?好吧,看看b&gt;c&gt; 的顺序;)

      当您在构造函数中调用virtual 方法时,它并没有达到您的预期。它确实会转到 _vtable 来查找指针,但 _vtable 尚未完全初始化。所以,不管怎样,效果如下:

      D() { this->call(); }
      

      其实是:

      D() { this->D::call(); }
      

      当从构造函数中调用虚方法时,您没有正在构建的对象的完整动态类型,而是调用了当前构造函数的静态类型。

      在我的Interface / Concrete 示例中,它表示Interface 类型,并且该方法是virtual pure,因此_vtable 不包含真正的指针(例如0x0 或0x01,如果您的编译器足够友好的话设置调试值以帮助您)。

      析构函数

      巧合的是,让我们来看看析构函数的例子;)

      struct Interface { ~Interface(); virtual void logClose() const = 0; }
      Interface::~Interface() { this->logClose(); }
      
      struct Concrete { ~Concrete(); virtual void logClose() const; char* m_data; }
      
      Concrete::~Concrete() { delete[] m_data; } // It's all about being clean
      void Concrete::logClose()
      {
        std::cout << "Concrete refering to " << m_data << std::endl;
      }
      

      那么破坏时会发生什么?那么 _vtable 工作得很好,并且调用了真正的运行时类型......然而,它在这里意味着未定义的行为,因为谁知道m_data 在被删除之后和Interface 析构函数被调用之前发生了什么?我没有;)

      结论

      永远不要在构造函数或析构函数中调用虚方法。

      如果不是这样,你的内存就会损坏,运气不好;)

      【讨论】:

      • 非常感谢您的详细回答。不幸的是,就我而言,情况并非如此。
      【解决方案4】:

      我的第一个猜测是某些代码是 memset()'ing 类对象。

      【讨论】:

        【解决方案5】:

        这完全取决于实现。但是,可以假设在删除后某些其他操作可能会将内存空间设置为空,这将是非常安全的。

        其他可能性包括用一些松散的指针覆盖内存——实际上在我的情况下几乎总是这样......

        也就是说,您永远不应该在删除后尝试使用对象。

        【讨论】:

        • delete 当然可以这样做,但我不知道有任何实现
        • @Neil:实现不是我的领域。我把答案改成了“更真实”的东西……
        猜你喜欢
        • 1970-01-01
        • 2012-10-21
        • 1970-01-01
        • 2015-09-28
        • 2010-09-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-06-02
        相关资源
        最近更新 更多