不同平台上,C/C++动态链接是如何工作的?

32

动态链接的工作原理是什么?

在Windows(使用LoadLibrary),你需要在运行时调用一个.dll文件,但在链接时,你需要提供相应的.lib文件,否则程序无法链接......那么.lib文件包含什么呢?.dll方法的描述吗?这不是头文件中包含的吗?

类似地,在*nix上,你不需要lib文件......那么编译器如何知道头文件中描述的方法在运行时将可用?

作为一名新手,当你考虑其中任何一种方案时,再考虑另一种方式,两种方案都没有意义......


6
不需要链接到任何特定的库,如果你使用LoadLibrary加载 DLL,这就是“动态加载”库的优点。其他平台也是如此。 - Some programmer dude
8
@JoachimPileborg 是的,但是OP(原帖发布者)对于为什么Windows上的动态库与.dll文件相关联却不像其他平台那样必要感到困惑,这是可以理解的。 - jalf
2
认为但不确定,Windows的.LIB文件包含存根定义,动态链接器会替换它们。但这并非必要。如果系统使用存根函数,它们可以在编译时被发射到可执行文件中。或者,可执行文件可以只有缺失的符号(就像一个目标文件),动态链接器会在启动时链接它们,就像静态链接器所做的那样。我认为这两种方法都已经在Unix上使用过。 - Adrian Ratnapala
2
不确定大多数人在这里获取他们的Windows信息,但你绝对不需要一个.lib文件来使用LoadLibrary或链接它的.lib文件,也不需要调用它的函数,这是完全错误的。 - paulm
3
据我所知,dlopen和dlsym与LoadLibrary/GetProcAddress相同,因此样板代码实际上是相同的? - paulm
显示剩余4条评论
9个回答

32
动态链接将一部分链接过程推迟到运行时。它可以以两种方式使用:隐式和显式。隐式地,静态链接器会将信息插入可执行文件中,这会导致库加载并解析必要的符号。显式地,您必须手动调用LoadLibrarydlopen,然后为每个需要使用的符号调用GetProcAddress/dlsym。隐式加载用于像系统库这样的东西,其中实现将取决于系统的版本,但接口是有保证的。显式加载用于像插件这样的东西,其中要加载的库将在运行时确定。 .lib文件仅对隐式加载必要。它包含库实际提供此符号的信息,因此链接器不会抱怨符号未定义,并告诉链接器在哪个库中找到这些符号,以便它可以插入必要的信息来自动加载此库。所有的头文件告诉编译器的只是符号将存在于某个地方;链接器需要.lib来知道在哪里。
在Unix下,所有信息都从.so中提取。为什么Windows需要两个单独的文件而不是把所有信息放在一个文件中,我不知道;实际上,这是重复大部分信息,因为.lib中所需的信息在.dll中也是必需的。(也许是许可问题。您可以使用.dll分发程序,但除非他们有一个.lib,否则没有人可以链接到库。)
要记住的主要内容是,如果要进行隐式加载,则必须向链接器提供适当的信息,无论是使用.lib还是.so文件,以便它可以将该信息插入可执行文件中。如果要进行显式加载,则必须手动加载和解析库,并对要使用的每个符号调用相应函数。加载库后,您无法直接引用库中的任何符号;您必须调用GetProcAddress/dlsym来获取它们的地址(并进行一些有趣的类型转换才能使用它们)。

1
有了两个单独的文件,您可以在没有实际拥有 DLL 的情况下链接到 DLL。这可能不是他们使用两个文件的实际原因,但这是一个合理的解释。 - user253751
这个问题涉及到动态链接,但是了解一下.lib文件也可以用于静态链接可能会很有帮助。静态链接将库的内容链接到可执行文件中,从而不需要使用DLL。 - Excelcius
关于第二点:您可以指出这个问题只存在于MSVC特定的情况下,并且在MS Windows平台上可以链接到DLL本身。请参阅我的答案编辑。 - cubuspl42
我想值得一提的是,使用DLL并不需要.lib文件。它只是简化了这个过程。 - Spook
@Excelcius 这不是同一个“.lib”文件。微软使用相同的扩展名来表示两种不同类型的文件。 - James Kanze
@JamesKanze 哇,我不知道那个。虽然这很有道理,因为一个常规的.lib文件不需要包含任何编译代码。 - Excelcius

15
在Windows上,.lib文件不是加载动态库所必需的,它只是提供了一种方便的方式。

原则上,您可以使用LoadLibrary来加载dll文件,然后使用GetProcAddress来访问该dll提供的函数。在这种情况下,封装程序的编译不需要访问dll,它仅在运行时(即LoadLibrary实际执行时)需要。MSDN有一个代码示例

