【问题标题】:Determine static initialization order after compilation?编译后确定静态初始化顺序?
【发布时间】:2010-11-16 11:57:29
【问题描述】:

在 C++ 中,我知道编译器可以选择以它选择的任何顺序(受一些约束)初始化静态对象,并且通常您无法选择或确定静态初始化顺序。

然而,一旦程序被编译,编译器必须决定初始化这些对象的顺序。有没有办法从带有调试符号的编译程序中确定以什么顺序静态构造函数会被调用吗?

上下文是这样的:我有一个相当大的程序,当它在一个新的工具链下构建时,它在 main() 之前突然出现段错误。这是一个静态初始化顺序问题,或者它正在加载的库之一有问题。但是,当我使用 gdb 进行调试时,崩溃位置只是简单地报告为原始地址,没有任何符号信息或回溯。我想通过在第一个静态初始化对象的构造函数处放置一个断点来确定这两个问题中的哪一个,但我不知道如何判断是哪个对象。

【问题讨论】:

  • 你试过用“-g3”标志重新编译吗?这应该会放入大量调试符号供您使用。
  • 链接器决定了所有编译单元的最终排序。我相信 g++ 有一些编译指示可能有助于定义顺序。
  • 答案是高度特定于平台的,您已经设法为您的平台保密。请公开它,以及您使用的 GDB 版本。
  • 另外,请显示您获得的 GDB 堆栈跟踪。很可能包含重要线索。
  • 只是回答这些问题,它是 Linux/g++,实际上没有任何堆栈跟踪(只有一个内存地址),即使对程序和库进行了全面调试包括最终成为问题的图书馆。我仍然不知道为什么会这样。

标签: c++ gdb initialization g++ segmentation-fault


【解决方案1】:

您能否在静态空间中初始化虚拟变量,并在这些函数调用上设置断点?

extern "C" int breakOnMe () { return 0 };

int break1 = breakOnMe ();
float pi = 3.1415;
int break2 = breakOnMe ();
myClass x = myClass (1, 2, 3);

然后在gdb 中运行break breakOnMe,然后再执行程序。这应该让 gdb 在每个静态初始化之前暂停。

我认为这应该可行..我对 gdbbing 有点生疏了。

【讨论】:

  • 是否保证非对象静态变量总是先于静态对象初始化?
  • 在 Linux 中,POD 类型将通过将它们放在数据部分进行初始化 - 这些将在执行任何用户代码之前加载。
  • 但是你不能在数据部分中断,所以问题是如果通过调用函数来初始化 POD 类型(如 eduffy 示例中的 break1 和 break2),是保证在对象的构造函数被调用之前发生?
  • @Tyler,不,它们像构造函数调用一样被初始化。在我有限的测试中,它们似乎是按声明顺序初始化的,但这可能无法保证。
  • @bdonlan:在一个编译单元内。此订单是有保证的。
【解决方案2】:

在 Linux 上的 G++ 中,静态构造函数和析构函数的顺序由 .ctors 和 .dtors 部分中的函数指针确定。请注意,如果有足够的调试可用,您实际上可以获得回溯:

(gdb) bt
#0  0xb7fe3402 in __kernel_vsyscall ()
#1  0xb7d59680 in *__GI_raise (sig=6)
    at ../nptl/sysdeps/unix/sysv/linux/raise.c:64
#2  0xb7d5cd68 in *__GI_abort () at abort.c:88
#3  0x08048477 in foo::foo() ()
#4  0x0804844e in __static_initialization_and_destruction_0(int, int) ()
#5  0x0804846a in global constructors keyed to foo_inst ()
#6  0x0804850d in __do_global_ctors_aux ()
#7  0x08048318 in _init ()
#8  0x080484a9 in __libc_csu_init ()
#9  0xb7d4470c in __libc_start_main (main=0x8048414 <main>, argc=1,
    ubp_av=0xbfffcbc4, init=0x8048490 <__libc_csu_init>,
    fini=0x8048480 <__libc_csu_fini>, rtld_fini=0xb7ff2820 <_dl_fini>,
    stack_end=0xbfffcbbc) at libc-start.c:181
#10 0x08048381 in _start () at ../sysdeps/i386/elf/start.S:119

这是安装了 libc 和 libstdc++ 的调试符号。如您所见,这里的崩溃发生在静态对象 foo_inst 的 foo::foo() 构造函数中。

如果你想中断初始化过程,你可以在 __do_global_ctors_aux 上设置一个断点并逐步完成它的反汇编,我想。或者只是等待它崩溃以获取像上面这样的回溯。

