静态变量被初始化两次。

11
考虑我在编译单元中有一个静态变量,最终会被打包成静态库libA。然后,我有另一个编译单元访问此变量,最终会被打包成共享库libB.so(所以必须将libA链接到libB中)。最后,我还有一个主函数直接从A访问静态变量,并且依赖于libB(因此我同时链接了libA和libB)。
然后我观察到,静态变量被初始化两次,也就是说它的构造函数运行了两次! 这似乎不对。难道链接器不能识别这两个变量为同一个并将它们优化为一个吗?
为了让我的困惑更加完美,我发现它使用相同的地址运行了两次!所以也许链接器确实识别了它,但没有在静态初始化和销毁代码中删除第二个调用?
以下是一个示例:
ClassA.hpp:
#ifndef CLASSA_HPP
#define CLASSA_HPP

class ClassA
{
public:
    ClassA();
    ~ClassA();
    static ClassA staticA;

    void test();
};

#endif // CLASSA_HPP

ClassA.cpp:

#include <cstdio>
#include "ClassA.hpp"

ClassA ClassA::staticA;

ClassA::ClassA()
{
    printf("ClassA::ClassA() this=%p\n", this);
}

ClassA::~ClassA()
{
    printf("ClassA::~ClassA() this=%p\n", this);
}

void ClassA::test()
{
    printf("ClassA::test() this=%p\n", this);
}

ClassB.hpp:

#ifndef CLASSB_HPP
#define CLASSB_HPP

class ClassB
{
public:
    ClassB();
    ~ClassB();

    void test();
};

#endif // CLASSB_HPP

ClassB.cpp:

 #include <cstdio>
 #include "ClassA.hpp"
 #include "ClassB.hpp"

 ClassB::ClassB()
 {
     printf("ClassB::ClassB() this=%p\n", this);
 }

 ClassB::~ClassB()
 {
     printf("ClassB::~ClassB() this=%p\n", this);
 }

 void ClassB::test()
 {
     printf("ClassB::test() this=%p\n", this);
     printf("ClassB::test: call staticA.test()\n");
     ClassA::staticA.test();
 }

Test.cpp:

#include <cstdio>
#include "ClassA.hpp"
#include "ClassB.hpp"

int main(int argc, char * argv[])
{
    printf("main()\n");
    ClassA::staticA.test();
    ClassB b;
    b.test();
    printf("main: END\n");

    return 0;
}

我接下来按照以下步骤进行编译和链接:

g++ -c ClassA.cpp
ar rvs libA.a ClassA.o
g++ -c ClassB.cpp
g++ -shared -o libB.so ClassB.o libA.a
g++ -c Test.cpp
g++ -o test Test.cpp libA.a libB.so

输出结果为:

ClassA::ClassA() this=0x804a040
ClassA::ClassA() this=0x804a040
main()
ClassA::test() this=0x804a040
ClassB::ClassB() this=0xbfcb064f
ClassB::test() this=0xbfcb064f
ClassB::test: call staticA.test()
ClassA::test() this=0x804a040
main: END
ClassB::~ClassB() this=0xbfcb064f
ClassA::~ClassA() this=0x804a040
ClassA::~ClassA() this=0x804a040

可以有人解释一下这里正在发生什么吗?链接器在做什么?同一个变量怎么可能被初始化两次?


1
相关(可能是重复的):https://dev59.com/cGw15IYBdhLWcg3wSJvQ - jogojapan
1
这可能与编译静态库,然后使用它来编译共享库有关吗?这样libB.so中就会有来自ClassA.o和ClassB.o的init代码? - heksesang
@heksesang:是的,这只发生在这种情况下。如果我将AB都设置为静态库或共享库,则不会出现此问题(A的构造函数仅运行一次)。但是,我希望链接器能够识别和消除重复的符号和初始化调用。我的假设是错误的还是链接器有问题? - bselu
2个回答