这里的缺点是您需要手动编写从dll中加载函数的代码。如果您首先编译了dll,则此代码仅重复了编译器可以自动从dll源代码中提取的知识(如导出函数的名称和签名)。

这就是 .lib 文件的作用:它包含由编译器生成的 Dll 函数的 GetProcAddress 调用,因此您不必担心。从 Windows 角度来看,这被称为加载时动态链接,因为当加载您的封闭程序时,.lib 文件中的代码会自动加载 Dll(与手动方法相对,称为运行时动态链接)。

3
".lib"文件可能没有任何额外信息(也许以其他格式存在)而不在“.dll”中。我不确定为什么Microsoft决定使用两个文件(因为当Microsoft实现动态链接库时,现有的做法是使用单个文件),我只能猜想这是为了提供一种方法,使您能够分发运行可执行文件所需的库,而无需分发连接这些库的程序的方法。 - James Kanze
@JamesKanze我不确定这完全正确。 DLL允许有限的反射,例如可以枚举导出函数的名称。 但是,我不确定是否仅从dll中可以重构完整的函数签名。 - ComicSansMS
1
@JamesKanze 您当然是正确的,这些签名来自头文件而不是“.lib”文件。因此,似乎运行时加载器提供的通过“.lib”文件进行加载的方便性收益可以通过更智能的运行时加载器轻松实现。 - ComicSansMS
1
我见过函数名被剥离的DLL,其中函数只能通过序数访问。导入库可以为这些函数命名,而不会在DLL中公开它们。 - cHao
1
@Charlie:这可能部分是IP问题。但也要记住,Windows生态系统与*nix非常不同;在Windows中没有一个真正的编译器和一个真正的调用约定。(至少有两个:cdecl和stdcall。任何编译器都可以自己想出来。)将导入库与DLL区分开来,允许隐藏在包装函数后面的差异。因此,代码可以使用编译器的本地调用约定,而导入库会进行翻译。 - cHao
显示剩余5条评论

9
一般来说,动态链接是如何工作的?
动态链接库(也称为共享对象)文件包含机器代码指令和数据,以及一张元数据表,表明哪些代码/数据偏移量与哪些“符号”相关联,该符号的类型(如函数vs数据),数据中的字节数或字数等等。不同的操作系统往往有不同的共享对象文件格式,事实上,同一个操作系统可能支持多种格式,但这就是要点。
因此,想象一下共享库就像一个大块字节,具有以下索引:
SYMBOL       ADDRESS        TYPE        SIZE
my_function  1000           function    2893
my_number    4800           variable    4

一般来说,元数据表中无需捕获符号的确切类型——预计库头文件中的声明包含所有缺失的信息。C++有些特殊——与C相比,函数重载意味着可能有多个同名函数,并且名称空间允许使用其他符号,否则这些符号将具有歧义——因此,通常使用名称修饰将命名空间和函数参数的某些表示连接到函数名,形成可以在库对象文件中唯一的内容。
想要使用共享对象的程序通常可以做两件事:
  • 让操作系统在大约同时(在执行main()之前)加载自身和共享对象,由操作系统加载程序文件映像中有关这些符号使用的元数据并查找符号,然后在程序使用的内存中插入符号地址,使得程序可以正常运行,就好像在它第一次编译时已经知道了符号地址(但可能会慢一些)。

  • 或者,在其自己的源代码中明确调用dlopen,然后使用dlsym或类似方法获取符号地址,根据程序员对预期数据类型的了解将它们保存到(函数/数据)指针中,然后使用指针显式调用它们。

在Windows(LoadLibrary)中,你需要一个.dll文件在运行时调用,但在链接时,你需要提供一个相应的.lib文件,否则程序将无法链接...

这听起来不对。我认为应该是其中之一。

那个.lib文件包含什么?.dll方法的描述?这不是头文件包含的内容吗?

在这个级别上,lib文件与共享对象文件基本相同...主要区别在于编译器在程序发货和运行之前找到符号地址。


只是猜测,您来自Unix背景。在Windows下,.lib可以是静态库(此时没有.dll),也可以是一些来自.dll的信息的集合,由链接器用于生成必要的自动库加载信息。 (在Unix下,链接器直接从.so文件中提取此信息。) - James Kanze
@JamesKanze 哦,奇怪 - 感谢您提供的信息。干杯。 - Tony Delroy
当我第一次看到它时,我的反应是它很愚蠢,而不是奇怪。我现在已经在Windows上工作了四年多,逐渐地相信他们表面上的愚蠢通常背后都有原因——这个原因通常与金钱有关,或多或少。(虽然有时候……) - James Kanze
正如其他人所指出的那样,如果您使用 LoadLibrary,则不需要相应的 .lib 文件。我曾经使用 LoadLibrary,正是因为我的开发系统没有 .lib 文件,尽管目标系统有 .dll 文件。 - Adrian Ratnapala
如果您可以使用LoadLibrary,并通过名称找到要调用的函数,并且它们实际起作用,那么DLL中有足够的信息自动构建导入库。 您的IDE应该包括一个执行此操作的工具。VS有,我使用Borland时也有。 - cHao
@cHao 我在一个受限制的环境中工作,无法直接安装VS。我使用MinGW和文本编辑器进行工作。 - Adrian Ratnapala

