【问题标题】:Multiple initialization of Static library members when in multiple shared libraries在多个共享库中时静态库成员的多次初始化
【发布时间】:2018-12-15 11:55:18
【问题描述】:

请注意,这不是 Multiple instances of singleton across shared libraries on Linux 的重复项,因为添加 -rdynamic 标志不会改变问题中的描述。

我有一个静态 c++ 库,其中包含一些静态初始化代码(对于需要在未命名的命名空间中调用构造函数的变量)。 然后我有 2 个(或更多)共享库在内部使用该静态库。

使用这两个共享库的任何可执行文件都将调用静态库的初始化代码两次,从而使 folly::Singleton 或 gflags 全局变量等行为异常。 这是预期的行为吗?这是动态链接器的错误/弱点吗?

编辑:请注意,在 macOS 上它只初始化一次。

示例:

静态.h

int get();

静态.cpp

static int y = 0;
struct C { C() : x(y++) {}; int x; };
namespace { C c; }
int get() { return c.x; }

shared.h

int shared_get();

shared.cpp

#include "static.h"
int shared_get() { return get(); }

shared2.h

int shared2_get();

shared2.cpp

#include "static.h"
int shared2_get() { return get(); }

main.cpp

#include "shared.h"
#include "shared2.h"
#include <iostream>
int main() {
    std::cout << shared_get() << " " << shared2_get() << std::endl;
    return 0;
}

编译:

g++ -fPIC -g -c  -o static.o static.cpp
ar rcs libstatic.a static.o
g++ -g -fPIC -shared -o libshared.so shared.cpp ./libstatic.a
g++ -g -fPIC -shared -o libshared2.so shared2.cpp ./libstatic.a
g++ main.cpp ./libshared.so ./libshared2.so

运行:

LD_LIBRARY_PATH=. ./a.out

结果是“1 1”,而我认为它是“0 0”。

看看nmlibshared.solibshared2.so的符号都包含:

t _GLOBAL__sub_I_static.cpp

仅将静态库链接到可执行文件可以解决问题,但没有解释两个共享库如何在内部使用静态库而不会相互干扰。为了完成这里是如何获得“0 0”:

编译:

g++ -fPIC -g -c  -o static.o static.cpp
ar rcs libstatic.a static.o
g++ -g -fPIC -shared -o libshared.so shared.cpp
g++ -g -fPIC -shared -o libshared2.so shared2.cpp
g++ main.cpp ./libshared.so ./libshared2.so ./libstatic.a

【问题讨论】:

  • 每个共享库都有自己的静态库副本。他们彼此不了解。
  • @PeteBecker 即使有两个副本,它们不应该都有自己的yc,而且c.x 不应该都等于0吗?
  • @Leonard:请发一个minimal reproducible example(也就是main的代码)。
  • @Aconcagua — 是的;我没有仔细阅读这个问题。
  • @PeteBecker 在同时加载之前,他们并不知道彼此:例如get 的符号不会被加载两次,只会使用第一个加载的符号,正确的?所以我有点疑惑为什么对初始化的调用也被调用了两次。

标签: c++ compilation linker shared-libraries static-initialization


【解决方案1】:

为了进一步简化问题,您得到的结果等同于

静态.cpp:

static int y = 0;
struct C { C() : x(y++) {}; int x; };
static C c; 
int get() { return c.x; }

shared1.cpp:

#include "static.h"
#include "static.cpp"
int shared1_get() { return get(); }

shared2.cpp:

#include "static.h"
#include "static.cpp"
int shared2_get() { return get(); }

(IOW,2 个 DSO 中的每一个都将包含静态库的所有代码,因此我们可以跳过静态库部分以使示例更简单。)

你的情况

static C c; 

本质上是对struct C 的构造函数的每个 DSO 调用,在您的情况下(在结构上没有隐藏属性)被导出。

由于您有两个带有static C c; 的库,c 的构造函数将在每个 DSO 中调用一次,并且由于构造函数符号已导出(默认),因此其中一个构造函数将获胜(连同其关联的 @ 987654328@) 并将其拖拽。

基本上,将使用static C c; 中的一个,并将构造两次。这可能听起来很奇怪,但就共享库而言,您应该考虑使用 C,而不是 C++,并将 static C c; 视为

/*C*/
static int y;
static struct C c;
__attribute__((constructor))
void C__ctor(struct C *this) /*exported function*/
{
    C.x=y++;
}

要解决此问题,您可以将整个 C 结构标记为隐藏,以便 属性也适用于构造函数

struct __attribute__((visibility("hidden")))  C { C() : x(y++) {}; int x; };

或者更好的是,使用-fvisibility=hidden 编译并应用visibility("default") 明确的属性。

下面是一个可执行(shell 脚本)示例:

#!/bin/sh -eu
echo 'int get();' > static.h
cat > static.cpp <<EOF
static int y = 0;
#if 1 /*toggle to play with the visibility attribute*/
    #define MAYBE_HIDDEN __attribute__((visibility("hidden")))
#else
    #define MAYBE_HIDDEN
#endif
struct MAYBE_HIDDEN C { C() : x(y++) {}; int x; };
static C c; 
int get() { return c.x; }
EOF
cat > shared.h <<EOF
int shared_get();
EOF

cat > shared.cpp <<EOF
#include "static.h"
#include "static.cpp"
int shared_get() { return get(); }
EOF

cat > shared2.h <<EOF
int shared2_get();
EOF

cat > shared2.cpp <<EOF
#include "static.h"
#include "static.cpp"
int shared2_get() { return get(); }
EOF
cat > main.cpp <<EOF
#include "shared.h"
#include "shared2.h"
#include <iostream>
int main() {
    std::cout << shared_get() << " " << shared2_get() << std::endl;
    return 0;
}
EOF
g++ -fPIC -g -c  -o static.o static.cpp
#ar rcs libstatic.a static.o
g++ -g -fPIC -shared -o libshared.so shared.cpp #./libstatic.a
g++ -g -fPIC -shared -o libshared2.so shared2.cpp #./libstatic.a
g++ main.cpp ./libshared.so ./libshared2.so
./a.out

我已经跳过了静态库,并直接包含了 C++ 代码,但你也可以恢复它——它不会改变结果。

如果您在没有隐藏属性的情况下编译,您可能想尝试运行 nm -D libshared.so libshared1.so_ZN1CC1Ev_ZN1CC2Ev 符号应该是导出的构造函数。

【讨论】:

  • 我明白发生了什么,我主要是问为什么会这样。为什么加载第二个库时,全局符号被跳过,而其初始化(构造函数)代码却没有?我可以在标准或链接器中的何处找到所描述的行为?在 macos 上,动态链接器似乎更智能,因为它只调用一次初始化。
  • @Léonard 我必须参考 C++ 标准(这是我尽量避免的东西,我正在使用我自己的扩展 C),但我猜这可能是 Linux 的一个意外错误关于 C++ 的共享库实现。 macOS 没有这种行为的事实似乎证实了这一点。 AFAIK,您没有违反任何 C++ 法令,但平台上构造函数的 Linux 实现细节无意中泄露。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多