查找 C++ 静态初始化顺序问题

Posted

技术标签:

【中文标题】查找 C++ 静态初始化顺序问题【英文标题】:Finding C++ static initialization order problems 【发布时间】:2010-09-25 00:33:10 【问题描述】:

我们在static initialization order fiasco 上遇到了一些问题,我正在寻找方法来梳理大量代码以找出可能出现的情况。有关如何有效地做到这一点的任何建议?

编辑:关于如何解决静态初始化顺序问题,我得到了一些很好的答案,但这不是我的问题。我想知道如何查找受此问题影响的对象。在这方面,埃文的回答似乎是迄今为止最好的;我不认为我们可以使用 valgrind,但我们可能有可以执行类似功能的内存分析工具。这只会在给定构建的初始化顺序错误的情况下发现问题,并且顺序可以随着每次构建而改变。也许有一个静态分析工具可以捕捉到这一点。我们的平台是在 AIX 上运行的 IBM XLC/C++ 编译器。

【问题讨论】:

Plus One 用于查找它们。几年前我向 GCC 提交了一个功能请求,但没有任何结果。 【参考方案1】:

初始化的求解顺序:

首先,这只是一个临时解决方法,因为您有一些全局变量,您正试图摆脱它们但还没有时间(您最终会摆脱它们,不是吗?: -)

class A

    public:
        // Get the global instance abc
        static A& getInstance_abc()  // return a reference
        
            static A instance_abc;
            return instance_abc;
        
;

这将保证它在第一次使用时被初始化并在应用程序终止时被销毁。

多线程问题:

C++11 确实保证这是线程安全的:

§6.7 [stmt.dcl] p4 如果控制在变量初始化的同时进入声明,则并发执行等待初始化完成。

但是,C++03 并没有官方保证静态函数对象的构造是线程安全的。所以从技术上讲,getInstance_XXX() 方法必须用临界区来保护。从好的方面来说,gcc 有一个显式补丁作为编译器的一部分,它保证每个静态函数对象即使在存在线程的情况下也只会被初始化一次。

请注意:不要使用double checked locking pattern 来尝试避免锁定成本。这在 C++03 中不起作用。

创作问题:

在创建时,没有问题,因为我们保证在使用之前创建它。

破坏问题:

在对象被销毁后访问对象存在潜在问题。仅当您从另一个全局变量的析构函数访问对象时才会发生这种情况(全局,我指的是任何非局部静态变量)。

解决方案是确保您强制执行销毁顺序。 请记住,破坏顺序与构造顺序完全相反。所以如果你在你的析构函数中访问对象,你必须保证对象没有被销毁。为此,您必须确保在构造调用对象之前完全构造该对象。

class B

    public:
        static B& getInstance_Bglob;
        
            static B instance_Bglob;
            return instance_Bglob;;
        

        ~B()
        
             A::getInstance_abc().doSomthing();
             // The object abc is accessed from the destructor.
             // Potential problem.
             // You must guarantee that abc is destroyed after this object.
             // To guarantee this you must make sure it is constructed first.
             // To do this just access the object from the constructor.
        

        B()
        
            A::getInstance_abc();
            // abc is now fully constructed.
            // This means it was constructed before this object.
            // This means it will be destroyed after this object.
            // This means it is safe to use from the destructor.
        
;

【讨论】:

我无法摆脱它们。我没有创造它们。我只是坚持找到它们的任务。据我所知,它们都是 const 对象,还不错。 双重检查锁定在 C++ 中工作得很好。 保证可以工作,但在实践中确实可以。两者是有区别的。 @coryan, @Martin York:不仅是序列点,还有诸如 CPU 指令重新排序、推测执行和跨多个 CPU 的缓存行失效之类的事情。使用线程 API,这是唯一确定的方法。 执行生命周期的绝招。请注意,如果我们使用 DI,我们将拥有:B(A&),这将巧妙地解决问题。当然不是默认可构造有其自身的问题,所以也许B(): mInstanceOfA(A::getInstance()) @coryan:你忘了加上“..until it break”。【参考方案2】:

我刚刚写了一些代码来追踪这个问题。我们有一个大小合适的代码库(1000 多个文件),在 Windows/VC++ 2005 上运行良好,但在 Solaris/gcc 上启动时崩溃。 我写了以下 .h 文件:

#ifndef FIASCO_H
#define FIASCO_H

/////////////////////////////////////////////////////////////////////////////////////////////////////
// [WS 2010-07-30] Detect the infamous "Static initialization order fiasco"
// email warrenstevens --> [initials]@[firstnamelastname].com 
// read --> http://www.parashift.com/c++-faq-lite/ctors.html#faq-10.12 if you haven't suffered
// To enable this feature --> define E-N-A-B-L-E-_-F-I-A-S-C-O-_-F-I-N-D-E-R, rebuild, and run
#define ENABLE_FIASCO_FINDER
/////////////////////////////////////////////////////////////////////////////////////////////////////

#ifdef ENABLE_FIASCO_FINDER

#include <iostream>
#include <fstream>

inline bool WriteFiasco(const std::string& fileName)

    static int counter = 0;
    ++counter;

    std::ofstream file;
    file.open("FiascoFinder.txt", std::ios::out | std::ios::app);
    file << "Starting to initialize file - number: [" << counter << "] filename: [" << fileName.c_str() << "]" << std::endl;
    file.flush();
    file.close();
    return true;


// [WS 2010-07-30] If you get a name collision on the following line, your usage is likely incorrect
#define FIASCO_FINDER static const bool g_psuedoUniqueName = WriteFiasco(__FILE__);

#else // ENABLE_FIASCO_FINDER
// do nothing
#define FIASCO_FINDER

#endif // ENABLE_FIASCO_FINDER

#endif //FIASCO_H

在解决方案的每个 .cpp 文件中,我添加了以下内容:

#include "PreCompiledHeader.h" // (which #include's the above file)
FIASCO_FINDER
#include "RegularIncludeOne.h"
#include "RegularIncludeTwo.h"

当你运行你的应用程序时,你会得到一个像这样的输出文件:

Starting to initialize file - number: [1] filename: [p:\\OneFile.cpp]
Starting to initialize file - number: [2] filename: [p:\\SecondFile.cpp]
Starting to initialize file - number: [3] filename: [p:\\ThirdFile.cpp]

如果您遇到崩溃,罪魁祸首应该在列出的最后一个 .cpp 文件中。至少,这将为您提供一个设置断点的好地方,因为这段代码应该是您要执行的代码的绝对第一个(之后您可以单步执行您的代码并查看所有正在初始化的全局变量)。

注意事项:

将“FIASCO_FINDER”宏放在尽可能靠近文件顶部的位置很重要。如果您将它放在其他一些#includes 之下,您将面临在识别您所在的文件之前崩溃的风险。

如果您使用的是 Visual Studio 和预编译的头文件,则可以使用查找和替换快速将此额外的宏行添加到 所有 .cpp 文件对话框用相同的文本加上 FIASCO_FINDER 行替换您现有的#include“precompiledheader.h”(如果您勾选“正则表达式,您可以使用“\n”插入多行替换文本)

李>

【讨论】:

这似乎对小型项目很有帮助。对我来说不幸的是,我有一个包含 1000 多个实现文件和数十万行代码的代码库:/ Chad - 通常有一种方法可以快速替换,即使在 1000 多个文件上也是如此(例如,如果您已经有预编译的头文件,您可以搜索/替换该头文件名称,或编写一个小脚本对所有文件进行替换)。我在我们的项目中做了前者,它有数百个文件。 @Chad 有了一个好的 grep 工具,比如grepWin,就可以用正则表达式来做到这一点。对于所有 *.cpp/cxx/cc 文件,将 [\s\S]*(整个文件)替换为 #include "HeaderContainingTheMacro.h"\nFIASCO_FINDER\n$0$0 是在放置序言后放回整个文件。【参考方案3】:

