我试图了解当具有全局变量和静态变量的模块动态链接到应用程序时会发生什么。通过模块,我指的是解决方案中的每个项目(我经常使用Visual Studio!这些模块内置于 *.lib 或 *.dll 或 *.exe 本身中。
我知道应用程序的二进制文件包含数据段中所有单个翻译单元(对象文件)的全局和静态数据(如果 const,则只读数据段)。
-
当此应用程序使用具有加载时动态链接的模块 A 时会发生什么情况?我假设 DLL 有一个用于其全局变量和静态的部分。操作系统是否加载它们?如果是这样,它们被装载到哪里?
-
当应用程序使用具有运行时动态链接的模块 B 时会发生什么情况?
-
如果我的应用程序中有两个同时使用 A 和 B 的模块,那么 A 和 B 的全局变量的副本是否如下所述(如果它们是不同的进程)?
-
DLL A 和 B 是否可以访问应用程序全局变量?
(请同时说明您的原因)
引用自MSDN:
编译器和链接器将 DLL 源代码文件中声明为全局变量的变量视为全局变量,但加载给定 DLL 的每个进程都会获取该 DLL 的全局变量的自己的副本。静态变量的范围仅限于声明静态变量的块。因此,默认情况下,每个进程都有自己的 DLL 全局变量和静态变量实例。
从这里开始:
动态链接模块时,可能不清楚不同的库是否具有自己的全局实例,或者全局变量是否共享。
谢谢。
这是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++标准真正定义的,这是特定于平台的领域,这意味着它的可靠性/可移植性要低得多)。
Mikael Persson留下的答案虽然非常彻底,但在全局变量方面包含一个严重的错误(或至少是误导性的),需要澄清。 最初的问题是,是否存在全局变量的单独副本,或者全局变量是否在进程之间共享。
真正的答案如下:每个进程都有单独的(多个)全局变量副本,并且它们不会在进程之间共享。 因此,通过声明一个定义规则(ODR)适用也是非常具有误导性的,它不适用于它们不是每个进程使用的相同全局变量,因此实际上它不是进程之间的"一个定义"。
即使全局变量对过程不"可见",..它们总是很容易被进程"访问",因为任何函数都可以轻松地向进程返回全局变量的值,或者就此而言,进程可以通过函数调用设置全局变量的值。 因此,这个答案也具有误导性。
实际上,"是"进程确实对全局变量具有完全的"访问权限",至少是通过对库的函数调用。 但重申一下,每个进程都有自己的全局变量副本,因此它不会与另一个进程使用的全局变量相同。
因此,与全球外部输出有关的整个答案确实是题外话,是不必要的,甚至与原始问题无关。 由于不需要访问 extern,因此始终可以通过对库的函数调用间接访问全局变量。
当然,进程之间共享的唯一部分是实际的"代码"。 代码只加载在物理内存(RAM)中的一个位置,但相同的物理内存位置当然映射到每个进程的"本地"虚拟内存位置。
相反,静态库具有已烘焙到可执行文件中的每个进程的代码副本(ELF、PE 等),当然,像动态库一样,每个进程都有单独的全局变量。
在 unix 系统中:
需要注意的是,如果两个动态库导出相同的全局变量,链接器不会抱怨。 但在执行过程中,根据访问冲突,可能会出现段错误。表现出此行为的常见数字是分段错误 15
segfault at xxxxxx ip xxxxxx sp xxxxxxx error 15 in a.out