158

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

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

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

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

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

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

(请同时说明你的理由)

引用MSDN

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

这里开始

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

谢谢。

4

3 回答 3

207

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

无论:

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

所以,这里的关键问题是真正的可见性

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

当你有extern全局变量时,它变得复杂。在这里,Windows 和类 Unix 系统完全不同。

对于 Windows(.exe 和 .dll),extern全局变量不是导出符号的一部分。换句话说,不同的模块根本不知道其他模块中定义的全局变量。这意味着,例如,如果您尝试创建一个应该使用externDLL 中定义的变量的可执行文件,您将收到链接器错误,因为这是不允许的。您需要提供具有该外部变量定义的目标文件(或静态库)并将其与可执行文件和 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++ 标准定义,这是特定于平台的领域,这意味着它可靠性/便携性要低得多)。

于 2013-10-15T06:02:00.730 回答
2

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

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

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

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

因此,与全局变量的外部导出有关的整个答案确实是题外话,而且是不必要的,甚至与原始问题无关。因为全局变量不需要访问外部变量,所以始终可以通过对库的函数调用间接访问全局变量。

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

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

于 2022-01-15T06:53:29.313 回答
0

在 unix 系统中:

需要注意的是,如果两个动态库导出相同的全局变量,链接器不会报错。但是在执行过程中,可能会根据访问冲突而出现段错误。表现出这种行为的通常数字是分段错误 15

segfault at xxxxxx ip xxxxxx sp xxxxxxx error 15 in a.out
于 2020-09-24T19:56:26.293 回答