根据您的编译器,您可以在构造函数初始化代码处放置断点。在 Visual C++ 中,这是 _initterm 函数,它被赋予了要调用的函数列表的开始和结束指针。

然后进入每个函数以获取文件和函数名称(假设您已使用调试信息进行编译)。获得名称后,退出函数(返回到 _initterm)并继续直到 _initterm 退出。

这为您提供所有静态初始化程序,而不仅仅是代码中的初始化程序 - 这是获取详尽列表的最简单方法。您可以过滤掉您无法控制的那些(例如第三方库中的那些)。

该理论适用于其他编译器,但函数的名称和调试器的功能可能会改变。

【讨论】:

【参考方案4】:

也许使用 valgrind 来查找未初始化内存的使用情况。 “静态初始化顺序失败”的最佳解决方案是使用静态函数,该函数返回对象的实例,如下所示:

class A 
public:
    static X &getStatic()  static X my_static; return my_static; 
;

这种访问静态对象的方法是调用 getStatic,这将保证它在第一次使用时被初始化。

如果您需要担心反初始化的顺序,请返回一个新对象而不是静态分配的对象。

编辑:删除了多余的静态对象,我不知道为什么,但我在原始示例中混合并匹配了两种具有静态的方法。

【讨论】:

这个答案很好。并且还保证了反初始化:它与构造函数的完成相反,也跨翻译单元。所以一切都很好,我想。它甚至不会泄漏 @litb,C++ FAQ Lite 似乎不同意你的销毁顺序。见parashift.com/c++-faq-lite/ctors.html#faq-10.12。你很清楚这个标准——有什么东西可以证明马歇尔克莱恩错了吗? 8v) 我认为使用静态对象 ref 的问题是您最终可能会在程序终止期间引用死对象。 @Evan,我认为你的例子有些不对劲。 getStatic() 方法返回一个值,但它似乎与 something_static 成员无关。 something_static 有目的吗? 这样你就可以通过调用 getStatic 来访问 A::something_static 【参考方案5】:

有一些代码基本上“初始化”了编译器生成的 C++。当时找到此代码/调用堆栈的一种简单方法是创建一个静态对象,其中包含在构造函数中取消引用 NULL 的内容 - 在调试器中中断并进行一些探索。 MSVC 编译器设置了一个函数指针表,该表被迭代以进行静态初始化。您应该能够访问此表并确定程序中发生的所有静态初始化。

【讨论】:

【参考方案6】:

我们遇到了一些问题 静态初始化顺序惨败, 我正在寻找梳理的方法 通过一大堆代码找到 可能发生的情况。任何建议 如何有效地做到这一点?

这不是一个微不足道的问题,但如果您的代码有易于解析的中间格式表示,那么至少可以通过相当简单的步骤完成。

1) 找出所有具有非平凡构造函数的全局变量并将它们放入一个列表中。

2) 对于每个非平凡构造的对象,生成由其构造函数调用的整个势函数树。

3) 遍历非平凡构造函数树,如果代码引用了任何其他非平凡构造的全局变量(在您在第一步生成的列表中非常方便),您就有潜在的早期静态-初始化顺序问题。

4) 重复第 2 步和第 3 步,直到用完第 1 步中生成的列表。

注意:如果您有单个类的多个全局变量,则可以通过只访问每个对象类一次而不是每个全局实例一次来优化这一点。

【讨论】:

【参考方案7】:

用返回对函数中声明为静态对象的引用的全局函数替换所有全局对象。这不是线程安全的,因此如果您的应用程序是多线程的,您可能需要一些技巧,例如 pthread_once 或全局锁。这将确保一切都在使用之前进行初始化。

现在,要么你的程序可以工作(哇!),要么它处于无限循环中,因为你有一个循环依赖(需要重新设计),或者你继续下一个错误。

