文章目录一、Linux的共享库的管理1. 共享库版本1.1 共享库兼容性2. 共享库版本命名3. SO-NAME4. 符号版本4.1 次版本号交会问题4.2 Solaris中基于符号的版本机制也说明范围机制4.2.1 概述4.2.2 案例演示4.2.2.1 初始版本libstack.so.1版本 SUNW_1.14.2.2.2 增量升级添加新接口 swap版本 SUNW_1.24.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_PATH2. LD_PRELOAD3. 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 共享库兼容性兼容更新在原有接口基础上添加内容原有接口保持不变。不兼容更新改变或删除原有接口导致依赖程序无法运行。ABIApplication Binary Interface二进制接口包括函数调用堆栈结构、符号命名、参数规则、数据结构内存分布等。更改类型兼容性往共享库中添加一个导出符号兼容删除一个原有的导出符号不兼容给导出函数添加一个参数不兼容删除导出函数中的一个参数不兼容改变导出函数中使用的结构类型的长度、内容、成员类型不兼容修正Bug或改进性能不改变接口类型兼容修正Bug或改进性能同时改变接口类型不兼容导致C语言共享库ABI改变的4种行为导出函数的行为发生改变导出函数被删除导出数据结构的内存布局发生变化导出函数的接口发生变化返回值、参数数量等C共享库ABI注意事项尽量避免使用C接口不要在接口类中使用虚函数或不要随意删除/添加不要改变类中成员变量的位置和类型不要删除非内嵌的public/protected成员函数不要将非内嵌成员函数改为内嵌不要改变成员函数的访问权限不要在接口中使用模板最好不要使用C作为共享库接口2. 共享库版本命名规则libname.so.x.y.zx主版本号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.mapSUNW_1.1{global: push;pop;local: *;# 将所有未在 global 中列出的符号隐藏为局部};SUNW_1.1是版本集合的名称通常以SUNW_为前缀。global块列出要导出的公共符号。local: *;表示除上述符号外其他所有全局符号都降为局部外部无法访问。编译生成共享库gcc-shared-Wl,-soname,libstack.so.1 -Wl,--version-scriptlibstack.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.mapSUNW_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-scriptlibstack.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 查看符号版本信息使用pvsSolaris 下的符号版本查看工具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.3GLIBC_2.2、GLIBC_2.2.1、GLIBC_2.2.2、GLIBC_2.2.3、GLIBC_2.2.4、GLIBC_2.2.6GLIBC_2.3、GLIBC_2.3.2、GLIBC_2.3.3、GLIBC_2.3.4GLIBC_2.4、GLIBC_2.5、GLIBC_2.6这些版本号代表着 Glibc 每次添加新接口时的符号集合每个新版本继承旧版本的所有符号。特殊版本标签GCC_前缀用于 GCC 编译器相关的符号普通程序不应依赖。GLIBC_PRIVATEGlibc 内部使用的符号不对外公开可能在版本升级中被删除或改变。开发者使用这些符号需要“后果自负”。稳定库的示例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, addVERS_1.1);intadd(inta,intb){returnab;}这样符号add被标记为版本VERS_1.1。当程序链接到这个共享库时链接器会记录对addVERS_1.1的依赖。4.3.2.2 同一符号的多版本共存符号重载Linux 的符号版本机制允许同一个符号名存在多个不同版本的实现。这解决了 Solaris 2.5 的缺陷——每个符号只能有一个版本。典型场景当共享库升级时某个函数的接口或行为发生变化。若直接覆盖原符号依赖旧接口的程序将无法运行。通过多版本机制可以在同一个共享库中同时保留旧版本和新版本的实现并根据程序的链接版本自动选择正确的符号。示例asm(.symver old_printf, printfVERS_1.1);asm(.symver new_printf, printfVERS_1.2);intold_printf(constchar*format,...){// 旧的实现行为与 1.1 版一致}intnew_printf(constchar*format,...){// 新的实现行为改变或参数不同}old_printf被别名为printfVERS_1.1。new_printf被别名为printfVERS_1.2。链接行为程序在编译时如果链接到VERS_1.1版本的库最终可执行文件会记录对printfVERS_1.1的依赖运行时链接器将其解析为old_printf。如果程序链接到VERS_1.2版本则会记录printfVERS_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-scriptgcc-shared-fPIClib.c -Wl,--version-scriptlib.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: versionVERS_1.2not 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-gnu6.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_DEBUGfiles ./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-NAMEldconfig 对其无效。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 什么叫运行时完成组合过程脚本本身不包含真正的代码和数据。动态链接器在运行时读取这个脚本然后按照指示去加载列出的真实共享库并完成符号解析与重定位。因此“链接过程是动态完成的也就是运行时完成的”正是强调最终的实际链接动作发生在程序运行期间而不是编译时。