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


127

我试图了解将具有全局变量和静态变量的模块动态链接到应用程序时会发生什么。所谓模块,是指解决方案中的每个项目(我在Visual Studio中工作很多!)。这些模块内置于* .lib或* .dll或* .exe本身中。

我了解到,应用程序的二进制文件在数据段中包含所有单个转换单元(目标文件)的全局和静态数据(如果为const,则为只读数据段)。

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

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

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

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

(请同时说明您的原因)

MSDN引用:

在DLL源代码文件中声明为全局的变量被编译器和链接器视为全局变量,但是加载给定DLL的每个进程都会获得该DLL全局变量的自己的副本。静态变量的范围仅限于声明静态变量的块。结果,每个进程默认都有自己的DLL全局和静态变量实例。

这里

动态链接模块时,不清楚不同的库是否具有自己的全局实例或是否共享全局实例。

谢谢。


3
对于模块,您可能指的是libs。有人建议将模块添加到C ++标准中,以更加精确的定义模块的含义,以及与目前的常规库不同的语义。
大卫·罗德里格斯(DavidRodríguez)-dribeas

嗯,应该澄清一下。我将解决方案中的不同项目(与Visual Studio一起工作)视为模块。这些模块内置于* .lib或* .dll中。
拉贾

3
@DavidRodríguez-dribeas术语“模块”是独立(完全链接)的可执行文件的正确技术术语,包括:可执行程序,动态链接库(.dll)或共享库(.so)。这在这里非常合适,其含义是正确的并且易于理解。正如我所解释的那样,在存在名为“模块”的标准功能之前,其定义仍然是传统功能。
Mikael Persson

Answers:


176

这是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;

这样做时,全局变量将添加到导出符号列表中,并且可以像所有其他函数一样链接。

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

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

当然,作为必要的最后说明:应避免使用全局变量。而且我相信您引用的文字(关于“不清楚的东西”)完全是指我刚刚解释过的特定于平台的差异(C ++标准并未真正定义动态库,这是特定于平台的领域,这意味着它更不可靠/便携式)。


5
好答案,谢谢!我有一个跟进:由于DLL是一段完整的代码和数据,它是否具有类似于可执行文件的数据段部分?我试图了解使用共享库时将数据加载到何处以及如何加载。
拉惹

18
@Raja是的,DLL有一个数据段。实际上,就文件本身而言,可执行文件和DLL实际上是相同的,唯一真正的区别是在可执行文件中设置的标志是说它包含“主要”功能。当进程加载DLL时,其数据段将复制到进程的地址空间中的某个位置,并且静态初始化代码(用于初始化非平凡的全局变量)也将在进程的地址空间内运行。加载与可执行文件相同,只是进程地址空间被扩展而不是创建一个新的地址空间。
Mikael Persson

4
在类的内联函数中定义的静态变量如何?例如,在头文件中定义“类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将拥有其自己的副本,而不是共享的。
魏国

1
@camino __declspec(dllexport)
ruipacheco
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.