2

相关的是,在OS X上(我认为*nix... dlopen也是如此),您不需要一个lib文件...编译器如何知道在运行时可用的头文件中所描述的方法?

编译器或链接器不需要这样的信息。作为程序员,您需要处理尝试通过dlopen()打开的共享库可能不存在的情况。


这证实了它在GCC风格的编译器上的工作方式(几乎)肯定是如此,但我正在寻找一个回答,涉及不同平台之间的相似之处和差异以及它们存在的原因。 - Charlie
1
@Charlie 为什么它们存在是一个真正的问题。Unix的.so范例是由Sun在大约1980年建立的。它从来没有真正是秘密,所以微软在实现动态加载时本可以使用它,大约20年后。我只是猜测,但我认为这是许可问题所在(但这可以通过静态链接更好地解决)。 - James Kanze

2
在Windows中,你可以有两种方式使用DLL文件:一种是链接它,然后就完成了,不需要再做什么;另一种是在运行时动态加载它。
如果你选择链接它,那么就会使用DLL库文件。链接库包含链接器用来实际知道要加载哪个DLL以及DLL函数在哪里的信息,这样它才能调用它们。当你的程序被加载时,操作系统也会为你加载DLL,基本上它所做的就是为你调用LoadLibrary
在其他操作系统(如OS X和Linux)中,它的工作方式类似。不同之处在于,在这些系统上,链接器可以直接查看动态库(.so/.dynlib文件)并找出所需内容,而无需像Windows那样使用单独的静态库。
要动态加载库,你不需要链接任何与你想要加载的库相关的东西。

1
我猜问题的主要点是:为什么Windows需要一个.lib文件而Linux/OSX不需要呢? - cubuspl42

2
现代*nix系统从Solaris OS中引入了动态链接的处理过程。特别是Linux不需要单独的.lib文件,因为所有外部依赖都包含在ELF格式中。 ELF文件的.interp部分指示此可执行文件中存在需要动态解析的外部符号。这就是所谓的动态链接。
有一种处理用户空间动态链接的方法称为动态加载。这是当您使用系统调用从外部*.so获取方法的函数指针时。
更多信息可以在本文中找到:http://www.ibm.com/developerworks/library/l-dynamic-libraries/