【讨论】:

  • 一些 平台上,这个答案是正确的。在其他平台上是错误的。您可能不应该假设“整个世界都是由 ELF 二进制文件组成的”。
【解决方案3】:

Matthew Wilson 在Imperfect C++this section(需要Safari Books Online 订阅)中提供了一种回答此问题的方法。 (顺便说一句,好书。)总而言之,他创建了一个CUTrace.h 标头,该标头创建了一个类的静态实例,该实例在创建时打印包含源文件的文件名(使用非标准预处理器宏__BASE_FILE__),然后他在每个源文件中都包含CUTrace.h

这需要重新编译,但 #include "CUTrace.h" 可以通过脚本轻松添加和删除,因此设置起来应该不会太难。

【讨论】:

  • 非常聪明的想法。我今天晚些时候会试试这个,如果可行,可能会接受这个答案。
  • 虽然这本身并不能完全回答我的问题,但它确实解决了激发问题的问题。使用这种方法,我能够看到问题发生在我的应用程序中的任何静态初始化之前,并且通过修改它用来做同样事情的库,我能够确定哪个库有问题。然后更容易找到静态初始化错误,因为该库的静态初始化比我的应用少得多。
【解决方案4】:

g++ 对此提供了一些帮助。
它不是便携式的,但我确信这不是您的主要问题。

http://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Attributes.html#C_002b_002b-Attributes

【讨论】:

    【解决方案5】:

    您可以使用 question 突出显示的模板找到初始化 TU 的顺序。它需要对您感兴趣的每个 TU 进行少量代码更改:

    // order.h
    //
    
    #ifndef INCLUDED_ORDER
    #define INCLUDED_ORDER
    
    #include <iostream>
    
    inline int showCountAndFile (const char * file)
    {
      static int cnt = 0;
      std::cout << file << ": " << cnt << std::endl;
      ++cnt;
      return cnt;
    }
    
    template <int & i>
    class A {
      static int j;
    };
    
    template <int & i>
    int A<i>::j = showCountAndFile (SRC_FILE);
    
    namespace
    {
      int dummyGlobal;
    }
    template class A<dummyGlobal>;
    
    #endif
    

    基本思想是每个 TU 将具有不同的 dummyGlobal 唯一地址,因此模板将在每个 TU 中具有不同的实例化。静态成员的初始化导致调用“showCountAndFile”,然后打印出 SRC_FILE(在 TU 中设置)和 cnt 的当前值,因此将显示顺序。

    您可以按如下方式使用它:

    static const char * SRC_FILE=__FILE__;
    #include "order.h"
    
    int main ()
    {
    }
    

    【讨论】:

      【解决方案6】:

      实际上,通过使用单例,您可以在 C++ 中非常有效地控制全局/静态对象的初始化顺序。

      例如,假设您有:

      class Abc
      {
      public:
          void foo();
      };
      

      以及在全局范围内定义的相应对象:

      Abc abc;
      

      那么你有一个类:

      class Def
      {
      public:
          Def()
          {
              abc.foo();
          }
      };
      

      其中也有一个在全局范围内定义的对象:

      Def def;
      

      在这种情况下,您无法控制初始化顺序,如果首先初始化 def,那么您的程序很可能会崩溃,因为它正在调用尚未初始化的 Abc 上的 foo() 方法。

      解决方案是让全局范围内的函数执行如下操作:

      Abc& abc()
      {
          static Abc a;
          return a;
      }
      

      然后 Def 看起来像:

      class Def
      {
      public:
          Def()
          {
              abc().foo();
          }
      };
      

      这样,abc 总是保证在使用之前被初始化,因为这将在第一次调用 abc() 函数期间发生。同样,您应该对 Def 全局对象执行相同的操作,以使其也不会有任何意外的初始化依赖项。

      Def& def()
      {
          static Def d;
          return d;
      }
      

      如果你需要严格控制初始化的顺序,除了简单地确保一切在使用之前都被初始化,那么将所有全局对象放在一个全局单例中,如下所示。

      struct Global
      {
          Abc abc;
          Def def;
      };
      
      Global& global()
      {
          static Global g;
          return g;
      }
      

      并将这些项目引用如下:

      //..some code
      global().abc.foo();
      //..more code here
      global().def.bar();
      

      无论哪一个首先被调用,C++ 成员初始化规则都将保证 abc 和 def 对象按照它们在 Global 类中定义的顺序进行初始化。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-07-12
        • 2010-09-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多