程序员的自我修养——Linux共享库
Linux 共享库
共享库(Shared Library)其实在文件结构上和共享对象没有什么区别,Linux 下的共享库就是普通的 ELF 共享文件。由于共享对象可以被各个程序使用,所以它就成了库的很好的存在形式。
共享库的命名
有必要了解一下你的 Linux 里面那群 xxx.so.1… 表示啥含义的
-
共享库的命名模板为:
libname.so.x.y.z
- 其中最前面使用前缀 “lib”,中间的库名和 “.so” 后缀。最后面跟着的三个数字表示版本号。
- “x” 表示主版本号(Major Version Number),表示库的重大升级,不同主版本号之间不兼容。
- “y” 表示次版本号(Minor Version Number),表示库的增量升级,即增加一些新的接口符号,且保持原来的符号不变。
- “z” 表示发布版本号(Release Version Number),表示库的一些错误的修正,性能改进,并不增加新的接口,也不对接口进行更改。
大体上按照这样的标准,但实际上有不少库的名字并不符合这个模板。
SO-NAME
在 Linux 系统中,系统会为每个共享库在它的所在目录创建一个和 “SO-NAME” 相同的并且指向它的软链接(Symbol Link)。比如说系统中有一个共享库 /lib/libfoo.so.2.6.1
,那么 Linux中的共享库管理程序就会为它产生一个软链接 /lib/libfoo.so.2
指向它。
建立以 SO-NAME 为名字的软链接的目的是,使得所有依赖某个共享库的模块,在编译,链接和运行时,都使用共享库的 SO-NAME,而不使用详细的版本号。
共享库系统路径
目前大多数包括 Linux 在内的开源操作系统都遵循一个叫做**FHS(File Hierarchy Standard)**的标准。根据其规定,一个系统中主要有3个存放共享库的位置:
/lib
: 这个位置主要放系统最关键和基础的共享库,比如说动态链接器,C 语言运行库,数学库/usr/lib
: 这个目录下主要保存的是一些非系统运行时所需要的关键性的共享库,主要是一些开发时用到的共享库,这些共享库一般不会被用户的程序或 Shell 脚本直接用到。这个目录下面还包含了一些开发时可能用到的静态库,目标文件。/usr/local/lib
: 这个目录用来放置一些跟操作系统本身并不相关的库,主要是一些第三方应用程序的库。比如说 python 语言的解释器。GNU 的标准推荐第三方的应用程序的库全部安装到usr/local/lib
下。
共享库查找过程
在开源的系统中,包括所有的 Linux 系统在内的很多都是基于 Glibc 的。我们知道在这些系统里面,动态链接的 ELF 可执行文件在启动的同时会启动动态链接器。在 Linux 系统中,动态链接器是 /lib/ld-linux.so.X(version)
。程序所依赖的共享对象全部由动态链接器负责装载和初始化。任何一个动态链接器的模块所依赖的模块路径保存在 “.dynamic” 里面,由 DT_NEED 类型的项来表示。如果保存的是绝对路径,则就会去按照这个路径寻找。如果是相对路径,那么动态链接器会在 /lib
, /usr/lib
和 /etc/ld.so.conf
配置文件指定的目录中查找共享库。
1 | cat ld.so.conf |
如果动态链接器每次都要去查找这些文件目录,将会非常耗时间。所以 Linux 系统中有一个叫做 ldcofig 的程序,这个程序的作用是为共享库目录下的各个共享库文件创建,删除,更新相应的 SO-NAME(符号链接),这个程序还会收集这些 SO-NAME,集中放到 /etc/ld.so.cache
里面查找。如果没有找到,还是需要遍历 /lib
, /usr/lib
这两个目录去寻找,如果还是找不到就链接失败。
理论上我们增加,删除,更新任意一个共享库或者我们修改了 /etc/ld.so.conf
的配置,都需要运行 idcofig 这个程序。很多软件包的安装程序往往在里面安装共享库以后都会调用 idcofig
环境变量
LD_LIBRARY_PATH
Linux 提供很多方法来改变动态链接装载共享库路径的方法,使用这些方法我们可以满足一些特殊的要求,比如共享库的调试和测试,应用程序级别的虚拟等。改变共享库查找路径最简单的方法是使用 LD_LIBRARY_PATH 环境变量,这个方法可以临时改变某个应用程序的共享库查找路径,而不会影响系统中的其他程序。
总的来说,动态链接器会按照下列顺序依次装载或查找共享对象(目标文件):
- 由环境变量 LD_LIBRARY_PATH 指定的路径
- 由路径缓存文件
/etc/ld.so.cache
指定的路径 - 默认共享库目录,先
/usr/lib
再/lib
LD_PRELOAD
系统中另外还有一个环境变量叫做 LD_PRELOAD,这个文件中我们可以指定预先装载的一些共享库甚或是目标文件。在LD_PRELOAD 里面指定的文件会在动态链接器按照固定的规则搜素共享库之前装载,它比 LD_LIBRARY_PATH 里面所指定的目录中的共享库还要优先。无论程序是否依赖于它们,LD_PRELOAD 里面指定的共享库或目标文件都会被装载。
由于全局符号介入这个机制的存在,LD_PRELOAD 里面指定的共享库或目标文件中的全局符号就会覆盖后面加载的同名全局符号,正常情况下应该避免使用 LD_PRELOAD
LD_DEBUG
这个变量可以打开动态链接器的调试功能,当我们设置这个变量是,动态链接器会在运行时打印各种有用的信息。
值 | 含义 |
---|---|
bindings | 显示动态链接的符号绑定过程 |
libs | 显示共享库的查找过程 |
versions | 显示符号的版本依赖关系 |
reloc | 显示重定位过程 |
symbols | 显示符号表查找过程 |
statistics | 显示动态链接过程中的各种统计信息 |
all | 显示以上全部信息 |
help | 显示上面的各种可选值和帮助信息 |
我们运行一个简单的动态链接程序
1 |
|
用 gcc 编译然后设置 LD_DEBUG 值执行:
1 | LD_DEBUG=libs ./hello |
共享库的创建
创建共享库的过程和创建一般的共享对象的过程基本一致,最关键的是使用 GCC 的两个参数,即 “-shared” 和 “-fPIC”。“-shared” 表示输出结果是共享库类型的, “-fPIC” 表示使用地址无关代码的技术来生成输出文件。另外还有参数是 “-WI”,这个参数可以将指定的参数传递给链接器,比如当我们使用 “-Wl, -soname, my_soname” 时,GCC 会将 “-soname my_soname” 传递给链接器来指定输出共享库的 SO-NAME。
我们可以使用下面的命令来生成一个共享库
$gcc -shared -Wl, -soname, my_soname -o library_name source_files...
- !如果我们不使用 -soname来指定,那么该共享库默认没有 SO-NAME,使用 idconfig 更新软链接时对该共享库是无效的
例如我们有源码 “libfoo1.c” 和 “libfoo2.c”,我们希望产生一个 “libfoo.so.1.0.0” 的共享库,这个共享库依赖于 “bar1.so” 和 “bar2.so”,这样你就可以使用下面命令:
$gcc -shared -fPIC -Wl, -soname, libfoo.so.1 -o libfoo.so.1.0.0 libfoo1.c libfoo2.c -lbar1 -lbar2
多步进行:
1 | gcc -c -g -Wall -o libfoo1.o libfoo1.c |
清除符号信息
正常情况下编译出来的共享库或可执行文件里面带有符号信息和调试信息,这些信息在调试时很有用,但对于最终的发布版本意义不大。我们可以使用一个叫 “strip” 的工具清除掉共享库或可执行文件的所有符号和调试信息,这样可以减少文件的大小
strip libfoo.so
也可以使用 ld 的 “-s” 和 “-S” 参数,使得在生成输出文件时就不产生符号信息,前者消除所有的符号信息,后者消除调试符号信息。
程序员的自我修养——Linux共享库