0%

《程序员的自我修养》 Ch8 Linux共享库的组织

本章开始,记录的方式将变得越来越简要(以节省时间,提升更新速度),起到一个要点记录的作用。

虽然这一章最终还是又臭又长。

​ ——来自写完后的我

本章主要讲的是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SUNW_1.2 {
global:
swap;
}SUNW_1.1;

SUNW_1.1 {
global:
pop;
push;
}

SUNWprivate {
global:
__pop;
__push;
local:
*;
}

这个脚本中定义了3个集合:”SUNW_1.2“、”SUNW_1.1“和”SUNWprivate“,第一个集合有一个全局符号swap,同时继承了SUNW_1.1集合中的所有符号,SUNW_1.1中包含了poppush两个全局符号,SUNWprivate中则声明了__pop__push这两个全局符号,同时将其他所有的符号(除了这3个集合中声明过的全局符号)都声明为局部符号。

这种方法又被称为范围机制(Scoping)

8.2.3 Linux中的符号版本

Linux下用符号版本机制用的并不广泛,主要是用共享库符号版本机制的是Glibc中的共享库。

GCC对Solaris的符号版本机制有扩展,可以在C/C++的源代码中嵌入汇编指令,一个例子:

1
2
3
4
5
asm(".symver add, add@VERS_1.1");	// 把add添加到VERS_1.1的集合中,同时命名为add
int add(int a, int b)
{
return a+b;
}

这个机制允许在不同的符号版本中存在相同的符号命名,类似于符号重载,一个例子:

1
2
3
4
5
6
asm(".symver old_printf, printf@VERS_1.1");
asm(".symver new_printf, printf@VERS_1.2"); //外部的程序可以根据符号版本的不同来访问不同版本的printf
int old_printf()
{
...
}

书中有一个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_PATHLD_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),这个共享库相当于是把libclibm这两个库组合成一个了。