两个具有共享静态库的macOS dylib:为什么全局变量是共享的



令我惊讶的是,当编译碰巧共享一个静态库的两个单独的dylib时,该静态库中定义的全局变量似乎是共享的。这篇SO文章似乎表明,每个动态库都将保持其全局变量的独立性,但在上述情况下,下面包含的测试代码证明了这不是真的。我正在寻求确认,这是预期在macOS(可能还有Linux)。

在这种情况下:

  • 静态库"Foo";具有名为";Bar";;这是一个初始化为123的int
  • dylib";AAA";链接到";Foo">
  • dylib";BBB";链接到";Foo">
  • 应用程序";MyApp";链接到dylibs AAA和BBB
  • 应用程序调用dylib中的函数";AAA";修改Bar,将其设置为111
  • 应用程序调用dylib中的函数";AAA";打印Bar
  • 应用程序调用dylib中的函数";BBB";打印Bar

我预计当";AAA";印刷的";条形图";它将是111;BBB";打印出来的条形图仍然是123。相反,当";BBB";打印";Bar";,它是111,表明从MyApp的角度来看,只有一个"的共享实例;酒吧";。

我的怀疑是;条形图";被二者"暴露";AAA";以及";BBB";,当动态链接两个dylib时;获胜";因为名称完全相同,链接器无法区分两者。

这种怀疑似乎可以通过设置"-fvisibility=hidden"标志在";其它C++标志";在Xcode中。如果我这样做是为了"dylibs";AAA";以及";BBB";,那么这两个全局变量似乎是不同的。我认为这是因为"visibility=hidden"隐藏了"visibility"的两个副本;Bar";,从而解决了上文所述的冲突。

有人能证实我对此的理解吗?

---示例代码--

静态库CGlobalTest有一个C++类,如下所示。该类在函数内部声明一个全局,在.cpp文件中声明一个类全局和静态全局。函数GetGlobal()基于GlobalType参数返回对其中一个的引用。

CGlobalTest.cpp:

class CGlobalTest
{
public:
CGlobalTest() { }

static int& GetFunctionGlobal()
{
static int sFunctionGlobal = 123;
return sFunctionGlobal;
}

static int& GetClassGlobal()
{
return sClassGlobal;
}

static int& GetFileGlobal();

static int& GetGlobal(
GlobalType  inType)
{
switch (inType) {
case kFunctionGlobal:
return GetFunctionGlobal();
break;
case kClassGlobal:
return GetClassGlobal();
break;
case kFileGlobal:
return GetFileGlobal();
break;
}
}

static int  sClassGlobal;
};

CGlobalTest.h

#include "static_lib.h"
int CGlobalTest::sClassGlobal = 456;
int sFileGlobal = 789;
int&
CGlobalTest::GetFileGlobal()
{
return sFileGlobal;
}

然后我得到了两个使用CGlobalTest静态库的动态库,称为global_test_dynamic_1和global_test_dynamic_2。1和2的代码本质上是相同的,所以我只包括第一个。

dynamic_lib_1.cpp:

#include "dynamic_lib_1.h"
#include "static_lib.h"
#include "stdio.h"
const char*
GlobalTypeToString(
GlobalType  inType)
{
const char* type = "";
switch (inType) {
case kFunctionGlobal:
type = "Function Global";
break;
case kClassGlobal:
type = "Class Global";
break;
case kFileGlobal:
type = "File Global";
break;
}

return type;
}
void dynamic_lib_1_set_global(enum GlobalType inType, int value)
{
int& global = CGlobalTest::GetGlobal((GlobalType) inType);
global = value;
printf("Dynamic Lib 1: Set %s: %d (%p)n", GlobalTypeToString(inType), global, &global);
}
void dynamic_lib_1_print_global(enum GlobalType inType)
{
const int& global = CGlobalTest::GetGlobal((GlobalType) inType);
printf("Dynamic Lib 1: %s = %d (%p)n", GlobalTypeToString(inType), global, &global);
}

dynamic_lib_1.h

#ifdef __cplusplus
#define EXPORT extern "C" __attribute__((visibility("default")))
#else
#define EXPORT
#endif
#include "global_type.h"
EXPORT void dynamic_lib_1_set_global(enum GlobalType inType, int value);
EXPORT void dynamic_lib_1_print_global(enum GlobalType inType);

最后,还有一个链接到这两个dylib的应用程序。

