第八章:Linux共享库的组织
1.共享库版本不同,可能引起ABI发生变化,从而影响程序的正常运行。因此各个系统会有自己的一套共享库版本命名规则。Linux的为
libname.so.x.y.z
x:主版本号,y:次版本号,z:发布版本号
不同主版本之间共享库差异大,互不兼容。
次版本之间仅是添加了新的符号,互相兼容。
发布版本之间修改了接口的bug,互相兼容。
2.程序依赖的共享库由.dynamic段指出,若指出完整版本(主次发布都有)的共享库名字,则每次更新共享库必须保存之前的版本,会造成磁盘和内存(会同时装载不同版本的共享库)的压力巨大,因此Linux制订了SO-NAME命名规则。
libname.so.x
x为共享库的主版本号。
系统会给每个共享库所在的目录创建一个与SO-NAME同名的软连接,指向最新版本的共享库(主版本号相同),可见,一个共享库若安装了多个主版本,则会有多个对应的软连接。
Linux中有一个工具“ldconfig”,当系统安装或更新一个共享库时,会遍历默认共享库目录,更新或创建相应的软连接。
gcc -lXXX 表示链接XXX共享库,系统会查找最新版本的XXX库(往往由-L指定)。其可以是动态的或是静态的,使用-lc 会根据输出文件的情况选择合适版本(静态或是动态)的库。
3.SO-NAME引发的次版本号交会问题及符号版本机制解决方案
若应用程序依赖lib.so的2.5版本而系统中只有2.3版,因为SO-NAME相同程序会正常运行,但是若引用到了2.5添加的符号则会异常退出,这就是次版本号交会问题。
符号版本就是在这个前提下引入的。共享库构建的链接过程可以链接编写好的符号版本脚本。脚本里就是一些集合,集合有自己的名称如VERS_1.1,集合里包含共享库定义的符号,这样共享库的符号都有了自己的版本(所属集合名称)。同时集合之间可以有继承关系,如VERS_1.2继承于VERS_1.1,这样共享库构建完成的.so文件就会包含集合的相关内容。
当应用程序在链接lib.so这个共享库时就会在程序的最终输出文件(往往是可执行文件)记录下该应用程序所依赖的共享库最低符号集合版本,注意不是系统拥有的该共享库最高版本。
在程序运行时,动态连接器就可以通过程序记录的它所依赖的所有共享库符号集合版本判断系统安装的对应共享库是否符合要求,若不符合,则阻止程序运行。
符号版本方法是对SO-NAME机制的一种补充。
----Linux的符号版本
Linux下符号版本机制并没有广泛应用,主要使用的是glibc下的一部分共享库。
Linux下对Solaris的符号版本有两种扩展。
除了可以在符号版本脚本中指定符号的版本外,可以在c/c++源代码中通过
asm(".symver symbolInCode,symbolInScript@VERS_1.1")
添加符号到符号标签。
gcc允许同一个符号存在于多个版本的共享库中,在链接层面的一种符号重载机制。
这种重载机制的好处是,可以在少量已有接口的接口或含义修改时,不用升级主版本,即可保证不影响旧版的功能。
asm(".symver newprintf,printf@VERS_1.1")
asm(".symver oldprintf,printf@VERS_1.1")
void oldprintf(){}
void newprintf(){}
4.共享库系统路径
/usr/lib
/lib 下一般是系统本身所需要的库;
/usr/local/lib一般是非系统所需的第三方程序的共享库。
5.共享库的查找过程
共享库的查找是由动态链接器(/lib/ld-linux.so.x)依据任何一个动态链接的模块的“.dynamic”段里由DT_NEED类型的项指出的。
而其值若为绝对路径则依据绝对路径查找,相对路径的话则会在/lib,/usr/lib和/etc/ld.so.conf配置文件指定的目录中查找。而为了共享库的可移植性,其路径往往是相对的。
若ld每次都遍历这些目录,则会非常耗时。因此前文提到的有一个叫ldconfig程序,除了前文说的负责共享库的SO-NAME软链接,它还会收集这些SO-NAME已特殊数据结构存储在/etc/ld.so.cache,从而加快查找速度。若ld在cache里没有找到,则会遍历/lib,/usr/lib,无则失败。
因此理论上每次增删更新共享库都应该运行一次ldconfig,许多软件包安装好共享库后都会调用它。
6.3个与动态链接相关的环境变量
LD_LIBRARY_PATH
改变ld查找共享库的过程,会先查询由该变量指定的路径在默认查找。方便共享库开放的调试和测试。不应被滥用。且会影响gcc编译查找库的路径,效果相当于gcc的-L参数。
LD_LIBRARY_PATH=/home/mary:/home/ben /bin/ls
LD_PRELOAD
会提前装载指定的共享库,比前面那个变量优先级高。由于全局符号介入机制的存在,可以方便改写标准C库的某个或某几个函数而不影响其他函数,调试和测试有用。
(/etc/ld.so.preload与该变量作用一样)
LD_DEBUG
根据值的不同,动态链接器会在运行时打印出各种有用的信息。
LD_DEBUG=files ./helloworld.out
7.共享库的创建
gcc编译时,除了的-fPIC,-shared参数,还有传给链接器的-W1,-so-name,mysoname参数以指定输出共享库的SO-NAME。
不用这个参数的话,该共享库默认没有SO-NAME,即使用ldconfig更新SO-NAME软链接,对该共享库没有效果。
注意事项:
不要将输出共享库的符号和调试信息去掉,也不要使用gcc的"-fomit-frame-pointer"去掉栈帧选项,会影响共享库调试。
测试新共享库而不想影响现有程序运行。除了使用LD_LIBRARY_PATH,还可以使用ld的"-rpath"选项指定目标程序的共享库查找路径。
默认情况下,链接器产生的可执行文件中,只会将主模块被其他共享模块引用到的符号放在动态符号表中,以减少动态符号表大小。而这会造成一种情况是,当程序事业dlopen动态加载共享模块,而它需要引用主模块的符号时,而因为该符号没有在动态符号表中从而反向引用失败。链接器提供"-export-dynamic"参数将可执行文件(主模块)中所有全局符号导出到动态符号表中,以防出现上述问题。
对于发布版本来说,符号作用不大可以使用“strip”工具清除掉共享库或可执行文件的所有符号和调试信息或ld的“-s”或“-S”参数。
strip libfoo.so
需要系统root权限的方法:
将共享库复制到/lib,/usr/lib等系统目录下,运行ldconfig即可。
不需要root权限的方法:
使用ldconfig建立SO-NAME(以供动态链接器查找)
ldconfig -n shared_Library_directory
且编译程序时也需要指定共享库的位置(-L和-l,或-rpath参数(静态连接器需要)
10.gcc中共享库的构造和析构函数
attribute((constructorx))
attribute((destructorx))指定,x指定优先级,可以不写。构造函数中x越小优先级越高,析构函数中x越小优先级越低,符合先申请的资源后释放原则。
这种方式下共享库的构建不可以使用gcc的“-nostartfiles”或“-nostdlib”参数 因为这些构造函数和析构函数是在系统默认的标准运行库或启动文件里被运行的。没有这些辅助结果,它们不会运行。
11.共享库脚本
可以将几个现有共享库组合起来,从用户的角度来看就是一个新的共享库。
GROUP(/lib/libc.so.6 /lib/libm.so.2)
语法命令方面跟链接脚本没有什么不同,作用也相似,即将一个或多个输入文件以一定格式经过变换后形成一个输出文件,所以共享库脚本也叫做动态链接脚本,链接过程是在运行时完成的。