共享库:Windows与Linux方法

33

我有一个关于Windows共享库(DLL)和Linux共享库(SO)的问题。

为什么当你创建一个 Windows DLL 时,它需要客户端程序链接静态库(.lib 文件),但是在 Linux 中创建的应用程序不需要链接任何静态库。

这是否与代码重定位等有关呢?谢谢。


它被称为“导入库”,并且存在用于从DLL中提取它们的工具。 - Ben Voigt
3个回答

46
为什么创建Windows DLL时需要客户端程序链接静态库(.lib文件),但在Linux中创建的应用程序不需要连接此类静态库呢?
这是微软历史上做出的设计决策,以便于连接器可以在没有特定版本DLL存在的链接时间将DLL引用添加到可执行文件中。这样做的原因是,一直以来Windows存在不同版本,带有不同版本的DLL。同时,当时微软正在与IBM合作开发OS/2,计划是Windows程序也可以在OS/2上执行。然而,微软决定 "背叛" OS/2,自己基于NT内核推出了专业级操作系统。但这意味着,为了开发,你希望开发人员能够链接到系统DLL,而不必拥有所有不同变体的DLL。取而代之的是,会使用动态链接 "模板" 来创建DLL和可执行文件(两者都在PE格式中),这些特定的.lib文件实际上并不是库,而只是符号和序数表(这是一个很少被知道的事实,PE二进制符号不仅可以通过字符串标识符加载,还可以通过整数编号加载,即所谓的序数)。
序数的一个副作用是,它们允许隐藏易读的符号,这样只有当你知道序数←→函数关系时才能使用DLL。
在Unix中,传统是"在运行它的系统上构建它",或者"你已经放置了所有目标系统文件"。因此,从来没有动机去分离库和链接信息。技术上说,同样适用于DLL。PE可以导出符号和重定位表格,DLL也可以,连接器可以从其中获取所需的所有信息。如果要隐藏Unix共享对象中的符号,通常会使用一个包含所有函数指针的单个struct,并仅通过名称导出全局常量实例,其中包含许多未明确命名的指针。你也可以用同样的方法来处理Windows DLLs。
简而言之,这不是技术上的原因,而是市场营销和分销决策的原因。

30

这实际上与代码重定位无关,它涉及到不同的体系结构:

  • 在Windows中,DLL就像可执行文件(EXE)一样。 EXE和DLL之间的主要区别是EXE具有入口点(main/WinMain函数),因此可以用于启动进程,而DLL只能加载到预先存在的进程中。但参见(1)

  • 在Linux中,.so类似于静态库(.a)。主要区别在于.so文件可以链接到正在运行的程序,而.a文件只能在编译程序时链接。

这种方法的一个后果是,在Linux中,同一个文件可以用来构建和运行程序。但在Windows中,您需要一个适当的库(LIB)来链接程序。实际上,对应于DLL的lib通常仅包含函数名称以满足链接器以及执行重定位的存根。但参见(2)

(1)好吧,DLL也有入口点,但它不像主函数那样使用,而仅用作某种初始化/终止挂接点。

(2)一些链接器足够聪明,能够在某些简单情况下仅使用DLL本身来链接到DLL,而无需额外的LIB文件。我认为至少MinGW链接器可以做到这一点。


9
一个.so库和一个静态库(.a)是完全不同的。 - user149341
@duskwuff:格式不同,但行为差别不大。.so 的设计目的就是像 .a 一样运行,只不过它在运行时链接。即使符号解析和可见性规则也几乎相同,因此程序可以从静态链接移植到动态链接而带来最小的痛苦。 - rodrigo
12
它们的行为完全不同。.so 是一个 ELF 对象,外观类似于没有入口点的可执行文件。.a 是一个存档文件,其中包含多个 .o 文件;它的外观和行为更像是 tar 文件而不是库! 链接 .a 文件只是解压缩所有成员的 .o 文件并将它们引入,这与链接 .so 的方式完全不同。 - user149341
2
@duskwuff:我不同意:这些只是实现细节。看看编译器可观察的行为:它们都定义外部符号,它们都可以链接(但一个在链接时另一个在运行时),它们都可以有未定义的引用,它们都使用几乎完全相同的算法处理符号解析。除了静态与动态之外,主要区别是.so不由几个.o组成,因此它实际上更像.o而不是.a或者像一个.a里面只有一个.o。当然,.so文件具有更多的功能(RPATH,NEEDED)等,但这些都是动态需求。 - rodrigo
8
@rodrigo: 你的答案在两个方面都是错误的。首先,就像在Windows中一样,在Linux中共享对象的文件格式与普通可执行文件的格式完全相同:ELF(而在Windows中是PE)。然后在Windows中编写链接器,只需要提供DLL文件即可,链接器会从DLL本身提取符号和重定位信息是完全可能的。单独使用.lib主要是出于历史原因,这样你可以在没有实际库的情况下链接库;这很重要,因为不同版本的Windows具有不同的DLL。 - datenwolf
显示剩余2条评论

4
在Windows中,客户端程序不需要链接静态库就可以访问DLL中的函数。动态链接可以完全在运行时发生,而且客户端程序在编译时甚至不知道DLL的存在。
例如,如果你想要在名为“bar.dll”的DLL中调用名为“foo”的函数,你可以编写如下代码:
HINSTANCE hinst = LoadLibrary("bar.dll");
FARPROC foo = GetProcAddress(hinst, "foo");
foo();

“foo”和“bar.dll”可能只是在运行时通过配置文件或其他用户输入获得的值。

静态库的目的是自动化这个动态加载的过程,通过创建存根来实现,从客户程序的角度看,它们就像是常规函数,但在运行时与DLL链接。通常这种链接发生在客户进程加载时,但也可以生成库以根据需要加载和链接,因此直到实际需要才会将DLL带入内存。静态库决定了链接发生的时间。

在大多数情况下,编译器可以自动生成这些库,因此在仅链接到DLL函数时技术上没有必要使用它们。然而,唯一的例外(我所知道的)是链接到共享变量。

在Windows DLL中,您可以创建一个共享数据段,并使用变量访问已加载该DLL的任何进程。关于这些变量的大小和类型的信息存储在相关的静态库中,并且无法仅从DLL中确定。为了访问这些变量,客户端程序必须链接到该DLL的静态库。

据我所知,Linux共享库不支持这种概念。

更新

在Windows上,可以创建仅按序数(编号)导出函数入口点的DLL。这可以视为一种数据隐藏形式,并且通常在实现者希望某些函数保持私有时使用。

具有静态库访问权限的人将能够按名称调用这些函数,因为该库将详细信息链接到函数名称对应的序数。只有DLL的用户将不得不通过序数手动链接到函数,或者生成自己的静态库并使用虚构名称。


不介意的话,能否分享一些关于这种共享内存形式的参考资料? - Ismael Luceno
4
在Linux操作系统中,LoadLibrary+GetProcAddress被替换为dlopen+dlsym,这种方法经常被用来加载“插件”等内容。 - user1686
2
据我所知,Linux共享库不支持这样的概念。Linux也有它们。在ELF中还可以看到GNU_SHR部分。 - jww

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