动态链接最佳实践
UNIX通用模式
现今动态链接设计(在BSD、MacOS和Linux中)源自1988年的SunOS。《SunOS中的共享库》论文清晰阐述了其目标、设计和实现。
作者的主要动机是节省磁盘和内存空间,以及无需重新链接程序即可升级库(或操作系统)。在当今强大的个人计算机上,资源使用动机可能不如1988年重要。然而,升级库的灵活性以及轻松检查每个应用程序使用的库版本的能力仍然非常有用。
动态链接并非没有批评者,也不适用于所有情况。由于位置无关代码(PIC)和延迟加载,它的运行速度稍慢。某些系统上加载器的复杂性增加了攻击面。最后,升级的库可能对不同程序产生不同影响,例如破坏那些依赖未记录行为的程序。
链接编辑器和加载器
在编译时,链接编辑器解析指定库中的符号,并在生成的二进制文件中记录加载这些库的说明。在运行时,应用程序调用代码将共享库符号映射到正确的内存地址。
SunOS及后续类UNIX系统向链接器(ld)添加了编译时标志,用于生成或链接动态链接库。设计者还添加了一个特殊的系统库(ld.so),其中包含为应用程序查找和加载其他库的代码。程序的pre-main()初始化例程加载ld.so并从程序内部运行它以查找和加载其余所需库。
版本管理
应用程序可以利用更新的库而无需重新编译。库更新可分为三类:
- 当前接口的实现改进。错误修复、性能优化。(补丁发布)
- 新功能,接口添加。(次要发布)
- 接口或其操作的向后不兼容更改。(主要发布)
链接到给定主版本的库的应用程序在加载任何更新的次要或补丁版本时将继续正常工作。当加载不同的主版本或比链接时使用的更早的次要版本时,应用程序可能无法正常工作。
版本标识符
每个库版本都可以用版本标识符标记,旨在捕获库的发布历史信息。将发布历史映射到版本标识符有多种方式。
两种最常见的映射系统是语义版本控制和libtool版本控制。语义版本控制计算各种类型的发布次数,并按词典顺序编写。Libtool版本控制计算不同的库接口。
语义版本控制写为major.minor.patch,libtool写为current:revision:age。直觉是current计算接口更改。每当接口更改时,无论是次要还是主要方式,current都会增加。
API与ABI
版本管理的一个微妙之处是更改可能发生在库的编程接口(API)或二进制接口(ABI)中。C库的编程接口通过其头文件定义。向后不兼容的API更改意味着为先前版本编写的程序在包含新版本的头文件时将无法编译。
相比之下,二进制接口是运行时概念。它涉及函数的调用约定,或程序与库之间共享数据的内存布局(和含义)。ABI确保在加载和运行时的兼容性,而API确保在编译和链接时的兼容性。
系统的链接器和加载器差异
链接器(ld, lld)
编译目标文件后,编译器前端(gcc、clang、cc、c99)将调用链接器(ld、lld)来查找未解析的符号,并在目标文件或共享库中匹配它们。链接器仅搜索前端请求的共享库,按命令行指定的顺序。如果在列出的库中找到未解析的符号,链接器会在生成的可执行文件中标记对该库的依赖。
加载器(ld.so, dyld)
在启动时,具有动态库依赖关系的程序加载并运行ld.so(或在Mac上为dyld)以查找并加载其余依赖项。加载库检查DT_NEEDED ELF标签(或在Mac上的Mach-O中的LOAD_DYLIB名称)以确定要在系统上查找的库文件名。
可移植最佳实践
链接
标准实践是在共享系统目录中创建符号链接libfoo.so -> libfoo.so.x -> libfoo.so.x.y.z。第一个链接(无版本号)用于构建时链接。问题是,它固定在一个版本上。当安装多个版本时,没有可移植的方法来选择要链接的版本。
我建议将所有开发(链接)库文件捆绑到每个版本的不同目录结构中:
|
|
使用pkg-config的版本灵活性
Pkg-config允许应用程序表达可接受的库版本范围,而不是硬编码特定版本。
加载
本节讨论为系统范围加载安装动态库。为此目的安装的库不用于编译时链接,而是用于运行时加载。
ELF安装(BSD/Linux)
ELF对象没有太多版本元数据。SONAME就是其中之一。结合某些系统上加载器的平庸行为,意味着传统安装技术效果不佳。
更安全的安装方法
- 对于版本x.y.z,使用SONAME libfoo.so.x.y编译libfoo.so
- 将libfoo.so复制到/usr/local/lib/libfoo.so.x.y.z
- 在目标目录中回填次要版本符号链接
Mach-O安装(MacOS)
Mach-O内部比ELF有更多版本元数据,因此传统安装在这里效果很好。
- 对于版本x.y.z,使用以下设置编译libfoo.dylib:
- install_name libfoo.x.dylib
- current version x.y.z
- compatibility version x.y
示例代码
有关如何可移植地构建库,并方便地安装以供链接器和加载器使用的示例,请参阅begriffs/libderp。这是我的第一个共享库,我在其中测试了本文的想法。