ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

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

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

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

(请同时说明你的理由)

引自 MSDN

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

并从 here

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

谢谢。

通过模块,您可能是指库。有一个提议将模块添加到 C++ 标准中,对模块的定义和语义比现在的常规库有更精确的定义。
啊,应该澄清一下。我将解决方案中的不同项目(我经常使用 Visual Studio)视为模块。这些模块内置于 *.lib 或 *.dll 中。
@DavidRodríguez-dribeas 术语“模块”是独立(完全链接)可执行文件的正确技术术语,包括:可执行程序、动态链接库(.dll)或共享对象(.so)。在这里用得恰到好处,意思是正确的,很好理解。正如我所解释的,在有一个名为“模块”的标准功能之前,它的定义仍然是传统的。

M
Mikael Persson

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

无论:

每个进程都有自己的地址空间,这意味着进程之间永远不会共享任何内存(除非您使用一些进程间通信库或扩展)。

单一定义规则 (ODR) 仍然适用,这意味着您只能在链接时看到全局变量的一个定义(静态或动态链接)。

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

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

当您有 extern 个全局变量时,情况会变得复杂。在这里,Windows 和类 Unix 系统完全不同。

对于 Windows(.exe 和 .dll),extern 全局变量不是导出符号的一部分。换句话说,不同的模块根本不知道其他模块中定义的全局变量。这意味着,例如,如果您尝试创建一个应该使用 DLL 中定义的 extern 变量的可执行文件,您将收到链接器错误,因为这是不允许的。您需要提供一个包含该外部变量定义的目标文件(或静态库),并将其与可执行文件和 DLLboth 静态链接,从而产生两个不同的全局变量(一个属于可执行文件和一个属于 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 系统,您都可以对动态库进行 run-time 链接,即使用 LoadLibrary() / GetProcAddress() / FreeLibrary() 或 { 4} / dlsym() / dlclose()。在这种情况下,您必须手动获取指向您希望使用的每个符号的指针,其中包括您希望使用的全局变量。对于全局变量,您可以像使用函数一样使用 GetProcAddress()dlsym(),前提是全局变量是导出符号列表的一部分(根据前面段落的规则)。

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


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

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

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

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

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

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

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

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


谢谢!我很困惑 ODR 和名称可见性与任何事情有什么关系。
c
choppe

在 unix 系统中:

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

segfault at xxxxxx ip xxxxxx sp xxxxxxx error 15 in a.out