程序员的自我修养-第八章
。
共享库版本
兼容性
共享库的更新可能兼容可能不兼容。
以前还以为这种库提供的接口只是属于API层面,现在看看其实这些接口属于ABI层面。因为这些库(以ELF文件形式存在的动态链接文件)其中的一些操作,比如参数传递方式、虚函数表、多重继承等都给出了明确定义,但这些东西在库的使用者电脑上可能是完全不同的,这就可能导致兼容性问题。
其实想想啊,如果不是以共享库的形式,而是以源代码的形式提供给用户,是不是接口就只是在API层面,就不用考虑ABI层面的兼容问题了呢?
共享库命名方式
- 主版本,不同号码,可以不兼容
- 次版本,高级的可以兼容低级的,一般是增加一些接口
- 发布版本号,一般是修改库的错误,性能,不提供新接口
不遵守规则的库,比如 /lib/x86_64-linux-gnu/libc-2.31.so
SO-NAME
Linux使用一种SO-NAME的方式来记录共享库的依赖关系。SO-NAME就是去掉第二个和第三个版本号剩下的部分。系统会为每个共享库在他的目录创建一个和SO-NAME相同的软链接指向该库。标准格式:libfoo.so.2.6.1
这些库这么喜欢标新立异吗
1
2
3
4
5
6
7
8
9 lrwxrwxrwx 1 root root 10 5月 1 2024 ld-linux-x86-64.so.2 -> ld-2.31.so
lrwxrwxrwx 1 root root 14 5月 1 2024 libanl.so.1 -> libanl-2.31.so
lrwxrwxrwx 1 root root 17 2月 18 2020 libaudit.so.1 -> libaudit.so.1.0.0
lrwxrwxrwx 1 root root 17 4月 9 2024 libblkid.so.1 -> libblkid.so.1.1.0
lrwxrwxrwx 1 root root 18 4月 27 2021 libbrlapi.so.0.6 -> libbrlapi.so.0.6.6
lrwxrwxrwx 1 root root 18 3月 4 2020 libbrlapi.so.0.7 -> libbrlapi.so.0.7.0
lrwxrwxrwx 1 root root 23 5月 1 2024 libBrokenLocale.so.1 -> libBrokenLocale-2.31.so
lrwxrwxrwx 1 root root 15 9月 5 2019 libbz2.so.1 -> libbz2.so.1.0.4
lrwxrwxrwx 1 root root 15 9月 5 2019 libbz2.so.1.0 -> libbz2.so.1.0.4
这个SO-NAME其实会指向目录中主版本号相同、次版本号和发布版本号最新的共享库。这使得以来某个共享库的模块在编译链接运行时,都使用共享库的SO-NAME, 而不需要详细的版本号。
之前讲过,动态链接文件的.dynaamic
有DT_NEED类型的字段,如果值为B,则代表依赖于B,运行时需要将B加载到内存。但如果直接存储B的全称,不太方便维护库(只依赖于某个具体版本的库,变一个数字都不行),所以改为存储共享库的SO-NAME, 使用SO-NAME保证了兼容性,同时也能指向最新的共享库,可以避免维护太多库。
Linux提供了
ldconfig
这个工具,当安装或更新共享库时,他会遍历所有的默认共享库目录如/lib, /usr/lib
, 然后更新所有的软链接,使其指向新的共享库。
符号版本
前面的SO-NAME有一个问题,那就是只判断主版本号。如果一个程序依赖于1.8.1版本的库,但是一个电脑中只有1.1.1版本的库,那么因为SO-NAME指向了这个1.1.1的库,程序表面上能正常运行。但如果程序使用了1.8.1存在的函数接口但这些接口在1.1.1不存在,就会出错。这是次版本交会问题。
基于符号的版本控制
给每个新添加的符号也增加个版本标记,如果链接发时现机器上的中符号的版本过低,会报错
共享库存放路径
许多Linux在内的操作系统都遵循FHS标准.一个系统有三个存放动态库的地方:
共享库查找过程
动态连接的ELF启动过程中,当系统调用返回时会返回到动态链接器继续执行,动态链接器会根据.dynamic段中的地址进行查找库.
- 如果DT_NEED保存的绝对地址,那么就找这个地址
- 如果是相对地址,那么动态链接器会在
/lib, /usr/lib
和/etc/ld.so.conf 配置文件
指定的目录中查找共享库
如果每次执行时都需要动态链接器去找这些文件,会费耗时,所以系统有个名为ldconfig的程序,他会
- 及时更新,创建,删除SO-NAME
- 收集SO-NAME,并存放到
/etc/ld.so.cache
中,这个文件非常适合查找.(如果在这个文件中没找到才会遍历文件夹进行寻找)
所以理论上往系统的共享库目录下添加、删除、更新任何一个共享库,都应该执行ldconfig,来更新SO-NAME 和 ld.so.cache.
环境变量
每个进程会有一个从父进程继承来的环境变量(环境变量存放在每个进程中,每个进程可以有不同的变量)。BASH进程启动时,也会从一些文件中加载全局变量,比如:
- 全局环境变量:这些环境变量对系统中的所有用户和所有进程都是可用的。它们在
/etc/profile
、/etc/environment
或其他系统级别的配置文件中设置。 - 用户环境变量:这些环境变量只对特定的用户可用,通常在用户的家目录下的
.bash_profile
、.bashrc
或.profile
文件中设置。 - 局部环境变量:这些环境变量只在特定的进程或脚本中有效,可以通过在命令行中使用
export
命令来设置。
通常要让应用程序在包含某些路径的环境变量下执行,不是直接设置这些应用程序的环境变量,而是在bash进程中先把环境变量设定为期望的值,然后再bash中启动进程。(这样应用程序会继承父进程即bash进程的环境变量,巧妙)
LD_LIBRARY_PATH
改变共享库查找路径最简单的方式就是修改这个变量,这个路径有若干个路径组成,之间有冒号隔开。
动态链接器会按照如下路径查找共享对象:(如果是绝对地址,那么直接访问就行了)
LD_LIBRARY_PATH
指定的路径- 由
ldconfig
维护的/etc/ld.so.cache
指定的路径 - 默认共享库目录,比如
/usr/lib,/lib
LD_DEBUG
这个变量可以打开动态链接器的调试功能,动态链接器在运行时会打印很多调试信息。比如将LD_DEBUG设置为files,然后运行一个动态连接的文件。
1 | moveai415:~/cpp/effective_cpp$ LD_DEBUG=files ./ab |
共享库的创建和安装
共享库的创建
前面用-shared, -fPIC
创建过共享对象,共享库的创建也非常类似。最关键的一点是需要给共享库指定一个soname
,可以向链接器传递-soname my_soname
来指定(不然ldconfig对该共享库没有效果),如果要是有GCC来编译,那么前面还需要加一个-W1
。
gcc -shared -W1,-soname, my_soname -o library_name source_files library_files
- 要想测试一下共享库,又不影响其他程序的运行,可以设置待测试进程的bash进程的
LD_LIBRARY_PATH
, 也可以使用连接器的-rpath
选项来指定共享库的查找路径。 - 假设一个进程由一个主模块和其他共享模块组成,默认情况下,链接器在生成可执行文件时,只会将主模块中被其他共享模块使用到的符号放到动态符号表。如果程序使用
dlopen
动态加载模块,可能导致没法访问主模块的符号(按道理说,链接时不会去检查符号吗),可以使用-export-dynamic
参数来导出所有的全局符号。
清除符号信息
可以用strip来删除共享库或者可执行文件中的所有符号和调试信息。但还是非保留需要动态链接相关的符号。
1 | 删除前: |
共享库的安装
最简单的方法:将库复制到标准的共享库目录,然后运行ldconfig即可,但这需要root权限。
稍复杂的方法:
- 建立SO-NAME软链接(通过ldconfig -n your_directory 来建立)
- 然后告诉编译器和程序如何查找该库(
-L, -l
参数以及-rpath
参数以及LD_LIBRARY_PATH
)。
共享库构造以及析构函数
定义如下的构造/析构函数,可以在共享库加载时(main之前)被执行构造函数,可以在main执行完毕后执行析构函数。