回溯显示在奇怪行的析构函数调用的零星崩溃

Sporadic crash where backtrace shows destructor call at strange line

我有一个正常运行的程序,但今天它在启动时崩溃了。 运行 之后它再次运行得很好,所以不幸的是我无法给出一个最小的例子。但是,代码看起来像这样:

    #include "Configuration.hpp"
    #include "Program.hpp"
  
    int main()
      {
         ConfigurationReader confReader; // this is line 6, where gdb indicates a 
                                         // segfault in the *destructor* of 
                                         // ConfigurationReader

         confReader.readConf();
         Conf & conf = confReader.getConf();
         Program program(conf);
         program.run();
 
        return 0;
     }

程序报告了一个段错误,并且在 gdb 中启动核心它说段错误发生在上面代码示例中的第 6 行,在 析构函数ConfigurationReader.

当然,在这里调用析构函数是没有意义的,因为只有一个 ConfigurationReader 的实例在浮动,它不应该析构直到它在结束时超出范围main。即使进行了积极的优化,在 program 之前它也无法破坏,因为 program 被传递给了对存在于 confReader.

中的东西的引用

问题:这里(或可能)发生了什么?是否有一些我没有看到的未定义行为? gdb 关于它的堆栈跟踪是不是非常错误?我是否应该怀疑构建过程出了问题?

注意: 我知道最好不要让 ConfigurationReader 拥有它读入的 Conf 实例,但这不是这个问题的目的。请不要在没有解决实际问题的情况下回答只是告诉我这样做。

更新:正如约翰在评论中指出的那样,我还应该在这里提供一些关于 ConfigurationReader::getConf 的信息:

class ConfigurationReader
{
  private:

    Conf conf;

  public:

    // ...

    Conf & getConf() { return conf; }
}

更新 2: 我删除了行号以使代码示例可复制,并添加了一条注释,指示 gdb 显示调用析构函数的位置。

注 2: 正如我最初所说的,不幸的是我无法提供最小的可重现示例。我无法在玩具程序中重现此问题。我什至无法在 real 程序中重现这个问题;这种崩溃只发生过一次。

仅凭您发布的代码无法找出实际问题所在。因此,这不是您问题的答案,而是一种帮助您使用 gdb 自己找到解决方案的方法。不过评论有点大。

运行 您在 gdb 中的程序并在 main 的开头停止。您可以为此使用 start 命令。现在在程序的最后一条指令处添加一个断点。您可以在 gdb 中使用 b _exit 执行此操作。如果问题没有发生,您的程序应该只 命中断点。当您在 gdb 中添加一个断点时,您会得到一个标识该断点的数字。假设 _exit 中的断点是断点 2。您可以在 gdb 中添加“命令”,当某些特定断点被击中时,它应该自动 运行 。使用 commands 2 将命令添加到断点 2 并键入 run(添加 run 命令),按 ENTER,然后键入 end(完成输入命令)。现在键入 run 以启动 运行 程序。

有了这个,你的程序将在 gdb 中 运行,如果错误没有发生,它会在遇到最后一条指令时从头开始再次 运行。当错误最终发生时,gdb 将停止执行,您可以使用实际进程(而不是仅仅使用核心转储)来调查问题。

我实际上能够在这里解决我自己的问题,部分是为了回应 PaulMcKenzie 在评论中提出的问题。为了说明,这里有一个玩具程序:

class C { };

class D
{
  public:

    ~D()
    {
      int * p = nullptr;
      int x = *p;
    }
};

class E { };

class F { };

int main(int, char **)
{
  C c;
  D d; // this is line 21
  E e;
  F f;

  return 0;
}

d 析构时,这当然会在 main() 结束时崩溃,因为我故意在析构函数中放置了一个段错误。

如果我使用调试符号构建它,然后 运行 在 gdb 中构建它,我会得到以下回溯:

#0  0x00005555555546cc in D::~D (this=0x7fffffffde67, __in_chrg=<optimized out>) at program.cpp:10
#1  0x000055555555469a in main () at program.cpp:21

注意它在堆栈帧 1 中报告的行号。当然,实际上第 22、23、24、25 和 26 行已经完成,如果需要,我们可以通过登录来证明这一点。

显然 gdb(或工具的任何部分负责将行号与编译器输出相关联)已经决定当对象超出范围时的析构函数调用应该与实际的代码行相关联最终导致它们,即构造对象的行。

所以大概我的程序中发生的事情是程序完成了,而崩溃实际上在 运行 中发生的时间比指示的要晚得多。 (这是它自己的问题,因为 program.run() 是一个没有退出条件的无限循环。由于 ConfigurationReader 的析构函数不打算可达,并且以前从未 运行,所以它不是令人惊讶的是它不能正常工作。)