动态链接 - Linux与Windows的区别

14
在Windows中,当我在MSVC的DLL项目中编译C/C++代码时,会得到两个文件:
1. MyDll.dll 2. MyDll.lib
据我所知,MyDll.lib包含了一种指针表,指示dll中函数的位置。在使用这个dll(例如在exe文件中)时,MyDll.lib在链接期间嵌入到exe文件中,因此在运行时,程序“知道”函数在MyDll.dll中的位置并可以使用它们。
但是如果我在Linux下编译相同的代码,则只会得到一个文件MySo.so,没有MySo.a(类似于Linux中的lib文件),那么在链接期间不添加任何东西的情况下,可执行文件如何知道函数在MySo.so中的位置呢?
5个回答

7
MSVC链接器可以将目标文件(.obj)和对象库(.lib)链接在一起,生成.EXE或.DLL文件。
要链接到DLL,MSVC的过程是使用所谓的导入库(.LIB),它作为C函数名称和DLL导出表之间的粘合剂(在DLL中,函数可以按名称或序数导出 - 后者经常用于未记录的API)。
然而,在大多数情况下,DLL导出表具有所有函数名称,因此导入库(.LIB)包含大量冗余信息("导入函数ABC->导出函数ABC"等)。
甚至可以从现有的.DLL文件生成.LIB文件。
其他平台上的链接器没有这个“特性”,可以直接链接动态库。

其他平台上的链接器没有这个功能,但很容易实现(例如Implib.so在Linux上实现了此功能),以实现延迟加载和其他好处。 - yugr
@yugr:这就是为什么“特性”要加引号的原因——这不是你通常想做的事情,而且在Windows上还需要额外的工作。 - Chris Dodd
为了完整起见,我想补充一下,在MSVC中动态链接可以 隐式 执行,即使用 .dll 文件和导入文件,也可以 显式 执行,即仅使用 .dll 而不需要任何 .lib,但这需要使用类似于 LoadLibrary 的东西。参考: 将可执行文件链接到 DLL - Breno

2
在Linux上,链接器(而不是动态链接器)会在链接时搜索指定的共享库,并在可执行文件中创建对它们的引用。当动态链接器加载这些可执行文件时,会将它们所需的共享库加载到内存中并解析符号,从而使二进制文件能够运行。
如果创建了MySo.a,它实际上会将要链接的符号直接包含在二进制文件中,而不是像Windows上使用的“符号查找表”。 rustyx的答案比我更详细地解释了Windows上的过程;我已经很久没有使用Windows了。

1
Windows采用了不同的方法...可以向操作系统指定DLL中符号的确切位置,这与维基百科上的内容相矛盾(https://en.wikipedia.org/wiki/Dynamic-link_library#Symbol_resolution_and_binding),维基百科上说即使使用序数,函数名称仍然会在启动时或首次调用库函数时解析(除非使用直接地址绑定,但没有人这样做,因为它会强制库用户在库更改时重新编译和部署他们的代码)。 - yugr
@yugr 已经删除了那部分,我当时只是在试探而已。 - S.S. Anne

1
你看到的区别更多是实现细节——在底层,Linux和Windows都工作得很相似——你的代码调用一个存储在可执行文件中的存根函数,这个存根函数会加载DLL/shlib(如果需要的话,在延迟加载的情况下,否则库在程序启动时加载),并且(在第一次调用时)通过GetProcAddress/dlsym解析符号。
唯一的区别是,在Linux上,这些存根函数(称为PLT存根)在将应用程序与动态库链接时动态生成(库包含足够的信息来生成它们),而在Windows上,它们是在创建DLL本身时生成的,保存在一个单独的.lib文件中。
这两种方法非常相似,以至于在Linux上可以模仿Windows导入库(参见Implib.so项目)。

0
在Linux上,您将MySo.so传递给链接器,它能够仅提取链接阶段所需的内容,并放置一个引用,指出需要在运行时使用MySo.so

-4

.dll.so是共享库(在运行时链接),而.a.lib是静态库(在编译时链接)。这在Windows和Linux之间没有区别。

区别在于它们的处理方式。注意:区别仅在于使用习惯上的不同。要按照Windows的方式在Linux上进行构建,或者反过来,这并不太难,只是实际上几乎没有人这样做。

如果我们使用一个dll,或者甚至从我们自己的二进制文件中调用一个函数,有一种简单明了的方法。例如,在C语言中,我们可以看到:

int example(int x) {
  ...do_something...
}

int ret = example(42);

然而,在汇编级别上,可能会有许多差异。例如,在x86上,执行call操作码,并在堆栈上给出42。或者在某些寄存器中。或者任何地方。在编写dll之前,没有人知道它将如何使用。或者项目将如何使用它,可能是用现在甚至不存在的编译器(或语言!)编写的(或者对于dll的开发人员来说是未知的)。

例如,默认情况下,C和Pascal都从堆栈中放置参数(并获取返回值)-但它们按不同的顺序执行。您还可以通过一些-依赖于编译器的-优化,在寄存器之间交换函数中的参数。

正如您正确看到的那样,Windows的习惯是构建一个dll时,我们还会创建一个最小的.a/.lib。这个最小的静态库只是一个包装器,该dll的符号(函数)通过它被访问。这使得所需的汇编级别调用转换成为可能。

它的优点是兼容性。它的缺点是如果你只有一个.dll文件,你可能很难弄清楚它的函数应该如何调用。这使得使用dll成为一项麻烦的任务,如果dll的开发者没有给你提供 .a。因此,它主要用于封闭性的目的,例如更容易获取SDK的额外现金。

另一个缺点是即使你使用动态库,你也需要静态编译这个小包装器。

在Linux中,dll的二进制接口是标准的,并遵循C约定。因此,不需要 .a,并且共享库之间存在二进制兼容性,但我们没有Microsoft定制的优势。


1
请提供一个证明链接,证明存根函数可以更改参数顺序。我以前从未听说过这个,很难相信,因为性能开销会非常大。 - yugr
@yugr 简单的寄存器/堆栈重排并不会影响性能。如果您使用从msvc编译的二进制文件编译的msvc编译的dll,则显然不会发生太多事情,但也可能会发生一些变化。 - peterh
1
我们可以对此进行争论,但如果您是正确的,那么提供证明存根函数能够处理复杂参数(而不仅仅是虚拟跳板)应该很容易。 - yugr
@yugr,存根可以访问dll的函数签名,这使得非平凡的处理变得平凡。 - peterh
2
我只建议你在回答中附上一些关于导入库的证明链接(因为有些说法是有问题的)。 - yugr

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