大家好,欢迎来到【程序员的自我修养】专栏。正如其专栏名,本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度,我会尽自己最大的努力,争取深入浅出。若你希望与一群志同道合的朋友一起学习,也希望加入到我们的学习群中。文末有加入方式。
前几章,花了较大篇幅介绍了动态库链接过程原理,需要面对的场景以及解决思路,真真切切的了解到动态库的优秀之处。这就导致大量开始程序使用动态链接。随着系统中的动态库越来越多,也会出现一些问题。本文介绍动态库的发展过程中遇到的问题,以及如何解决的。以及非常好用的调试tips。
共享库的开发者也会不停的更新共享库的版本,用于修正原有的bug,增加新的功能或改进性能。共享库的更新可以分为两类:
兼容更新。所有的更新只是在原有的共享库基础上添加一些内容,所有原有的接口都保持不变。
不兼容更新。共享库更新改变了原有的接口,使用该共享库原有接口的程序可能不能运行或运行不正常。
常见的更新方式有以下几种:
问题:既然随着共享库的更新,可能出现兼容性问题,那么如何区分不同的版本呢?
Linux制定了一套命名规则,如下:
libname.so.x.y.z
x
表示主版本号。不同的主版本号之间表示存在重大升级,是不兼容的。y
表示次版本号。不同次版本号之间存在增量升级,且保持原来的符号不变。在主版本号相同的情况下,高的次版本号的库向后兼容低的次版本号的库。z
表示发布版本号。主要是对库进行一些错误的的修正、性能的改进,并不添加任何新的接口,也不对接口进行更新。主版本号和次版本相同的情况下,发布版本号之间相关兼容。问题:程序如何确定自己依赖哪个版本的库呢?
通过上面共享库版本可知**,主版本号和次版本号决定了共享库的接口**。理论上程序最少要知道依赖库的主版本号和次版本号。比如程序依赖libc.so.2.6
,而系统中存在libc.so.2.6.1
、libc.so.1.1.1
、libc.3.2.1
库,在动态链接过程中,动态链接器则会选择libc.so.2.6.1
。
但实际Linux 系统采用的是SO-NAME和符号版本机制实现程序对特定版本的依赖。我们一起来探讨Linux的实现过程。
SO-NAME
其表示该动态库的真正名称。它有两个作用:
ldconfig
,对动态库创建SO-NAME
的软连接。注:若动态库没有soname,则不会生成对应软链接。我们可以通过-Wl,-soname
链接参数修改动态库的SO-NAME
。如下示例:
//main.c
extern int func();
int main()
{
func();
return 0;
}
//func.c
#include
#include
int func()
{
printf("version=1.0.0\n");
return 0;
}
编译:
yihua@ubuntu:~/test/0110$ gcc -shared -fPIC -Wl,-soname,libfunc.so -o libfunc.so func.c
yihua@ubuntu:~/test/0110$ gcc main.c -o main -L. -lfunc
yihua@ubuntu:~/test/0110$
通过readelf -d libfunc.so
及ldd main
指令查看动态库的SO-NAME
及main
程序的动态库依赖:
yihua@ubuntu:~/test/0110$ readelf -d libfunc.so
Dynamic section at offset 0xe10 contains 25 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000e (SONAME) Library soname: [libfunc.so.1.0.0]
...
yihua@ubuntu:~/test/0110$ ldd main
linux-vdso.so.1 (0x00007ffd761eb000)
libfunc.so.1.0.0 => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4ae40ed000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4ae46e0000)
yihua@ubuntu:~/test/0110$
分析:
首先动态库编译,将libfunc.so
的SONAME
设置为libfunc.so.1.0.0
。
链接器在编译main
时,携带参数-lfunc
,则会在当前目录下查找libfunc.so
,确定func
符号类型。并将其SONAMElibfunc.so.1.0.0
保存到main
程序中。但是当main
运行时,则需要加载libfunc.so.1.0.0
文件,而不是找libfunc.so
。由于没有libfunc.so.1.0.0
文件,所以main
是无法执行的。如下:
yihua@ubuntu:~/test/0110$ export LD_LIBRARY_PATH=~/test/0110/
yihua@ubuntu:~/test/0110$ ./main
./main: error while loading shared libraries: libfunc.so.1.0.0: cannot open shared object file: No such file or directory
yihua@ubuntu:~/test/0110$
注:当未指定动态库的soname时,链接器默认程序依赖的动态库与文件名相同。
创建软链接:
yihua@ubuntu:~/test/0110$ ldconfig -n .
yihua@ubuntu:~/test/0110$ ls -la
total 36
drwxrwxr-x 2 yihua yihua 4096 Jan 10 01:36 .
drwxrwxr-x 13 yihua yihua 4096 Jan 9 23:24 ..
-rw-rw-r-- 1 yihua yihua 92 Jan 10 00:04 func.c
-rwxrwxr-x 1 yihua yihua 7896 Jan 10 01:20 libfunc.so
lrwxrwxrwx 1 yihua yihua 10 Jan 10 01:36 libfunc.so.1.0.0 -> libfunc.so
-rwxrwxr-x 1 yihua yihua 8288 Jan 10 01:20 main
-rw-rw-r-- 1 yihua yihua 92 Jan 10 00:03 main.c
yihua@ubuntu:~/test/0110$
如上所示,创建了一个以libfunc.so
的soname为名的软链接并指向libfunc.so
。此时则可以运行成功。
yihua@ubuntu:~/test/0110$ ldconfig -n .
yihua@ubuntu:~/test/0110$ ./main
version=1.0.0
yihua@ubuntu:~/test/0110$
问题:这样的做法有什么好处?如何使用呢?
我们可以考虑这样的一个场景:
若程序A依赖libfunc.so.2.1.1
,那么运行时,就必须要保证系统中存在libfunc.so.2.1.1
文件。同理程序B依赖libfunc.so.2.2.1
,则系统中也必须保存libfunc.so.2.2.1
文件,这就会导致磁盘的浪费。
因为根据动态库版本规则而言,我们知道libfunc.so.2.2.1
是向后兼容libfunc.so.2.1.1
。因此我们可以这样操作:
libfunc.so.2.1.1
的soname设置为libfunc.so.2
。libfunc.so.2.1.1
的soname设置为libfunc.so.2
。libfunc.so.2.2.1
,且其soname为libfunc.so.2
,再通过ldconfig
工具,创建一个软连接文件libfunc.so.2
指向libfunc.so.2.2.1
。这样既可以保证A
与B
的正常运行,也同时节约了磁盘资源。一般情况动态库的soname仅体现主版本号。如/lib/x86_64-linux-gnu/
目录下的库:
yihua@ubuntu:~/test/0110$ ls -la /lib/x86_64-linux-gnu/
total 19984
drwxr-xr-x 3 root root 16384 Oct 23 22:36 .
drwxr-xr-x 22 root root 4096 Aug 21 18:59 ..
-rwxr-xr-x 1 root root 179152 May 3 2022 ld-2.27.so
lrwxrwxrwx 1 root root 10 May 3 2022 ld-linux-x86-64.so.2 -> ld-2.27.so
lrwxrwxrwx 1 root root 15 Nov 9 2021 libacl.so.1 -> libacl.so.1.1.0
-rw-r--r-- 1 root root 31232 Apr 21 2017 libacl.so.1.1.0
lrwxrwxrwx 1 root root 15 Apr 1 2019 libaio.so.1 -> libaio.so.1.0.1
-rw-r--r-- 1 root root 5480 Apr 1 2019 libaio.so.1.0.1
-rw-r--r-- 1 root root 14920 May 3 2022 libanl-2.27.so
lrwxrwxrwx 1 root root 14 May 3 2022 libanl.so.1 -> libanl-2.27.so
lrwxrwxrwx 1 root root 20 Jun 20 2023 libapparmor.so.1 -> libapparmor.so.1.4.2
-rw-r--r-- 1 root root 64144 Jun 20 2023 libapparmor.so.1.4.2
lrwxrwxrwx 1 root root 20 Nov 9 2021 libatasmart.so.4 -> libatasmart.so.4.0.5
-rw-r--r-- 1 root root 51568 May 26 2016 libatasmart.so.4.0.5
-rwxr-xr-x 2 root root 2030928 May 3 2022 /lib/x86_64-linux-gnu/libc-2.27.so
lrwxrwxrwx 1 root root 12 May 3 2022 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.27.so
其中因为历史原因,动态链接器ld-2.27.so
和C语言库libc-2.27.so
的共享对象文件名规则不按Linux标准的共享库命名方法,但是其原理还是一样的。
动态链接器在进行动态链接时,可以通过soname对主版本号进行判断。比如程序依赖libfunc.so.2
,系统中有libfunc.so.2-->libfunc.so.2.6.1
和libfunc.so.3-->libfunc.so.3.6.1
,则会链接上libfunc.so.2.6.1
,而不是链接到libfunc.so.3.6.1
。
但是这里就存在一个问题,若程序的编译环境依赖更高的次版本号libfunc.so.2.8.1
时,那么同样也会在运行环境中链接上libfunc.so.2.6.1
。我们知道次版本号之间只保证向后兼容,不保证向前兼容,这样就容易存在问题,比如引用了libfunc.so.2.8.1
独有的符号,或新版本解决了旧版本的bug。也许程序可以继续运行,但是存在一定隐患。
为了能够将这种风险体现出来,Linux提供了一种叫做基于符号的版本机制方案,其类似于名称修饰。即给共享库中的全局符号打上一个标记。比如对应libfunc.so.2.6.1
与libfunc.so.2.8.1
库,其soname 依然为libfunc.so.2
,但是其全局符号中可以分别添加VERS_2.6
、VERS_2.8
的标签。
这也是我们经常在交叉编译环境时,遇到的错误:
Error version `GLIBC_2.34’ not found
其原因则是程序依赖的libc版本比当前系统的要高,正确且保险的做法则是下载对应或更高次版本号的glibc源码,进行交叉编译。
关于动态链接的两个不太常用但是非常有用的环境变量,和大家分享一下。
在LD_PRELOAD
里面指定的文件会在动态链接器按照固定规则搜索共享库之前加载,它比LD_LIBRARY_PATH
指令路径共享库更加优先。无论程序是否依赖于它们,LD_PRELOAD
里面指定的共享库或目标文件都会被加载。
在【程序员的自我修养09】动态链接过程的场景补充及其思考一章中,我们知道共享对象全局符号介入机制的存在。因此利用该环境变量使得我们非常方便的做到改写标准C库中的函数,而不影响其他函数,对程序的调试非常有用。
这个变量可以打开动态链接器的调试功能,打印各种有用信息,对于我们开发和调试共享库非常有用。比如:
yihua@ubuntu:~/test/0110$ LD_DEBUG=files ./main
38378:
38378: WARNING: Unsupported flag value(s) of 0x8000000 in DT_FLAGS_1.
38378:
38378: file=libfunc.so.1.0.0 [0]; needed by ./main [0]
38378: file=libfunc.so.1.0.0 [0]; generating link map
38378: dynamic: 0x00007f70f7c8fe10 base: 0x00007f70f7a8f000 size: 0x0000000000201030
38378: entry: 0x00007f70f7a8f540 phdr: 0x00007f70f7a8f040 phnum: 7
38378:
38378:
38378: file=libc.so.6 [0]; needed by ./main [0]
38378: file=libc.so.6 [0]; generating link map
38378: dynamic: 0x00007f70f7a88b80 base: 0x00007f70f769e000 size: 0x00000000003f0ae0
38378: entry: 0x00007f70f76bfda0 phdr: 0x00007f70f769e040 phnum: 10
38378:
38378:
38378: calling init: /lib/x86_64-linux-gnu/libc.so.6
38378:
38378:
38378: calling init: /home/yihua/test/0110/libfunc.so.1.0.0
38378:
38378:
38378: initialize program: ./main
38378:
38378:
38378: transferring control: ./main
38378:
version=1.0.0
38378:
38378: calling fini: ./main [0]
38378:
38378:
38378: calling fini: /home/yihua/test/0110/libfunc.so.1.0.0 [0]
38378:
yihua@ubuntu:~/test/0110$
可知,动态链接器将整个装载过程,显示程序依赖哪个共享库并且按照什么步骤装载和初始化,共享库装载时的地址等。它还具备其它参数:
bindings
,显示动态链接的符号绑定过程。libs
,显示共享库的查找过程。versions
,显示符号的版本依赖关系。reloc
,显示重定位过程。symbols
,显示符号表查找过程。statistics
,显示动态链接过程中的各种统计信息。all
,显示以上所有信息。建议大家实操一下,加强印象。
本文花了较大篇幅介绍了动态库如何解决和避免兼容性问题,以及分享两个分享实用的环境变量LD_PRELOAD
、LD_DEBUG
,至此关于动态库的分享算是结束了。下一章节,我们开始向《库与运行库》进军。
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途。