程序员的自我修养:链接、装载与库(库)
文章目录
- 一、Linux的共享库的管理
- 1. 共享库版本
- 1.1 共享库兼容性
- 2. 共享库版本命名
- 3. SO-NAME
- 4. 符号版本
- 4.1 次版本号交会问题
- 4.2 Solaris中基于符号的版本机制(也说明范围机制)
- 4.2.1 概述
- 4.2.2 案例演示
- 4.2.2.1 初始版本:libstack.so.1(版本 SUNW_1.1)
- 4.2.2.2 增量升级:添加新接口 swap(版本 SUNW_1.2)
- 4.2.2.3 运行时的版本检查
- 4.2.3 范围机制
- 4.2.4 查看符号版本信息
- 4.3 Linux中的符号版本
- 4.3.1 Glibc 中的符号版本演化
- 4.3.2 GCC 对 Solaris 符号版本机制的扩展
- 4.3.2.1 使用 `.symver` 汇编宏指定符号版本
- 4.3.2.2 同一符号的多版本共存(符号重载)
- 4.3.3 Linux 系统中符号版本机制的实践
- 4.3.3.1 使用 `--version-script` 生成带版本信息的共享库
- 4.3.3.2 符号版本脚本的内容示例
- 4.3.3.3 链接应用程序并检查版本依赖
- 4.3.3.4 运行时版本检查失败示例
- 4.3.3.5 查看共享库中的符号版本信息
- 5. 共享库系统路径(FHS标准)
- 6. 共享库查找过程
- 6.1 查找路径
- 6.2 ld.so.conf 配置文件
- 6.3 ldconfig 程序
- 6.4 查找顺序
- 6.5 何时需要运行 ldconfig
- 七、环境变量
- 1. LD_LIBRARY_PATH
- 2. LD_PRELOAD
- 3. LD_DEBUG
- 八、共享库的创建和安装
- 1. 创建共享库
- 1.1 创建演示
- 1.2 注意
- 1.3 关于路径的参数
- 1.4 动态符号表特别说明
- 2. 清除符号信息
- 3. 共享库的安装
- 4. 共享库的构造和析构函数
- 4.1 指定构造函数和析构函数
- 4.2 优先级设置
- 5. 共享库脚本
- 5.1 主要特点
- 5.2 示例
- 5.3 什么叫运行时完成组合过程
一、Linux的共享库的管理
1. 共享库版本
1.1 共享库兼容性
- 兼容更新:在原有接口基础上添加内容,原有接口保持不变。
- 不兼容更新:改变或删除原有接口,导致依赖程序无法运行。
ABI(Application Binary Interface):二进制接口,包括函数调用堆栈结构、符号命名、参数规则、数据结构内存分布等。
| 更改类型 | 兼容性 |
|---|---|
| 往共享库中添加一个导出符号 | 兼容 |
| 删除一个原有的导出符号 | 不兼容 |
| 给导出函数添加一个参数 | 不兼容 |
| 删除导出函数中的一个参数 | 不兼容 |
| 改变导出函数中使用的结构类型的长度、内容、成员类型 | 不兼容 |
| 修正Bug或改进性能,不改变接口类型 | 兼容 |
| 修正Bug或改进性能,同时改变接口类型 | 不兼容 |
导致C语言共享库ABI改变的4种行为:
- 导出函数的行为发生改变
- 导出函数被删除
- 导出数据结构的内存布局发生变化
- 导出函数的接口发生变化(返回值、参数数量等)
C++共享库ABI注意事项(尽量避免使用C++接口):
- 不要在接口类中使用虚函数(或不要随意删除/添加)
- 不要改变类中成员变量的位置和类型
- 不要删除非内嵌的public/protected成员函数
- 不要将非内嵌成员函数改为内嵌
- 不要改变成员函数的访问权限
- 不要在接口中使用模板
- 最好不要使用C++作为共享库接口
2. 共享库版本命名
规则:libname.so.x.y.z
- x:主版本号(Major Version)—— 主版本号表示共享库的重大升级。不同主版本号的库之间不兼容,依赖于旧主版本号的程序需要修改并重新编译才能在新版共享库中运行,或者系统必须保留旧版共享库以保证旧程序正常运行。
- y:次版本号(Minor Version)—— 次版本号表示共享库的增量升级,即增加新的接口符号,同时保持原有符号不变。在相同主版本号下,高次版本号的库向后兼容低次版本号的库。依赖于旧次版本号的程序可以在新的次版本号共享库中运行,因为新版本保留了所有原有接口且不改变其定义和含义。
- z:发布版本号(Release Version)—— 发布版本号表示共享库的错误修正、性能改进等,不添加任何新接口,也不对现有接口进行更改。在相同主版本号和次版本号的情况下,不同发布版本号之间完全兼容,依赖于某个发布版本号的程序可以在任何其他发布版本号中正常运行,无需修改。
例外:Glibc不遵循这种规则,通常使用
libc-x.y.z.so命名方式。
3. SO-NAME
- 定义:共享库文件名去掉次版本号和发布版本号,保留主版本号。
例如libfoo.so.2.6.1→ SO-NAME为libfoo.so.2 - 用途:程序在
.dynamic段中记录依赖库的SO-NAME,而非完整版本号,以便运行时自动链接到最新兼容版本。 - 软链接:系统为每个共享库创建指向最新版本的SO-NAME软链接。
- ldconfig:遍历共享库目录,更新SO-NAME软链接,并缓存到
/etc/ld.so.cache。 - 链接名:编译时使用
-lXXX,链接器根据环境查找最新版本的libXXX.so.x.y.z。这里的链接器指的是编译链接生成产物时的链接器。
4. 符号版本
4.1 次版本号交会问题
动态链接器在运行时只检查共享库的 SO-NAME(即主版本号),若 SO-NAME 一致则认为接口完全兼容,不再进一步检查次版本号。然而,次版本号只保证向后兼容(高次版本号库可以运行依赖低次版本号的程序),并不保证向前兼容(低次版本号库无法提供高次版本号中新增的符号)。因此,当一个程序在编译时依赖于较高次版本号的共享库(例如 libfoo.so.1.3),而运行时系统中只有较低次版本号的共享库(例如 libfoo.so.1.2)时,尽管两者的 SO-NAME 相同(均为 libfoo.so.1),动态链接器仍然会尝试运行程序。如果程序用到了高次版本号中新增的符号,就会因符号缺失而导致重定位错误甚至程序崩溃。这个问题无法单纯依靠 SO-NAME 机制解决,现代系统通过引入符号版本机制来精确记录程序实际依赖的符号版本,从而在运行时做出准确的兼容性判断。
4.2 Solaris中基于符号的版本机制(也说明范围机制)
4.2.1 概述
为每个导出/导入的符号关联一个版本号,解决次版本号交会问题:
- 使用符号版本脚本定义符号集合(如
SUNW_1.1、SUNW_1.2),集合之间可继承。 - 范围机制:通过
local: *;将未明确导出的符号隐藏为局部符号。 - 链接器在程序中记录程序实际依赖的最小符号版本集合,运行时动态链接器检查系统库是否满足。
这里可能会引起歧义,额外解释下,在符号版本机制中,链接器在程序中记录的不是最大或最小的版本号,而是记录了一个版本要求的下界。也就是说,链接器在程序中记录的是程序所依赖的符号版本中的最高版本号,因为只有这个版本号才能保证所有需要的符号都存在于共享库中。这个最高版本号同时也是系统库需要满足的最低版本门槛。这里最小的意思就是最低版本门槛。
4.2.2 案例演示
4.2.2.1 初始版本:libstack.so.1(版本 SUNW_1.1)
假设我们实现一个栈库,对外提供push和pop两个公共接口,内部还有辅助函数__stack_overflow和__stack_underflow,我们不希望这些内部符号被外部程序使用。
符号版本脚本 libstack.map:
SUNW_1.1{global: push;pop;local: *;# 将所有未在 global 中列出的符号隐藏为局部};SUNW_1.1是版本集合的名称,通常以SUNW_为前缀。global块列出要导出的公共符号。local: *;表示除上述符号外,其他所有全局符号都降为局部,外部无法访问。
编译生成共享库:
gcc-shared-Wl,-soname,libstack.so.1 -Wl,--version-script=libstack.map-olibstack.so.1.0.0 stack.c此时共享库的 SO-NAME 为libstack.so.1,导出的全局符号只有push和pop,内部函数被隐藏。
4.2.2.2 增量升级:添加新接口 swap(版本 SUNW_1.2)
在后续版本中,我们添加一个新函数swap,同时希望保持与旧版本的二进制兼容。
新的符号版本脚本 libstack.map:
SUNW_1.2{global: swap;}SUNW_1.1;# 继承 SUNW_1.1 的所有符号SUNW_1.1{global: push;pop;local: *;};SUNW_1.2集合显式继承了SUNW_1.1,因此它包含了push、pop和swap三个符号。- 继承语法为
} SUNW_1.1;,表示该集合包含父集合的所有符号。
编译升级后的共享库:
gcc-shared-Wl,-soname,libstack.so.1 -Wl,--version-script=libstack.map-olibstack.so.1.1.0 stack.c尽管 SO-NAME 依然是libstack.so.1,但共享库内部记录了符号版本信息。旧程序(只依赖push/pop)会被链接器标记为需要SUNW_1.1;新程序(使用swap)会被标记为需要SUNW_1.2。
4.2.2.3 运行时的版本检查
当一个程序被构建时,静态链接器会分析程序实际引用的符号,并记录程序所依赖的最小符号集合版本。例如:
- 程序
old_app只调用了push和pop,则它的动态段中会记录依赖SUNW_1.1。 - 程序
new_app调用了push和swap,则会记录依赖SUNW_1.2。
动态链接器在运行时加载共享库时,会检查共享库是否提供了程序所需的所有符号版本。如果系统中只有libstack.so.1.0.0(仅含SUNW_1.1),那么:
old_app可以正常运行(因为SUNW_1.1存在)。new_app会报错,提示找不到SUNW_1.2版本的符号,防止因缺少swap而导致的意外崩溃。
4.2.3 范围机制
实际上,上述案例已经演示了范围机制的作用,通过local: *;,我们成功隐藏了内部符号。这带来了两个好处:
- 减少符号冲突:内部函数不会与应用程序或其他库的同名符号发生意外覆盖。
- 隐藏实现细节:库的作者可以自由修改内部函数,而不必担心破坏外部程序的依赖(因为外部程序本就不能访问它们)。
4.2.4 查看符号版本信息
使用pvs(Solaris 下的符号版本查看工具,Linux 下的readelf -V可以查看共享库的符号版本信息):
pvs-dsvlibstack.so.1.1.0输出示例:
libstack.so.1.1.0: SUNW_1.2;SUNW_1.2(swap)SUNW_1.1(push, pop)这清晰地显示了符号与版本的归属关系。
4.3 Linux中的符号版本
4.3.1 Glibc 中的符号版本演化
Linux 系统下,符号版本机制主要被 Glibc 使用。以libc-2.6.1.so为例,其符号版本演化序列非常丰富:
- GLIBC_2.0、GLIBC_2.1、GLIBC_2.1.1、GLIBC_2.1.2、GLIBC_2.1.3
- GLIBC_2.2、GLIBC_2.2.1、GLIBC_2.2.2、GLIBC_2.2.3、GLIBC_2.2.4、GLIBC_2.2.6
- GLIBC_2.3、GLIBC_2.3.2、GLIBC_2.3.3、GLIBC_2.3.4
- GLIBC_2.4、GLIBC_2.5、GLIBC_2.6
这些版本号代表着 Glibc 每次添加新接口时的符号集合,每个新版本继承旧版本的所有符号。
特殊版本标签:
GCC_前缀:用于 GCC 编译器相关的符号,普通程序不应依赖。GLIBC_PRIVATE:Glibc 内部使用的符号,不对外公开,可能在版本升级中被删除或改变。开发者使用这些符号需要“后果自负”。
稳定库的示例:libcrypt(加密解密库)从 2.0 版本后从未添加新接口,因此它只有一个符号版本GLIBC_2.0。
4.3.2 GCC 对 Solaris 符号版本机制的扩展
GCC 在 Solaris 原有机制基础上提供了两个重要扩展:
4.3.2.1 使用.symver汇编宏指定符号版本
除了通过符号版本脚本(--version-script)指定版本外,GCC 允许在源码中使用.symver宏直接为符号绑定版本。该宏可用于 GAS 汇编代码,也可在 C/C++ 中通过asm嵌入。
示例:
asm(".symver add, add@VERS_1.1");intadd(inta,intb){returna+b;}这样,符号add被标记为版本VERS_1.1。当程序链接到这个共享库时,链接器会记录对add@VERS_1.1的依赖。
4.3.2.2 同一符号的多版本共存(符号重载)
Linux 的符号版本机制允许同一个符号名存在多个不同版本的实现。这解决了 Solaris 2.5 的缺陷——每个符号只能有一个版本。
典型场景:当共享库升级时,某个函数的接口或行为发生变化。若直接覆盖原符号,依赖旧接口的程序将无法运行。通过多版本机制,可以在同一个共享库中同时保留旧版本和新版本的实现,并根据程序的链接版本自动选择正确的符号。
示例:
asm(".symver old_printf, printf@VERS_1.1");asm(".symver new_printf, printf@VERS_1.2");intold_printf(constchar*format,...){// 旧的实现,行为与 1.1 版一致}intnew_printf(constchar*format,...){// 新的实现,行为改变或参数不同}old_printf被别名为printf@VERS_1.1。new_printf被别名为printf@VERS_1.2。
链接行为:
- 程序在编译时如果链接到
VERS_1.1版本的库,最终可执行文件会记录对printf@VERS_1.1的依赖,运行时链接器将其解析为old_printf。 - 如果程序链接到
VERS_1.2版本,则会记录printf@VERS_1.2,运行时调用new_printf。
这样,新旧程序都能正确运行,且共享库的主版本号(SO-NAME)无需改变。
为何需要这个特性:有时修改一个符号的接口或含义,并不足以构成主版本号升级(因为其他所有接口都兼容),但又不能直接破坏旧程序。多版本机制允许在同一个 SO-NAME 下平滑过渡。
4.3.3 Linux 系统中符号版本机制的实践
4.3.3.1 使用--version-script生成带版本信息的共享库
步骤:
- 编写符号版本脚本(例如
lib.ver)。 - 使用
gcc -shared -fPIC编译,并通过-Xlinker --version-script将脚本传递给 ld。
命令示例:
gcc-shared-fPIClib.c-Xlinker--version-script lib.ver-olib.so或者更简洁地使用-Wl,--version-script:
gcc-shared-fPIClib.c -Wl,--version-script=lib.ver-olib.so4.3.3.2 符号版本脚本的内容示例
假设lib.c中定义了一个函数foo,我们希望将其导出为版本VERS_1.2,并隐藏其他所有符号。
lib.ver内容:
VERS_1.2{global: foo;local: *;};global: foo;表示符号foo属于VERS_1.2版本集合。local: *;表示所有未明确列出的全局符号都降为局部,外部不可见。
4.3.3.3 链接应用程序并检查版本依赖
编译一个调用foo的main.c,并与lib.so链接:
gcc main.c ./lib.so-omain此时,静态链接器会分析main对foo的引用,发现foo的版本是VERS_1.2,因此会在生成的可执行文件的动态段中记录对VERS_1.2的依赖。
4.3.3.4 运行时版本检查失败示例
如果将main拿到一个只包含低于VERS_1.2版本的lib.so的系统(例如只有VERS_1.1),动态链接器会检测到版本不匹配并报错:
./main ./main: ./lib.so: version`VERS_1.2`not found(required by ./main)错误信息明确指出了缺少的符号版本,程序不会继续执行,避免了因符号缺失导致的不可预测行为。
4.3.3.5 查看共享库中的符号版本信息
使用readelf -V可以查看共享库的版本定义:
readelf-Vlib.so输出会显示VERS_1.2以及它包含的符号foo。
5. 共享库系统路径(FHS标准)
| 路径 | 用途 |
|---|---|
/lib | 系统最关键共享库(动态链接器、C语言库、数学库等),/bin、/sbin及系统启动所需 |
/usr/lib | 非系统运行关键性库,主要是开发用共享库、静态库、目标文件 |
/usr/local/lib | 第三方应用程序库(如Python相关库) |
6. 共享库查找过程
6.1 查找路径
- 依赖模块的路径保存在
.dynamic段的DT_NEED类型项中 - 查找规则:
- 绝对路径:动态链接器直接按此路径查找
- 相对路径:动态链接器在以下目录中查找:
/lib/usr/lib/etc/ld.so.conf配置文件指定的目录
为了程序的可移植性和兼容性,共享库路径通常使用相对路径。
6.2 ld.so.conf 配置文件
- 文本配置文件,可包含其他配置文件
- 示例目录(具体取决于系统):
/usr/local/lib/lib/i486-linux-gnu/usr/lib/i486-linux-gnu
6.3 ldconfig 程序
- 作用:
- 为共享库目录下的各共享库创建、删除或更新对应的 SO‑NAME(符号链接)
- 收集所有 SO‑NAME 信息,集中存入
/etc/ld.so.cache缓存文件
- 优势:
/etc/ld.so.cache结构经过特殊设计,查找速度快,大大提升共享库定位效率
文件名或路径可能不同,例如 FreeBSD 的 SO‑NAME 缓存文件为
/var/run/ld-elf.so.hints,可通过查看ldconfig的 man 手册获取具体信息。
6.4 查找顺序
- 动态链接器首先在
/etc/ld.so.cache中查找所需共享库 - 若未找到,则遍历
/lib和/usr/lib目录 - 若仍未找到,则宣告失败
6.5 何时需要运行 ldconfig
- 在系统指定的共享库目录下添加、删除或更新共享库
- 修改
/etc/ld.so.conf配置文件后
许多软件包的安装程序在安装共享库后会自动调用
ldconfig。
七、环境变量
1. LD_LIBRARY_PATH
- 作用:临时改变某个应用程序的共享库查找路径,不影响其他程序。
- 格式:由冒号分隔的路径列表,默认为空。
- 查找顺序:动态链接器会首先查找
LD_LIBRARY_PATH指定的目录。 - 使用示例:
$LD_LIBRARY_PATH=/home/user /bin/ls - 替代方法:直接运行动态链接器并指定 -library-path
$ /lib/ld-linux.so.2 -library-path /home/user /bin/ls
完整查找顺序如下:
- LD_LIBRARY_PATH 指定的路径
- /etc/ld.so.cache 缓存文件中的路径
- 默认目录:先 /usr/lib,后 /lib
警告:不要随意设置 LD_LIBRARY_PATH 并导出到全局,否则可能引起其他应用程序问题。该变量也会影响 GCC 编译时的库查找路径(相当于 -L 参数)。
2. LD_PRELOAD
- 作用:预先装载指定的共享库或目标文件,优先级高于
LD_LIBRARY_PATH。 - 无论程序是否依赖这些库,都会被装载。
- 利用全局符号介入机制,可以覆盖后面加载的同名全局符号(例如改写 C 标准库的某些函数),方便调试和测试。
- 系统配置文件:
/etc/ld.so.preload,效果与LD_PRELOAD相同。 - 注意:发布版程序不应依赖
LD_PRELOAD。
3. LD_DEBUG
- 作用:打开动态链接器的调试功能,打印各类信息。
- 使用示例:
$LD_DEBUG=files ./HelloWorld.out- 常用选项:
files– 显示整个装载过程(依赖库、初始化步骤、地址等)bindings– 显示符号绑定过程libs– 显示共享库查找过程versions– 显示符号版本依赖reloc– 显示重定位过程symbols– 显示符号表查找statistics– 显示统计信息all– 显示以上所有信息help– 显示帮助信息
八、共享库的创建和安装
1. 创建共享库
1.1 创建演示
- 关键 GCC 参数:
- -shared – 输出共享库类型
- -fPIC – 使用地址无关代码
- -Wl,soname,my_soname – 指定 SO-NAME
- 示例:当前有
libfoo1.c和libfoo2.c两个源码文件,希望产生一个libfoo.so.1.0.0的共享库,这个共享库依赖于libbar1.so和libbar2.so$ gcc-shared-Wl,-soname,libfoo.so.1-olibfoo.so.1.0.0 libfoo1.c libfoo2.c-lbar1-lbar2 - 分步编译链接:
$ gcc-c-g-Wall-olibfoo1.o libfoo1.c $ gcc-c-g-Wall-olibfoo2.o libfoo2.c $ ld-shared-sonamelibfoo.so.1-olibfoo.so.1.0.0 libfoo1.o libfoo2.o-lbar1-lbar2
注意:如果不使用 -soname,则共享库没有 SO-NAME,ldconfig 对其无效。
1.2 注意
- 不要去掉符号和调试信息,不要使用 -fomit-frame-pointer,以免影响调试。
- 使用 LD_LIBRARY_PATH 或链接器 -rpath 指定共享库查找路径:
$ ld-rpath/home/my/lib-oprogram.out program.o-lsomelib - 默认情况下,只有被其他共享模块引用到的符号才会放入动态符号表。若需导出所有全局符号(例如 dlopen() 反向引用主模块),使用
-export-dynamic:$ gcc -Wl,-export-dynamic...
1.3 关于路径的参数
| 选项/变量 | 作用 | 影响对象 | 生效阶段 |
|---|---|---|---|
-l | 指定要链接的库名(如-lfoo表示链接libfoo.so或libfoo.a) | 静态链接器(ld) | 编译/链接时 |
-L | 添加编译时库搜索目录 | 静态链接器(ld) | 编译/链接时 |
-rpath | 在可执行文件或共享库中写入运行时库搜索路径 | 动态链接器(ld-linux.so) | 链接时写入,运行时生效 |
LD_LIBRARY_PATH | 环境变量,指定额外的库搜索路径 | 同时影响静态链接器(编译时)和动态链接器(运行时) | 编译/链接时 和 运行时 |
1.4 动态符号表特别说明
默认情况下(即不使用 -export-dynamic 等特殊选项时),链接器在生成可执行文件时,只将那些被其他共享模块(共享库)引用到的全局符号 放入动态符号表(.dynsym)。即使一个符号是 GLOBAL(全局可见),只要它没有被任何共享模块在链接时引用,就不会被导出到动态符号表。
2. 清除符号信息
正常情况下编译出来的共享库或可执行文件里面带有符号信息和调试信息,这些信息在调试时非常有用,但是对于最终发布的版本来说,这些符号信息用处并不大,并且使得文件尺寸变大。我们可以使用一个叫strip的工具清除掉共享库或可执行文件的所有符号和调试信息(strip是 binutils 的一部分):
$ strip libfoo.so去除符号和调试信息以后的文件往往比之前要小很多,一般只有原来的一半大小,甚至不到一半。
除了使用strip工具,我们还可以使用 ld 的-s和-S参数,使得链接器生成输出文件时不产生符号信息:
-S:消除调试符号信息-s:消除所有符号信息
我们也可以在 gcc 中通过-Wl,-s和-Wl,-S给 ld 传递这两个参数。
3. 共享库的安装
- 标准方法(需要 root 权限):复制到
/lib、/usr/lib等目录,然后运行ldconfig。 - 无 root 权限时:
- 使用
ldconfig -n shared_library_directory建立SO-NAME软链接 - 编译程序时使用
-L和-l指定库位置
- 使用
4. 共享库的构造和析构函数
4.1 指定构造函数和析构函数
很多时候你希望共享库在被装载时能够进行一些初始化工作,比如打开文件、网络连接等,使得共享库里面的函数接口能够正常工作。GCC 提供了一种共享库的构造函数,只要在函数声明时加上__attribute__((constructor))的属性,即指定该函数为共享库构造函数,拥有这种属性的函数会在共享库加载时被执行,即在程序的main函数之前执行。如果我们使用dlopen()打开共享库,共享库构造函数会在dlopen()返回之前被执行。
与共享库构造函数相对应的是析构函数,我们可以使用在函数声明时加上__attribute__((destructor))的属性,这种函数会在main()函数执行完毕之后执行(或者是程序调用exit()时执行)。如果共享库是运行时加载的,那么我们使用dlclose()来卸载共享库时,析构函数将会在dlclose()返回之前执行。声明构造和析构函数的格式如下:
void__attribute__((constructor))init_function(void);void__attribute__((destructor))fini_function(void);当然,这种__attribute__的语法是 GCC 对 C 和 C++ 语言的扩展,在其他编译器上这种语法并不通用。
注意事项:
如果我们使用了这种析构或构造函数,那么必须使用系统默认的标准运行库和启动文件,即不可以使用 GCC 的-nostartfiles或-nostdlib这两个参数。因为这些构造和析构函数是在系统默认的标准运行库或启动文件里面被运行的,如果没有这些辅助结构,它们可能不会被运行。
4.2 优先级设置
如果有多个构造函数,默认情况下它们被执行的顺序是没有规定的。如果希望构造和析构函数能够按照一定的顺序执行,GCC 提供了一个参数叫做优先级,可以指定某个构造或析构函数的优先级:
void__attribute__((constructor(5)))init_function1(void);void__attribute__((constructor(10)))init_function2(void);- 对于构造函数:优先级数字越小的函数越早执行(即优先级高)
- 对于析构函数:优先级数字越小的函数越晚执行(与构造函数相反)
这种安排有利于构造函数和析构函数能够匹配,比如某一对构造函数和析构函数分别用来申请和释放某个资源,那么它们可以拥有一样的优先级。这样做的结果往往是先申请的资源后释放,符合资源释放的一般规则。
5. 共享库脚本
共享库脚本是一种用于动态链接的链接脚本文件,它允许将多个现有的 ELF 共享对象文件(.so)组合成一个逻辑上的新共享库,对用户而言表现为一个统一的库。
5.1 主要特点
- 组合现有库:通过脚本将多个共享库(如 C 运行库、数学库等)组合在一起。
- 语法与 LD 链接脚本一致:使用与 GNU LD 链接脚本相同的命令和语法。
- 动态链接:组合过程在运行时完成,因此也称为动态链接脚本。
5.2 示例
创建一个名为libfoo.so的共享库脚本,内容如下:
GROUP(/lib/libc.so.6 /lib/libm.so.2)该脚本表示libfoo.so由libc.so.6和libm.so.2共同组成。链接器在解析libfoo.so时,会将其视为这两个库的集合。
5.3 什么叫运行时完成组合过程
脚本本身不包含真正的代码和数据。动态链接器在运行时读取这个脚本,然后按照指示去加载列出的真实共享库,并完成符号解析与重定位。因此,“链接过程是动态完成的,也就是运行时完成的”正是强调:最终的实际链接动作发生在程序运行期间,而不是编译时。