在*nix/GCC上,链接器/编译器何时知道它必须考虑动态库?是在解析'dlopen()'符号时就知道了吗?还是链接器/编译器会注意到任何'#include <dlfcn.h>'指令?就像我说的,当你将一个DLL模型与另一个进行比较时,它们在并置时都似乎有点奇怪...不过我会查看链接的,谢谢! - Charlie
1
你还没有明白,Charlie。有两种完全不同的方法可以让DLL工作:a) 链接它(例如:通过GCC传递-lSDL来链接到SDL动态库)b) 在运行时加载它(例如:将此代码放入您的项目中:void* lib=dlopen("./libSDL.so", RTLD_LAZY); //现在通过dlsym导入函数指针...)。在**a)中,当开发人员机器上的libSDL.so在链接时不可用时,您将收到链接错误。在b)**中,只有当SDL在_end-user_的当前目录中不可用时,您才会收到运行时错误。 - cubuspl42

2
像其他人已经说过的那样:Windows上包含在.lib文件中的内容直接包含在Linux/OS X上的.so/.dynlib中。但是主要问题是...为什么呢? *nix解决方案不是更好吗? 我认为是的,但是.lib有一个优点。链接到DLL的开发人员实际上不需要访问DLL文件本身。 这种情况在现实世界中经常发生吗?维护每个DLL文件的两个文件值得吗?我不知道。 编辑:好的,伙计们让事情变得更加混乱!您可以使用MinGW在Windows上直接链接到DLL。因此,整个导入库问题与Windows本身不是直接相关的。摘自MinGW wiki上的sampleDLL文章:

由"--out-implib"链接器选项创建的导入库是 当且仅当从除MinGW工具链以外的某些C/C++编译器接口化DLL时才需要的。MinGW工具链是 完全可以直接链接到创建的DLL。更多详细信息 可以在binutils中作为一部分的ld.exe信息文件中找到 包(这是工具链的一部分)。


关于优势:我怀疑真正的优势是相反的:即使你拥有它(因为它是随一个需要它的程序分发的),你也无法链接到 .dll。可能在链接时有轻微的优势,因为.lib通常比 .dll 小得多。(仍然需要 .dll 进行开发,因为您必须运行刚刚链接的程序以进行测试。) - James Kanze
@JamesKanze 我猜真正的优势在于相反的方面:即使你拥有.dll文件,也无法链接它。为什么这是优势? - cubuspl42
@Charlie:一个导入库实际上只是一个静态库,它知道如何加载 DLL,并具有调用 DLL 的存根函数。而一个 .a 文件可以包含任何东西(“a” 代表“归档”),但通常包含 .o 文件。 - cHao
@cubuspl42 Windows模型允许分发任何需要的动态对象,同时不允许其他人链接它们。至少,我认为这是被认为的优势。在大多数情况下,静态链接将是更好的解决方案,这样您就不必分发“.dll”了。 - James Kanze
1
@cubuspl42:当然,如果你知道这些函数的作用,你可以使用它。但是,如果使用的是剥离了符号表的DLL,那么这将变得更加困难和法律上的风险也会增加。例如,“DoSomething”可能会变成“function#17”,你需要反向工程分析DLL(或导入库,或使用它的应用程序)才能知道要调用哪些函数。这将使你更容易被告侵权,如果DLL的所有者决定起诉的话。 - cHao
显示剩余11条评论

1

Linux同样需要链接,但是不像Windows那样链接到.Lib库,而是需要链接到动态链接器/lib/ld-linux.so.2,但是当使用GCC时,这通常是在后台自动完成的(但是如果使用汇编程序,则需要手动指定)。

两种方法,无论是Windows的.LIB方法还是Linux的动态链接器链接方法,在现实中都被认为是静态链接。但是有一个区别,在Windows中,部分工作是在链接时完成的,虽然它仍然需要在加载时进行一些工作(我不确定,但我认为.LIB文件仅用于让链接器知道物理库名称,符号只在加载时解析),而在Linux中,除了链接到动态链接器之外,所有工作都发生在加载时。

动态链接通常是指在运行时手动打开DLL文件(例如使用LoadLibrary()),在这种情况下,负担完全在程序员身上。


0
在共享库中,例如.dll .dylib.so,有一些关于符号名称和地址的信息,如下所示:
------------------------------------
| symbol's name | symbol's address |
|----------------------------------|
| Foo           | 0x12341234       |
| Bar           | 0xabcdabcd       |
------------------------------------

而加载函数,例如LoadLibrarydlopen,会加载共享库并使其可用于使用。

GetProcAddressdlsym会找到您符号的地址。例如:

HMODULE shared_lib = LoadLibrary("asdf.dll");
void *symbol = GetProcAddress("Foo");
// symbol is 0x12341234

在Windows中,有一个.lib文件用于使用.dll。当您链接到此.lib文件时,您无需调用LoadLibraryGetProcAddress,只需像使用“普通”函数一样使用共享库的函数即可。它是如何工作的?
实际上,.lib包含一个导入信息。就像这样:
void *Foo; // please put the address of Foo there
void *Bar; // please put the address of Bar there

当操作系统加载您的程序(严格来说,是您的模块)时,操作系统会自动执行LoadLibraryGetProcAddress

如果您编写了Foo();这样的代码,编译器会自动将其转换为(*Foo)();。因此,您可以像使用“普通”函数一样使用它们。


1
问题是“它是如何工作的?”,而不是“我需要一个跨平台的解决方案来解决插入问题名称”。 - cubuspl42
OP想要了解动态链接是什么。他并没有问我们“我该如何抽象LoadLibrary()”。 - Adrian Ratnapala
@jalf 给出了问题的答案;它抽象了平台共享库。 - ikh
我仍然认为这个解释不够清楚... "在Windows上,有一个.lib文件用于使用.dll文件。当你链接到这个.lib文件时,你不需要调用LoadLibrary和GetProcAddress"。其他操作系统提供相同的功能吗?"如果你编写类似Foo();的代码,编译器会将其转换为(*Foo)();"。是这样吗?难道不是加载器做这个吗?而且函数指针的类比也并不完美,因为调用最终变得和其他任何调用一样静态。我的意思是说,静态链接的DLL在运行时比dlopen的调用稍微快一点。 - cubuspl42
1
@cubuspl42 而编译器肯定不会将 Foo() 翻译成 (*Foo)()。加载器也不会,因为你指出了,没有指针。粗略地说:编译器将在目标文件中插入一个 Foo 的占位符,链接器将用实际地址填充它。静态链接和动态链接之间的区别在于这种填充发生的时间。 - James Kanze
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接