当动态链接共享库时,全局变量和静态变量会发生什么?

184

我试图理解当具有全局变量和静态变量的模块与应用程序动态链接时会发生什么。

所谓模块,就是解决方案中的每个项目(我经常使用Visual Studio!)。这些模块可以构建成*.lib或*.dll,也可以构建成*.exe本身。

我了解到应用程序的二进制文件包含所有单独的翻译单元(目标文件)中的全局和静态数据,这些数据位于数据段中(如果是const,则为只读数据段)。

  • 当此应用程序使用采用加载时动态链接的模块A时会发生什么?我假设DLL拥有其全局变量和静态变量的一个部分。操作系统是否会加载它们?如果是,它们会被加载到哪里?

  • 当应用程序使用运行时动态链接的模块B时会发生什么?

  • 如果我的应用程序中有两个模块都使用A和B,那么A和B的全局副本是否会像下面提到的那样被创建(如果它们是不同的进程)?

  • DLL A和B是否可以访问应用程序的全局变量?

(请陈述你的理由)

引用自MSDN:

在DLL源代码文件中声明为全局变量的变量由编译器和链接器视为全局变量,但加载给定DLL的每个进程都会得到该DLL的全局变量的副本。静态变量的范围限于声明静态变量的块。因此,默认情况下,每个进程都有其自己的DLL全局和静态变量实例。

引自这里:

在动态链接模块时,不清楚不同的库是否拥有它们自己的全局实例,还是全局变量是共享的。

谢谢。


3
你可能指的是“库”而不是“模块”。目前有一个提案,旨在将“模块”添加到C++标准中,并给出更精确的定义以及与常规库不同的语义。 - David Rodríguez - dribeas
啊,我应该澄清一下。我认为解决方案中的不同项目(我经常使用Visual Studio)是模块。这些模块被构建成*.lib或*.dll文件。 - Raja
6
“模块”一词是指独立的(完全链接的)可执行文件,包括可执行程序、动态链接库(.dll)或共享对象(.so),这是正确的技术术语。在这里使用“模块”一词是完全恰当的,并且其含义正确且被广泛理解。除非有一个名为“模块”的标准特性,否则它的定义仍然是传统的,如我所解释的那样。 - Mikael Persson
3个回答

226

这是Windows和类Unix系统之间非常著名的区别。

无论如何:

  • 每个进程都有自己的地址空间,这意味着永远不会在进程之间共享内存(除非您使用一些进程间通信库或扩展)。
  • 仍然适用单一定义规则(ODR),这意味着您只能在链接时看到一个全局变量的定义(静态或动态链接)。

所以,关键问题实际上是可见性

在所有情况下,static全局变量(或函数)从未从模块(dll / so或可执行文件)外部可见。 C ++标准要求这些具有内部链接,这意味着它们在定义它们的翻译单位(成为对象文件)之外不可见。所以,这解决了那个问题。

当存在extern全局变量时,情况变得复杂起来。这里,Windows和类Unix系统完全不同。

在Windows(.exe 和 .dll)的情况下,extern 全局变量不属于导出符号的一部分。换句话说,不同模块无法意识到其他模块中定义的全局变量。这意味着,如果您尝试创建一个应该使用在DLL中定义的extern变量的可执行文件,则会获得链接器错误,因为这是不允许的。您需要提供一个带有该extern变量的定义的对象文件(或静态库),并将其静态链接到两个可执行文件和DLL,从而产生两个不同的全局变量(一个属于可执行文件,一个属于 DLL)。

要在Windows中实际导出全局变量,您必须使用类似于函数导出/导入语法的语法,例如:

#ifdef COMPILING_THE_DLL
#define MY_DLL_EXPORT extern "C" __declspec(dllexport)
#else
#define MY_DLL_EXPORT extern "C" __declspec(dllimport)
#endif

