当一个共享库被动态链接时,它的全局变量和静态变量会发生什么变化?

170 人关注

我想了解当带有全局变量和静态变量的模块动态链接到一个应用程序时会发生什么。 我所说的模块是指解决方案中的每个项目(我经常用visual studio工作!)。这些模块要么被内置到*.lib或*.dll中,要么就是*.exe本身。

我知道,一个应用程序的二进制文件在数据段中包含所有单个翻译单元(对象文件)的全局和静态数据(如果有的话,还有只读数据段)。

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

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

  • 如果我的应用程序中有两个模块都使用A和B,那么A和B的globals的副本是否像下面提到的那样创建(如果它们是不同的进程)?

  • DLLs A和B是否可以访问应用程序的全局?

  • (请同时说明你的理由)

    Quoting from MSDN :

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

    并从 here :

    当动态连接模块时,可能不清楚不同的库是否有自己的globals实例,或者globals是否被共享。

    3 个评论
    By modules 你可能是指 libs .有一项建议是增加 modules 在C++标准中,对模块的定义更加精确,并且与目前的普通库有不同的语义。
    Raja
    啊,应该澄清一下的。我把一个解决方案中的不同项目(我经常使用visual studio)视为模块。这些模块被内置到*.lib或*.dll's中。
    @DavidRodríguez-dribeas 术语 "模块 "是独立(完全链接)可执行文件的正确技术术语,包括:可执行程序、动态链接库(.dll)或共享对象(.so)。它在这里是完全合适的,其含义也是正确的,而且很好理解。在有一个名为 "模块 "的标准功能之前,它的定义仍然是传统的,正如我解释的那样。
    c++
    linker
    global-variables
    global
    dynamic-linking
    Raja
    Raja
    发布于 2013-10-15
    3 个回答
    Mikael Persson
    Mikael Persson
    发布于 2020-09-25
    已采纳
    0 人赞同

    这是Windows和类Unix系统之间一个相当著名的区别。

    不管是什么。

  • Each process has its own address space, meaning that there is never any memory being shared between processes (unless you use some inter-process communication library or extensions).
  • The One Definition Rule (ODR) still applies, meaning that you can only have one definition of the global variable visible at link-time (static or dynamic linking).
  • 因此,这里的关键问题实际上是 visibility .

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

    当你有 extern 全局变量时,情况就变得复杂了。在这里,Windows和类Unix系统是完全不同的。

    在Windows(.exe和.dll)的情况下, extern 全局变量不属于导出的符号。换句话说,不同的模块根本不知道其他模块中定义的全局变量。这意味着,如果你试图创建一个应该使用定义在DLL中的 extern 变量的可执行文件,你会得到链接器错误,因为这是不允许的。你需要提供一个带有该外部变量定义的对象文件(或静态库),并将其静态链接到 both 可执行文件和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的系统,你可以做到run-time动态库的链接,即使用LoadLibrary()或/ 【替换代码9/ FreeLibrary()dlopen()。/ 【替换代码12/ dlclose()。在这种情况下,你必须手动获取你想使用的每个符号的指针,这包括你想使用的全局变量。对于全局变量,你可以像对待函数一样使用GetProcAddress()dlsym(),只要全局变量是导出的符号列表的一部分(根据前面几段的规则)。

    当然,作为一个必要的最后说明。应避免使用全局变量而且我相信你引用的那段文字(关于事情 "不清楚")正是指我刚才解释的平台特有的差异(动态库并不是由C++标准真正定义的,这是平台特有的领域,意味着它的可靠性/可移植性要差很多)。

    Raja
    Great answer, thank you! I have a follow up: Since the DLL is a self-contained piece of code & data, does it have a data segment section similar to executables? I'm trying to understand where and how this data is loaded (to) when the shared library is used.
    @Raja 是的,DLL有一个数据段。事实上,就文件本身而言,可执行文件和DLLs几乎是相同的,唯一真正的区别是在可执行文件中设置一个标志,表示它包含一个 "main "函数。当一个进程加载一个DLL时,它的数据段被复制到进程的某个地址空间,静态初始化代码(将初始化非微观的全局变量)也在进程的地址空间内运行。加载的过程与可执行文件相同,只是进程的地址空间被扩展,而不是创建一个新的。
    例如,在头文件中定义 "class A{ void foo() { static int st_var = 0; }",并将其包含在模块A和模块B中。}" 在头文件中,并将其包含在模块A和模块B中,A/B将共享相同的st_var还是各自拥有自己的副本?
    @camino 如果该类是导出的(即用 __attribute__((visibility("default"))) 定义的),那么A/B将共享同一个st_var。 但如果该类是以 __attribute__((visibility("hidden"))) 定义的,那么模块A和模块B将有自己的副本,而不是共享。
    @camino __declspec(dllexport)
    Deckard 5 Pegasus
    Deckard 5 Pegasus
    发布于 2020-09-25
    0 人赞同

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

    真正的答案是以下几点。每个进程都有独立的(多个)全局变量副本,它们在进程之间是不共享的。 因此,说 "单一定义规则"(ODR)适用也是非常具有误导性的,它并不适用,因为它们并不是每个进程所使用的相同的全局变量,所以实际上它并不是 "单一定义" between processes .

    另外,尽管全局变量对进程来说是不 "可见 "的,但它们总是很容易被进程 "访问",因为任何函数都可以很容易地将全局变量的值返回给进程,或者说,进程可以通过函数调用设置全局变量的值。 因此,这个答案也有误导性。

    在现实中,"是的",进程确实可以完全 "访问 "globals,至少可以通过调用函数库来实现。 但要重申的是,每个进程都有它自己的globals副本,所以它不会是另一个进程正在使用的相同的globals。

    因此,与外部导出globals有关的整个答案确实偏离了主题,而且没有必要,甚至与最初的问题没有关系。 因为globals不需要extern来访问,globals总是可以通过函数调用库来间接访问。

    当然,各进程之间唯一共享的部分是实际的 "代码"。 代码只加载在物理内存(RAM)的一个地方,但同一物理内存位置当然被映射到每个进程的 "本地 "虚拟内存位置。

    相反,静态库在可执行文件(ELF、PE等)中已经包含了每个进程的代码副本,当然,像动态库一样,每个进程都有独立的globals。