9
您正在将libA.a包含到libB.so中。这样做,libB.solibA.a都包含定义静态成员的ClassA.o
在您指定的链接顺序中,链接器从静态库libA.a中拉取ClassA.o,因此ClassA.o的初始化代码在main()之前运行。当访问动态libB.so中的第一个函数时,会运行libB.so所有初始化程序。由于libB.so包括ClassA.o,必须再次运行ClassA.o的静态初始化程序。
可能的解决方法:
  1. Don't put ClassA.o into both libA.a and libB.so.

    g++ -shared -o libB.so ClassB.o
    
  2. Don't use both libraries; libA.a is not needed.

    g++ -o test Test.cpp libB.so
    
应用以上解决方案之一即可解决问题:
ClassA::ClassA() this=0x600e58
main()
ClassA::test() this=0x600e58
ClassB::ClassB() this=0x7fff1a69f0cf
ClassB::test() this=0x7fff1a69f0cf
ClassB::test: call staticA.test()
ClassA::test() this=0x600e58
main: END
ClassB::~ClassB() this=0x7fff1a69f0cf
ClassA::~ClassA() this=0x600e58

关于修复1: 如果我不把 libA.a 放进 libB.so,那么最终会导致 libB.so 具有对静态库的隐式依赖。因此,如果我交付 libB.so 并忘记了 libA.a,接收者将获得未解决的符号,并且必须认为我提供了一个不完整的库。在这里我们能说这是一个健全的库吗?关于修复2: 如果 Test.cpp 引用了 libA.a 中未被 libB.so 使用的符号,则此方法无效。 在将 libA.a 链接到 libB.so 时,链接器会放弃 libA.a 未使用的符号。 因此,我仍然需要将可执行文件链接到 libA.a - bselu
你能把libA.a转换成libA.so并且同时提供这两个库吗?这可能是最简单的解决方案。更困难的方法是改写你的库。每个目标文件应该只存在于一个库中,用于最终链接阶段。 - Jay West

7

请问有人能解释这里发生了什么吗?

这很复杂。

首先,你把主可执行文件和共享库链接起来的方式导致了ClassA.cpp中所有代码,包括staticA在内,出现了两份:一份在主可执行文件中,另一份在libB.so中。

你可以通过运行以下命令进行确认:

nm -AD ./test ./libB.so | grep staticA

那么,ClassA的构造函数对于这两个实例运行两次并不奇怪,但仍然令人惊讶的是this指针是相同的(并且对应于主可执行文件中的staticA)。这是因为运行时加载器尝试模拟与存档库链接的行为,并将所有对staticA的引用绑定到它观察到的第一个全局导出实例(即在test中的实例)上。
那么该怎么解决呢?这取决于staticA实际代表的是什么。如果它是某种单例,在任何程序中只应存在一次,则简单的解决方案是确保只有一个staticA实例。而实现这一点的方法是要求使用libB.so的任何程序也链接到libA.a,而不是将libB.so链接到libA.a。这将消除libB.so中的sttaicA实例。你声称“必须将libA链接到libB”,但这种说法是错误的。
另一种选择是,如果您构建libA.so而不是libA.a,那么您可以将libB.so链接到libA.so(因此libB.so是自包含的)。如果主应用程序也链接到libA.so,那就不是问题了:无论使用该库多少次,libA.so中只会有一个staticA实例。
另一方面,如果staticA表示某种内部实现细节,并且您可以接受它们存在两个实例(只要它们彼此不干扰),则解决方案是将所有ClassA符号标记为隐藏可见性,如这个答案建议的那样。 更新:

为什么链接器不会从可执行文件中消除第二个staticA实例。

因为链接器执行你告诉它要做的事情。如果你将链接命令行更改为:
g++ -o test Test.cpp libB.so libA.a

如果链接器不将ClassA链接到主可执行文件中,则应该按照命令行库的顺序来理解其原因,阅读此文


我还是不明白,为什么链接器没有从可执行文件中消除第二个staticA实例。理论上应该是可能的。 - bselu
好的,我明白了。看了你的更新后,我最初认为改变链接顺序可能会解决我的问题。然而,它并没有。如果我在示例中添加另一个访问staticA并链接到libA.alibC.so,那么构造函数将再次被调用两次。我唯一合理的解决方案是不将libA.a链接到libB.so中。但是,这样libB.so就有了对静态库的隐式(不可见)依赖。这样做可以吗? - bselu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接