11

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

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

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

4

6 回答 6

11

在 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 上设置一个断点并逐步完成它的反汇编,我想。或者只是等待它崩溃以获得像上面这样的回溯。

于 2009-08-03T20:28:07.790 回答
7

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

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

于 2009-08-03T20:39:10.157 回答
2

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

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 有点生疏。

于 2009-08-03T20:19:27.107 回答
1

您可以使用此问题突出显示的模板找到 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 ()
{
}
于 2009-08-04T08:29:37.703 回答
0

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

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

于 2009-08-03T22:31:41.740 回答
0

实际上,通过使用单例,您可以在 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;
}

如果除了简单地确保在使用之前初始化所有内容之外,还需要严格控制初始化顺序,请将所有全局对象放入 Global 单例中,如下所示。

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 类中定义的顺序进行初始化。

于 2011-03-06T09:05:53.350 回答