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

Raj*_*aja 114 c++ linker global global-variables dynamic-linking

我试图理解当具有全局变量和静态变量的模块动态链接到应用程序时会发生什么.通过模块,我的意思是解决方案中的每个项目(我使用visual studio工作很多!).这些模块内置于*.lib或*.dll或*.exe本身.

我知道应用程序的二进制文件包含数据段中所有单个转换单元(目标文件)的全局和静态数据(如果是const,则只读取数据段).

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

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

  • 如果我的应用程序中有两个使用A和B的模块,则是如下所述创建的A和B的全局变量的副本(如果它们是不同的进程)?

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

(请说明你的理由)

引自MSDN:

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

这里:

当动态链接模块时,可能不清楚不同的库是否有自己的全局变量实例或者是否共享全局变量.

谢谢.

Mik*_*son 161

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

无论:

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

所以,这里的关键问题是能见度.

在所有情况下,static从模块外部(dll/so或可执行文件)永远不会看到全局变量(或函数).C++标准要求它们具有内部链接,这意味着它们在定义它们的转换单元(它成为目标文件)之外是不可见的.所以,这解决了这个问题.

变得复杂的地方就是你有extern全局变量.在这里,Windows和类Unix系统完全不同.

对于Windows(.exe和.dll),extern全局变量不是导出符号的一部分.换句话说,不同的模块无法识别其他模块中定义的全局变量.这意味着,如果您尝试创建应该使用externDLL中定义的变量的可执行文件,则会出现链接器错误,因为这是不允许的.您需要提供与外部变量的定义目标文件(或静态库),并用静态链接这两个可执行文件和DLL,从而导致两种不同的全局变量(一个属于可执行文件,一个属于DLL ).

要在Windows中实际导出全局变量,必须使用类似于函数export/import语法的语法,即:

#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;
Run Code Online (Sandbox Code Playgroud)

执行此操作时,全局变量将添加到导出符号列表中,并且可以像所有其他函数一样进行链接.

在类Unix环境(如Linux)的情况下,动态库称为"共享对象",扩展名.so导出所有extern全局变量(或函数).在这种情况下,如果从任何地方到共享对象文件进行加载时链接,则全局变量将被共享,即作为一个链接在一起.基本上,类似Unix的系统旨在使其与静态或动态库链接之间几乎没有区别.同样,ODR全面适用:extern全局变量将在模块之间共享,这意味着它应该在所有加载的模块中只有一个定义.

最后,在这两种情况下,对于Windows或类Unix系统,您可以进行动态库的运行时链接,即使用LoadLibrary()/ GetProcAddress()/ FreeLibrary()dlopen()/ dlsym()/ dlclose().在这种情况下,您必须手动获取指向您希望使用的每个符号的指针,其中包括您要使用的全局变量.对于全局变量,只要全局变量是导出符号列表的一部分(根据前面段落的规则),就可以使用GetProcAddress()或者dlsym()与函数相同.

当然,作为必要的最终说明:应该避免全局变量.我相信你引用的文字(关于"不清楚"的东西)正是指我刚才解释的特定于平台的差异(动态库并没有真正由C++标准定义,这是特定于平台的领域,这意味着它不太可靠/便携).

  • @Raja是的,DLL有一个数据段.实际上,就文件本身而言,可执行文件和DLL实际上是相同的,唯一真正的区别是在可执行文件中设置的标志,表示它包含"主"功能.当进程加载DLL时,其数据段被复制到进程的地址空间中,静态初始化代码(初始化非平凡的全局变量)也在进程的地址空间中运行.加载与可执行文件的加载相同,只是扩展了进程地址空间而不是创建新的进程地址空间. (16认同)
  • 很好的答案,谢谢!我有一个跟进:由于DLL是一个独立的代码和数据,它是否有一个类似于可执行文件的数据段部分?我试图了解在使用共享库时这些数据的加载位置和方式. (4认同)
  • 如何在类的内联函数中定义静态变量?例如,在头文件中定义"class A {void foo(){static int st_var = 0;}}"并将其包含在模块A和模块B中,A/B是否会共享相同的st_var,或者每个都有自己的副本? (2认同)
  • @camino 如果类是导出的(即用`__attribute__((visibility("default")))` 定义),那么A/B 将共享相同的st_var。但是如果类是用`__attribute__((visibility("hidden")))`定义的,那么模块A和模块B将有自己的副本,而不是共享的。 (2认同)

小智 21

Mikael Persson 留下的答案虽然非常彻底,但在全局变量方面包含一个严重的错误(或至少是误导性的),需要清除。最初的问题询问全局变量是否有单独的副本,或者全局变量是否在进程之间共享。

真正的答案如下:每个进程都有单独的(多个)全局变量副本,并且它们不在进程之间共享。因此,通过声明单一定义规则(ODR)适用也是非常具有误导性的,它并不适用于它们不是每个进程使用的相同全局变量,因此实际上它不是进程之间的“单一定义” 。

此外,即使全局变量对进程不“可见”,..它们总是很容易被进程“访问”,因为任何函数都可以轻松地将全局变量的值返回给进程,或者就此而言,进程可以通过函数调用设置全局变量的值。因此这个答案也是有误导性的。

事实上,“是的”进程确实可以完全“访问”全局变量,至少可以通过对库的函数调用。但重申一下,每个进程都有自己的全局变量副本,因此它不会与另一个进程使用的全局变量相同。

因此,与全局变量外部导出相关的整个答案实际上是偏离主题的,并且是不必要的,甚至与原始问题无关。由于全局变量不需要 extern 来访问,因此始终可以通过对库的函数调用来间接访问全局变量。

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

相反,静态库具有已烘焙到可执行文件(ELF、PE 等)中的每个进程的代码副本,当然,像动态库一样,每个进程都有单独的全局变量。