【讨论】:

【参考方案8】:

您需要做的第一件事是列出所有具有重要构造函数的静态对象。

鉴于此,您要么需要一次插入一个,要么只需将它们全部替换为单例模式对象。

单例模式受到很多批评,但懒惰的“按需”构造是解决现在和将来大多数问题的相当简单的方法。

老...

MyObject myObject

新...

MyObject &myObject()

  static MyObject myActualObject;
  return myActualObject;

当然,如果您的应用程序是多线程的,这可能会给您带来比一开始更多的问题...

【讨论】:

这不是单例模式。它有类似的实现,但不是一回事。 Scott Meyers 告诉我是 ;-) [有效 C++,第 2 版,P 222] 我把上面的'第一次使用构造'模式称为。澄清一下,单例模式确保一个类只有一个实例并提供对它的全局访问点。 “首次使用时构造”是/可以是提供单例实施的“全局访问点”部分的部分。但是,就其本身而言,并不能确保单个实例。 这是如何解决问题的?这是否给出了定义的 init/deinit 顺序? @paulm - 上面的“首次使用时构造”意味着您的对象在构造之前不能使用。 (典型的问题是不同编译单元中的两个全局对象)。而且它们以相反的顺序被破坏,所以是的,顺序是定义的。【参考方案9】:

Gimpel Software (www.gimpel.com) 声称他们的 PC-Lint/FlexeLint 静态分析工具可以检测到此类问题。

我对他们的工具有很好的经验,但在这个特定问题上没有,所以我不能保证他们会提供多少帮助。

【讨论】:

【参考方案10】:

其中一些答案现已过时。为了像我这样来自搜索引擎的人:

在 Linux 和其他地方,可以通过 Google 的AddressSanitizer 查找此问题的实例。

AddressSanitizer 是 LLVM 的一部分,从 3.1 版开始, GCC 的一部分,从 4.8 版开始

然后您将执行以下操作:

$ g++ -fsanitize=address -g staticA.C staticB.C staticC.C -o static 
$ ASAN_OPTIONS=check_initialization_order=true:strict_init_order=true ./static 
=================================================================
==32208==ERROR: AddressSanitizer: initialization-order-fiasco on address ... at ...
    #0 0x400f96 in firstClass::getValue() staticC.C:13
    #1 0x400de1 in secondClass::secondClass() staticB.C:7
    ...

更多详情请看这里: https://github.com/google/sanitizers/wiki/AddressSanitizerInitializationOrderFiasco

【讨论】:

【参考方案11】:

其他答案是正确的,我只是想补充一点,对象的 getter 应该在 .cpp 文件中实现,它不应该是静态的。如果您在头文件中实现它,则该对象将在您调用它的每个库/框架中创建......

【讨论】:

单一定义规则不能阻止这种情况的发生吗?【参考方案12】:

如果您的项目在 Visual Studio 中(我已经在 VC++ Express 2005 和 Visual Studio 2008 Pro 中尝试过):

    打开类视图(主菜单->视图->类视图) 在您的解决方案中展开每个项目并点击“全局函数和变量”

这应该会为您提供一份体面的列表,列出所有受惨败影响的全局变量。

最后,更好的方法是尝试从您的项目中删除这些对象(有时说起来容易做起来难)。

【讨论】:

谢谢,但您会在我的问题中看到我们的平台是 AIX 下的 IBM XL C/C++。 Visual Studio 不是一个选项。我同意最好摆脱它们;挑战在于找到它们。

以上是关于查找 C++ 静态初始化顺序问题的主要内容,如果未能解决你的问题,请参考以下文章

C++实现静态顺序表的增删查改以及初始化

静态变量初始化顺序

静态变量初始化顺序

Java的初始化块静态初始化块构造函数的执行顺序及用途探究

C++ 并发销毁

防止静态初始化命令“惨败”,C++