MY_DLL_EXPORT int my_global;
当你这样做时,全局变量将被添加到导出符号列表中,可以像所有其他函数一样链接。
在类Unix环境(如Linux)的情况下,称为“共享对象”的动态库使用扩展名为.so,导出所有extern 全局变量(或函数)。 在这种情况下,如果您从任何地方对共享对象文件进行加载时间链接,则全局变量是共享的,即作为一个链接在一起。 基本上,类Unix系统被设计成在静态或动态库链接之间几乎没有区别。 再次强调ODR适用于各种情况:一个extern全局变量将在模块间共享,这意味着它应该在加载的所有模块中具有唯一的定义。
最后,在Windows或类Unix系统的两种情况下,您可以对动态库进行运行时链接,即使用LoadLibrary() / GetProcAddress() / FreeLibrary()dlopen() / dlsym() / dlclose()。 在这种情况下,您必须手动获取指向想要使用的每个符号的指针,其中包括您想要使用的全局变量。 对于全局变量,您可以像处理函数一样使用GetProcAddress()dlsym(),前提是全局变量是导出符号列表的一部分(根据上面的规则)。
当然,作为必要的最后说明:应该避免使用全局变量。 我认为您引用的文本(关于事情“不清楚”)恰好指的是我刚刚解释的平台特定差异(动态库实际上并没有由C ++标准定义,这是平台特定的领域,这意味着它的可靠性/可移植性要低得多)。

6
非常感谢您的问题!我有一个后续问题:由于DLL是一个独立的代码和数据块,它是否具有类似可执行文件的数据段部分?我正在尝试理解当使用共享库时,这些数据被加载到哪里和如何加载。 - Raja
24
@Raja 是的,DLL有一个数据段。事实上,就文件本身而言,可执行文件和DLL几乎是相同的,唯一的真正区别是在可执行文件中设置了一个标志来表示它包含一个“主”函数。当进程加载DLL时,它的数据段被复制到进程的地址空间的某个地方,并且静态初始化代码(用于初始化非平凡全局变量)也在进程的地址空间内运行。加载方式与可执行文件相同,只是进程地址空间被扩展而不是创建新的地址空间。 - Mikael Persson
7
在类的内联函数中定义静态变量,比如在头文件中定义"class A{ void foo() { static int st_var = 0; } }",并将其包含在模块A和模块B中,A/B会共享同一个st_var还是每个模块都有自己的一个副本? - camino
4
如果这个类是被导出的(也就是使用 __attribute__((visibility("default"))) 定义),那么 A 和 B 将会共享同一个 st_var。但如果这个类是用 __attribute__((visibility("hidden"))) 定义的,那么模块 A 和模块 B 将拥有自己的 st_var 副本,不会共享。 - Wei Guo
1
@camino __declspec(dllexport) - ruipacheco
显示剩余4条评论

18
Mikael Persson留下的答案虽然非常详细,但包含了一个严重的错误(或者至少是误导性的),涉及到全局变量,需要澄清。原始问题询问是否存在全局变量的单独副本,或者全局变量是否在进程之间共享。
真正的答案如下:每个进程都有单独(多个)的全局变量副本,并且它们不在进程之间共享。因此,声明适用于ODR(One Definition Rule)也是非常误导人的,它并不适用于它们不是每个进程使用的相同全局变量,因此实际上它不是“进程之间”的“一个定义”。
此外,即使全局变量对进程不可见,它们对进程总是很容易“访问”,因为任何函数都可以轻松地将全局变量的值返回给进程,或者说,进程可以通过函数调用设置全局变量的值。因此,这个答案也是误导性的。
实际上,“是的”,进程确实可以完全“访问”全局变量,至少通过对库的函数调用。但是要重申的是,每个进程都有自己的全局变量副本,因此它不会是另一个进程正在使用的相同全局变量。

因此,与外部导出全局变量相关的整个答案都是离题的、不必要的,甚至与原始问题无关。因为全局变量不需要使用extern来访问,它们总是可以通过对库函数的调用间接地访问。

当然,唯一在进程之间共享的部分就是实际的“代码”。代码只加载在物理内存(RAM)中的一个位置,但当然该位置被映射到每个进程的“本地”虚拟内存位置。

相反,静态库已经将代码的副本嵌入到可执行文件(ELF、PE等)中的每个进程中,并且像动态库一样,为每个进程保留了单独的全局变量。


谢谢!我之前对于ODR和名称可见性与任何事情有什么关系感到非常困惑。 - John Cramerus

1
在Unix系统中:
需要注意的是,如果两个动态库导出相同的全局变量,链接器不会发出警告。但是,在执行过程中,可能会由于访问冲突而导致段错误。通常表现此行为的数字为15。
segfault at xxxxxx ip xxxxxx sp xxxxxxx error 15 in a.out

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