原文链接:再谈Linux下的动态库
发布时间:2015-08-26 18:35:55
为了解决上一篇的博客《Linux下静态库、动态库的创建和使用》最后留下的问题,今天总结一下Linux下动态库版本号的控制。
一、动态库版本号的组成
对于上一篇博客中提到的库文件libcurl.4.3.0,其中4代表主版本号,3代表次版本号,0代表发行版本号,因此动态库的命名形式为:libname.x.y.z
x – 主版本号(不兼容):重大升级,不同主版本的库之间的库是不兼容的。所以如果要保证向后兼容就不能删除旧的动态库的版本。
y – 次版本号(向下兼容): 增量升级,增加一些新的接口但保留原有接口。高次版本号的库向后兼容低次版本号的库。
z – 发布版本号(相互兼容):库的一些诸如错误修改、性能改进等,不添加新接口,也不更改接口。主版本号和次版本号相同的前提下,不同发布版本之间完全兼容。
二、动态库的3个名字
1、real name
动态库的real name即动态库本身的文件名,例如libcurl.so.4.3.0
2、soname (short for shared object name)
动态库的soname即是libname.so.x,显然是保留了主版本号,例如libcurl.so.4。
3、linker name
动态库的linker name即链接名,libname.so,去掉了后面的版本号,例如libcurl.so。
三、创建带有soname的动态库
在我的上一篇博客中创建的动态库是没有版本号的,也没有soname的。
假如我要开发并维护一个动态库,库的版本控制是必要的,就要按照上述版本号命名的方法。
1 2
| gcc -c -fpic mylib.c -o mylib.o gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0.0 mylib.o
|
生成了库文件libmylib.so.1.0.0,然后执行命令:
1
| readelf -d libmylib.so.1.0.0
|
结果如下:
1 2 3 4 5 6 7 8 9 10 11 12
| Dynamic section at offset 0x668 contains 21 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000e (SONAME) Library soname: [libmylib.so.1]
0x000000000000000c (INIT) 0x468
0x000000000000000d (FINI) 0x5c8 ...
|
由上面的结果可以看到,有一个SONAME字段,Library soname:libmylib.so.1,由此得出结论,在编译生成动态库时,由于我指定了库文件的soname为libmylib.so.1,编译器会这个soname写入库文件的头部。还有一个NEEDED字段表示该动态库所依赖的其他动态库,在本例中是依赖标C库。
四、使用带有SONAME的动态库
测试环境:CentOS6.5 x86_64 GCC 4.4.7
编译:
1
| gcc main.c -o main -L. -lmylib
|
编译报错:
1 2
| /usr/bin/ld: cannot find -lmylib collect2: ld returned 1 exit status
|
这样编译无法通过,上篇博客已经说明原因。
这样编译:
1
| gcc main.c -o main libmylib.so.1.0.0 -Wl,-rpath=/home/user/lib
|
可以通过,这样是直接把libmylib.so.1.0.0当成目标文件,完成链接过程,生成可执行程序main
然后执行命令:
1 2 3 4 5 6 7 8 9 10
| Dynamic section at offset 0x7a8 contains 22 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libmylib.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000f (RPATH) Library rpath: [/home/user/lib] ...
|
由以上执行结果可知,main依赖2个动态库,分别是libmylib.so.1,libc.so.6
显然libmylib.so.1是libmylib.so.1.0.0的SONAME,由此得出结论:
编译器在链接动态库时,会读取库的SONAME,并且写在可执行文件的头部,那么程序在运行时, 会根据自己的头部记录的SONAME,去搜索对应的动态库。其中RPATH字段是动态库搜索路径,程序运行时,会首先去这个路径寻找所依赖的动态库。
但是此时,生成的main并不能执行,报错如下:
1 2
| ./main: error while loading shared libraries: libmylib.so.1: cannot open shared object file: No such file or directory
|
为何不能执行呢?执行:
结果如下:
1 2 3 4 5 6 7 8 9
| [root@localhost lib]# ldd main
linux-vdso.so.1 => (0x00007fff7c3ff000)
libmylib.so.1 => not found
libc.so.6 => /lib64/libc.so.6 (0x00000034b0800000)
/lib64/ld-linux-x86-64.so.2 (0x00000034b0000000)
|
原来libmylib.so.1这个文件 not found,这个文件确实没有,只有libmylib.so.1.0.0,由此得出结论:
程序运行会很据自己头部信息去寻找这些以SONAME命名的库文件。
上一篇博客提到,使用动态库有4种方法,上面我们使用的是最后一种方法,那么再试试其他方法吧!!!
1 2 3
| gcc main.c -o main libmylib.so.1.0.0
ldconfig
|
查看当前目录有何变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| [root@localhost lib]# ll
total 32
lrwxrwxrwx. 1 user user 17 Aug 26 15:32 libmylib.so.1 -> libmylib.so.1.0.0
-rwxrwxr-x. 1 user user 5900 Aug 26 14:37 libmylib.so.1.0.0
-rwxrwxr-x. 1 user user 6731 Aug 26 15:13 main
-rw-r--r--. 1 user user 345 Aug 26 14:52 main.c
-rw-rw-r--. 1 user user 340 Aug 26 14:22 mylib.c
-rw-rw-r--. 1 user user 304 Aug 26 14:21 mylib.h
-rw-rw-r--. 1 user user 1544 Aug 26 14:37 mylib.o
|
由上面结果可以看到,当前目录生成了一个名为libmylib.so.1软链接,而且指向libmylib.so.1.0.0
此时再执行:
1 2 3 4 5 6 7 8 9
| [root@localhost lib]# ldd main
linux-vdso.so.1 => (0x00007fff8ebff000)
libmylib.so.1 => /home/user/lib/libmylib.so.1 (0x00007fac4c78a000)
libc.so.6 => /lib64/libc.so.6 (0x00000034b0800000)
/lib64/ld-linux-x86-64.so.2 (0x00000034b0000000)
|
可以看出,main头部信息里记录是:libmylib.so.1,因此运行时就去找这个文件,于是找到了这个文件/home/user/lib/libmylib.so.1,而它是个软链接指向libmylib.so.1.0.0,于是就可以加载这个库文件了,程序可以正确运行了。
五、ldconfig有何用处?
由上面可以看出,ldconfig作用就是生成了一个以SONAME命名并且指向库文件的软链接。
如果现在我的库文件更新了,增加一些接口,依然兼容原来的版本,那么库文件名应该为:libmylib.so.1.1.0,SONAME仍然是libmylib.so.1
1 2
| gcc -c -fpic mylib.c -o mylib.o gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.1.0 mylib.o
|
1 2 3 4 5 6 7 8 9 10 11 12
| [root@localhost lib]# ll
total 40
lrwxrwxrwx. 1 root root 17 Aug 26 15:47 libmylib.so.1 -> libmylib.so.1.0.0
-rwxrwxr-x. 1 user user 5900 Aug 26 14:37 libmylib.so.1.0.0
-rwxr-xr-x. 1 root root 5900 Aug 26 15:59 libmylib.so.1.1.0
-rwxrwxr-x. 1 user user 6731 Aug 26 15:13 main ...
|
显然多了一个库文件libmylib.so.1.1.0,然后再执行:
1 2 3 4 5 6 7 8 9 10 11 12
| [root@localhost lib]# ll
total 40
lrwxrwxrwx. 1 root root 17 Aug 26 16:28 libmylib.so.1 -> libmylib.so.1.1.0
-rwxrwxr-x. 1 user user 5900 Aug 26 14:37 libmylib.so.1.0.0
-rwxr-xr-x. 1 root root 5900 Aug 26 15:59 libmylib.so.1.1.0
-rwxrwxr-x. 1 user user 6731 Aug 26 15:13 main ...
|
结果:软链接libmylib.so.1不再指向libmylib.so.1.0.0,而是指向libmylib.so.1.1.0,这是为什么呢?
显然软链接会指向一个最新的版本,程序就会加载到这个最新的库,由于相同SONAME的库高版本是向下兼容的,因此可以最大程度的兼容程序。
如果我的动态库又更新了,这次变化很大,删除了一些以前的接口,那么这次主版本号要变化了,库文件名应该为 libmylib.so.2.0.0,SONAME也变化为libmylib.so.2,而且不兼容以前的版本libmylib.so.1.x.y.
1 2 3
| gcc -c -fpic mylib.c -o mylib.o gcc -shared -Wl,-soname,libmylib.so.2 -o libmylib.so.2.0.0 mylib.o ldconfig
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| [root@localhost lib]# ll
total 48
lrwxrwxrwx. 1 root root 17 Aug 26 16:28 libmylib.so.1 -> libmylib.so.1.1.0
-rwxrwxr-x. 1 user user 5900 Aug 26 14:37 libmylib.so.1.0.0
-rwxr-xr-x. 1 root root 5900 Aug 26 15:59 libmylib.so.1.1.0
lrwxrwxrwx. 1 root root 17 Aug 26 16:41 libmylib.so.2 -> libmylib.so.2.0.0
-rwxr-xr-x. 1 root root 5900 Aug 26 16:41 libmylib.so.2.0.0
-rwxrwxr-x. 1 user user 6731 Aug 26 15:13 main ...
|
于是乎,又生成一个库文件libmylib.so.2.0.0 和一个软链接 libmylib.so.2,显然多个不同SONAME的库文件可以同时工作了,互不影响。
结论:
- ldconfig会遍历/etc/ld.so.conf 中的目录(也包括/lib,/usr/lib)下的库文件,根据读取库文件的SONAME,会在库文件所在目录,生成一个以SONAME命名的软链接并且指向这个库文件。
- 如果有多个SONAME相同的库文件,那么这个软链接只会指向一个最新版本的库文件,即次版本号最大的库,若次版本号也相同,会指向发布版本号最大的库文件。
- ldconfig还会生成缓存记录在/etc/ld.so.cache中,程序寻找库,会首先找这个cache文件。
关于linker name:
我们在链接库的时候,比如 gcc main.c -o main -L. -lmylib,只需要使用库的名字即可,如何才能做到这样子呢?
那就必须手动创建软链接:
1
| ln -s libmylib.so.1 libmylib.so
|
生成软链接libmylib.so指向libmylib.so.1,libmylib.so.1 也是一个软链接。
编译参数 -L. -lmylib 表示在当前目录寻找libmylib.so文件,libmylib.so -> libmylib.so.1 -> libmylib.so.1.1.0 最终会链接到具体的库文件。当然 libmylib.so 也可以指向 libmylib.so.1,这个需要根据实际情况,选择库文件的版本。
六、解决上篇博客的留下的问题
通过以上的分析,已经清楚了动态库的加载细节,可以解决哪个问题了。
问题:
1
| gcc main.c -o main /home/user/lib/libcurl.so.4.3.0 -Wl,-rpath=/home/user/lib
|
这种可以编译通过,但是程序无法运行。
解决方法:
方法一、在进入/home/user/lib目录下,建立SONAME软链接
1
| ln -s libcurl.so.4.3.0 libcurl.so.4
|
方法二、把libcurl.so.4.3.0文件改名为libcurl.so.4,如果使用的库不更新的话,可以使用这种方法。
上面讲了那么多,下面做一个要点总结:
1、要解决不同版本库依赖问题,要遵守上述的库版本号命名约定。
2、如何创建带有SONAME的动态库。
3、编译器在编译链接动态库时,会先读取动态库的SONAME,并把SONAME记录在可执行文件的头部(还有一种特殊情况,假入动态库没有SONAME,例如libmylib.so,那么编译器会把动态库的文件名添加到可执行那个文件的头部,其实就是real name, 其实种情况,可以理解为real name,Soname,linker name 三者相同)
4、程序运行时,会读取自己头部记录的所依赖的库文件名字,会根据这个名字去寻找同名的文件(去哪里寻找,前面已经总结),找到该文件后加载它,显然大多时候,是找到一个软链接,该软连接指向具体的库文件,这样便可以加载成功。
5、关于ldconfig的作用,这里不再陈述。
由于笔者的水平有限,出错在所难免,恳请读者拍砖指正,谢谢阅读。