#include "dynamic_lib_1.h"
#include "dynamic_lib_2.h"
#include "global_type.h"
#include <assert.h>
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>
typedef void (*print_func)(enum GlobalType inType);
typedef void (*set_func)(enum GlobalType inType, int value);
int main()
{
printf("App is starting up...n");
// LOAD DYNAMIC LIBRARY 1
void* handle1 = dlopen("libglobal_test_dynamic_1.dylib", RTLD_NOW);
assert(handle1 != NULL);
print_func d1_print = (print_func) dlsym(handle1, "dynamic_lib_1_print_global");
assert(d1_print != NULL);

set_func d1_set = (set_func) dlsym(handle1, "dynamic_lib_1_set_global");
assert(d1_set != NULL);

// LOAD DYNAMIC LIBRARY 2
void* handle2 = dlopen("libglobal_test_dynamic_2.dylib", RTLD_NOW);
assert(handle1 != NULL);
print_func d2_print = (print_func) dlsym(handle2, "dynamic_lib_2_print_global");
assert(d2_print != NULL);

set_func d2_set = (set_func) dlsym(handle2, "dynamic_lib_2_set_global");
assert(d2_set != NULL);

enum GlobalType type;

printf("**************************************************n");
printf("** FUNCTION GLOBALn");
printf("**************************************************n");

type = kFunctionGlobal;

(d1_print)(type);
(d2_print)(type);

printf("** SET D1 TO 111 - THEN PRINT FROM D2n");
d1_set(type, 111);
d1_print(type);
d2_print(type);
printf("** SET D2 TO 222 - THEN PRINT FROM D1n");
d2_set(type, 222);
d2_print(type);
d1_print(type);
printf("**************************************************n");
printf("** CLASS GLOBALn");
printf("**************************************************n");

type = kClassGlobal;

(d1_print)(type);
(d2_print)(type);

printf("** SET D1 TO 111 - THEN PRINT FROM D2n");
d1_set(type, 111);
d1_print(type);
d2_print(type);
printf("** SET D2 TO 222 - THEN PRINT FROM D1n");
d2_set(type, 222);
d2_print(type);
d1_print(type);
printf("**************************************************n");
printf("** FILE GLOBALn");
printf("**************************************************n");

type = kFileGlobal;

(d1_print)(type);
(d2_print)(type);

printf("** SET D1 TO 111 - THEN PRINT FROM D2n");
d1_set(type, 111);
d1_print(type);
d2_print(type);
printf("** SET D2 TO 222 - THEN PRINT FROM D1n");
d2_set(type, 222);
d2_print(type);
d1_print(type);
return 0;
}

这是一个特性,而不是bug。每个dylib中的符号被明确地标记为";弱聚结";(除出口外):

% xcrun dyld_info -fixups libglobal_test_dynamic_1.dylib 
libglobal_test_dynamic_1.dylib [arm64]:
-fixups:
segment      section          address                 type   target
__DATA_CONST __got            0x00004000              bind  weak-coalesce/__ZZN11CGlobalTest17GetFunctionGlobalEvE15sFunctionGlobal
__DATA_CONST __got            0x00004008              bind  libSystem.B.dylib/_printf
__DATA_CONST __const          0x00004010            rebase  0x00003F45
__DATA_CONST __const          0x00004018            rebase  0x00003F55
__DATA_CONST __const          0x00004020            rebase  0x00003F62

这使得动态链接器为整个进程中使用此符号的所有二进制文件选择一个单独的定义。

请注意,这必须明确地完成。默认情况下,Mach-Os使用两级命名空间,其中既有符号名称,也有希望在其中找到它的库。除此之外,库必须明确要求导入它知道已经在导出的符号。对于任何普通的符号(比如基本的函数定义),这都不会发生,库中的所有东西都只使用本地定义。

但这里的核心问题是包含静态变量的内联函数。C++标准有这样的说法("dcl.inline/6"):

具有外部链接的内联函数中的static局部变量总是引用同一对象。

这正是您的情况。如果将GetFunctionGlobal()的定义从标头移到源文件中,那么您将获得与GetFileGlobal()完全相同的行为。

它必须以这种方式工作,因为否则内联函数中的static局部变量就会被破坏,因为每个翻译单元都会获得该变量的自己的副本。

现在,据我所知,直到今天,动态链接仍然是定义了很多实现的,所以它相对于dylib的行为方式并不是标准规定的,但如果不以这种方式实现,它就会被破坏。因为考虑这样一种情况,即您有一个导出CGlobalTest类的动态库和一个导入它的二进制文件。由于函数定义仍在标头中,二进制文件现在可以获得静态局部变量的自己的副本(已损坏),也可以与库的副本(sane)别名。通过链接到你的静态库,你就可以做到:导出它。如果你不想导出它,那么-fvisibility=hidden就是这么做的。

最新更新