本章开始,记录的方式将变得越来越简要(以节省时间,提升更新速度),起到一个要点记录的作用。
虽然这一章最终还是又臭又长。
——来自写完后的我
本章主要讲的是Linux下共享库的管理、安装、查找机制以及符号版本机制。
8.1 共享库版本
产生共享库需要组织的根本原因就是,不同版本的共享库的ABI(Application Binary Interface)不一样。API是源代码层面的接口,而ABI是二进制层面的接口,ABI就是API经过编译器编译后之后形成的二进制文件中的接口,由于不同版本的代码经过编译后的二进制库文件的不同,可能会改变某些ABI,进而导致不同版本的库不兼容,所以必须对不同版本的库进行管理。
按照书上的话说,ABI包括一些诸如:函数调用的堆栈结构、符号命名、参数规则、数据结构的内存分布等方面的规则。
库函数的更新分为可兼容和不可兼容两种:
兼容更新:在原先版本的共享库的基础上添加一些东西,并不改变原有的接口。
不兼容更新:改变了原先版本的共享库的某些接口。
C语言的库函数不同更改的兼容性如下:
编译器和系统库的不同(甚至版本不同)都可能会影响ABI。
C++的ABI兼容问题很严重,因为C++对其ABI并没有作出规定,所以不同的编译器甚至同一个编译器的不同版本对C++的一些特性都有着不同的方案,而且互不兼容,比如虚函数表、模板实例化、多重继承等。
代码、编译器、操作系统等因素的影响,不同的环境下,同一份源代码编译后的ABI也可能不同。
8.1.2 共享库版本命名
Linux的共享库命名规则如下:
libname.so.x.y.z
其中,so
是共共享库后缀,x
是主版本号,y
是次版本号,z
是发布版本号。
主版本号是重大升级,不同主版本之间是不兼容的
次版本号是增量升级,只增加了一些新的接口符号,在主版本号相同的前提下,高次版本向低次版本兼容。
发布版本号是一些错误的修正、性能的改进等,在主版本号和次版本号相同的情况下,不同发布版本之间完全兼容。
Linux下也有一些不符合上述规则的特例,比如Glibc,它包含了许多部分,其中的c语言库采用的方式是:
libc-x.y.z.so
,还有动态链接器ld-x.y.z.so
,Glibc还有其他的部分也是这样。
8.1.3 SO-NAME
因为不同版本的共享库不一定兼容,所以每个程序中必须包含其所依赖的库的版本信息。
因为不同的主版本是肯定不兼容的,所以系统主要通过主版本号来区分链接的版本,这个机制称为SO-NAME,也就是创建一个指向共享库的软链接,其名字只保留共享库的主版本号,例如:“/lib/libfoo.so.2.6.1”的SO-NAME就是“/lib/libfoo.so.2”。
由于某些历史原因,动态链接器的SO-NAME也与众不同,比如
ld-2.6.1.so
的SO-NAME是ld-linux.so
。
如果一个OS里有多个主版本号相同,次同版号不同的库,SO-NAME软链接会指向次版本号最新的库。
没有SO-NAME机制前,编译生成ELF文件时需要将所依赖的库的全名(比如libfoo.2.6.1
)保存到.dynamic
中,如果在新的OS环境中没有这个版本的库,就无法正常运行。
有了SO-NAME机制后,就可以将所依赖的库的SO-NAME保存到.dynamic
段中,之后运行的时候,动态链接器就可以根据SO-NAME来链接到符合条件的最新的库了。
由于次版本号是高版本兼容低版本的,更新库的时候也可以直接删掉旧的次版本号的库了,大大节省了空间。
如果是主版本升级,那么OS中会保存有多个SO-NAME。
Linux中的
ldconfig
工具,可以遍历所有默认的共享库目录(lib
、/usr/lib
),来更新所有SO-NAME的软链接的,使其指向最新的版本库。
链接名
GCC编译器可以用-l
参数使用某个共享库,具体请用看GCC的帮助或百度,这里不再赘述。
8.2符号版本
虽然SO-NAME是指向了本机器上最新的此版本号的库,但是存在这种情况:在其他机器上编译的软件依赖的库的次版本号(2.7.1)比运行该软件的环境中库的次版本号高(2.6.1),虽然SO-NAME一样,但是如果直接运行该软件的话,仍然有可能报错,因为该软件可能用到了高次版本库更新的符号。这个问题称作“次版本号交会问题”
有些OS会直接向用户发出警告但继续运行,有些OS则是直接禁止运行该软件。
为了更细致的控制符号的版本依赖问题,出现了一种基于符号的版本机制(Symbol Versioning)。思想就是给每个符号打上版本标记,比如“VERS_1.1”、“VERS_1.2”。
8.2.2 Solaris中的符号版本机制
符号的版本标记也可以看作一种集合,Solaris中程序员可以通过符号版本脚本来指定符号所属的集合,集合与几何之间也存在继承和以来的关系。
一个例子:
1 | SUNW_1.2 { |
这个脚本中定义了3个集合:”SUNW_1.2“、”SUNW_1.1“和”SUNWprivate“,第一个集合有一个全局符号swap
,同时继承了SUNW_1.1集合中的所有符号,SUNW_1.1中包含了pop
和push
两个全局符号,SUNWprivate中则声明了__pop
和__push
这两个全局符号,同时将其他所有的符号(除了这3个集合中声明过的全局符号)都声明为局部符号。
这种方法又被称为范围机制(Scoping)。
8.2.3 Linux中的符号版本
Linux下用符号版本机制用的并不广泛,主要是用共享库符号版本机制的是Glibc中的共享库。
GCC对Solaris的符号版本机制有扩展,可以在C/C++的源代码中嵌入汇编指令,一个例子:
1 | asm(".symver add, add@VERS_1.1"); // 把add添加到VERS_1.1的集合中,同时命名为add |
这个机制允许在不同的符号版本中存在相同的符号命名,类似于符号重载,一个例子:
1 | asm(".symver old_printf, printf@VERS_1.1"); |
书中有一个Linux符号版本机制的实验,可以看看。
8.3 共享库系统路径
Linux一般都遵守FHS(File Hierarchy Standard),它规定了系统中的系统文件该如何存放、组织。
FHS中规定的共享库的存放目录如下:
- /lib:最关键的和最基础的共享库,如动态链接器,C语言运行库、数学库,这些是系统工具/运行的基础
- /usr/lib:非系统运行时所需要的关键性的共享库,主要是开发时用到的库
- /usr/local/lib:主要是第三方应用的库,例如python的解释器
8.4 共享库查找过程
一个动态连接的模块所依赖的共享库的路径保存在.dynamic
段里面,由DT_NEED
类型的项表示。如果保存的是绝对路径,就直接按照这个去找;如果是相对路径,那么动态链接器就会在/lib
、/usr/lib
和/etc/ld.so.conf
配置文件中指定的目录去查找共享库。
我的wsl中的/etc/ld.so.conf
文件内容是:include /etc/ld.so.conf.d/*.conf
,也就是说包含了/etc/ld.so.conf.d/
目录下所有后缀名为conf
的配置文件的内容,具体可以进一步查看这些文件中包含的目录。
Linux中有一个ldconfig
程序,可以搜索这些目录中的库,然后更新相应的SO-NAME(需要该库指定了它自己的SO-NAME),并且存放到一个缓存中,就可以加速共享库的查找过程。每当修改了ld.so.conf
包含的内容或者手动安装了库,都应该运行一下这个程序(用包管理器安装的一般会自动帮你运行。
8.5 环境变量
环境变量,简单来说就是可以类比为变成中的变量,只不过这个变量的作用域是整个计算机系统。
同时,环境变量主要用来指示系统中一些“环境”的安装位置,比如各种编程语言的编译环境(例如Python、C++),或者是系统查找动态链接库的目录(比如LD_LIBRARY_PATH
、LD_PRELOAD
)。
PS:由于加载的全局符号会覆盖后加载的同名全局符号,
LD_PRELOAD
可以用来修改某些库的某些函数,经常被用在调试或测试中。书中还提到了一个
LD_DEBUG
环境变量,可以用来开启动态链接器的调试功能。
8.6 共享库的创建和安装
8.6.1 共享库的创建
创建很简单,就是之前讲过的生成动态链接库的过程,一个典型的命令是:
$gcc -shared -Wl,-soname,my_soname -o library_name source_files library_files
即,产生一个名为library_name
的共享库,它的so-name是my_soname
,它的源文件包括source_files
,它依赖library_files
这些库文件。
-Wl
参数用来把参数传递给链接器,这里就是传递soname参数,如果不指定soname,那么ldconfig
命令就不会查找和更新这个库的soname了。
8.6.2 清除符号信息
普通编译的共享库或可执行文件里面带有符号信息和调试信息,对于最终发布的版本来说,这些信息没什么用,所以可以用strip
工具来清除掉这些信息(或者向链接器传递-s
和-S
参数使得生成的时候就不产生这些信息)。
-s
是清除所有符号信息,-S
是清除调试符号信息。
8.6.3 共享库的安装
方法一:把共享库放在某个标准的系统共享库目录下,比如:/lib、/usr/lib等,然后运行ldconfig就行。(此方法需要root权限
方法二:手动建立SO-NAME(ldconfig -n shared_library_directory
),然后编译的时候告诉编译器和程序该去哪查找共享库(比如设置LD_LIBRARY_PATH
环境变量)。
8.6.4 共享库构造和析构函数
关于构造函数和析构函数的概念,在第四章的笔记中提到过。
GCC对C/C++语言进行了扩展,可以使用__attribute__((constructor))
和__attribute__((deconstructor))
来分别生命构造和析构函数。
还有可以指定优先级,例如:__attribute__((constructor(5)))
具体用法请使用的时候再搜索
8.6.5 共享库脚本
链接脚本有时候也可以看作一个共享库,需要符合一定的格式。比如libfoo.so
文件的内容就是一句脚本:GROUP(/lib/libc.so.6 /lib/libm.so.2)
,这个共享库相当于是把libc
和libm
这两个库组合成一个了。