不同版本的同一动态库(so文件)可能会不兼容。如果程序在编译时指定动态库是某个低版本,运行是用的一个高版本,可能会导致无法运行,如果系统内存在同一个so文件的多个版本,在运行时也可能产生错误。
Linux上对动态库的命名采用libxxx.so.a.b.c的格式,其中a代表大版本号,b代表小版本号,c代表更小的版本号。
以Linux自带的cp程序为例,通过ldd查看其依赖的动态库:
$ ldd /bin/cp
linux-vdso.so.1 => (0x00007ffff59df000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007fb3357e0000)
librt.so.1 => /lib64/librt.so.1 (0x00007fb3355d7000)
libacl.so.1 => /lib64/libacl.so.1 (0x00007fb3353cf000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007fb3351ca000)
libc.so.6 => /lib64/libc.so.6 (0x00007fb334e35000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007fb334c31000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb335a0d000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb334a14000)
左边是依赖的动态库名字,右边是链接指向的文件,再查看libacl.so相关的动态库
$ ll /lib64/libacl.so*
lrwxrwxrwx. 1 root root 15 1月 7 2015 /lib64/libacl.so.1 -> libacl.so.1.1.0
-rwxr-xr-x. 1 root root 31280 12月 8 2011 /lib64/libacl.so.1.1.0
我们发现libacl.so.1实际上是一个软链接,它指向的文件是libacl.so.1.1.0,命名方式符合上面的描述。当然,也有不按这种方式命名的,比如
$ ll /lib64/libc.so*
lrwxrwxrwx 1 root root 12 8月 12 14:18 /lib64/libc.so.6 -> libc-2.12.so
不管怎样命名,只要按照规定的方式来生成和使用动态库,就不会有问题。而且我们往往是在机器A上编译程序,在机器B上运行程序,编译和运行的环境其实是有略微不同的(有意和无意的)。
下面就说说动态库在生成和使用过程中的一些问题。
动态库的编译
我们以一个简单的程序作为例:
// filename:hello.c
#include <stdio.h>
void hello(const char* name)
{
printf("hello %s!\n", name);
}
// filename:hello.h
void hello(const char* name);
采用如下命令进行编译:
gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.0 -o libhello.so.0.0.1
需要注意的参数是 -Wl,-soname,libhello.so.0(中间没有空格),-Wl选项告诉编译器将后面的参数传递给链接器,-soname则指定了动态库的soname(简单共享名,Short for shared object name),现在我们生成了libhello.so.0.0.1。
运行 ldconfig -n . 命令,在当前目录创建一个软连接libhello.so.0
$ ll libhello.so.0
lrwxrwxrwx 1 handy handy 17 8月 17 14:18 libhello.so.0 -> libhello.so.0.0.1
个软链接是如何生成的呢,并不是截取libhello.so.0.0.1名字的前面部分,而是根据libhello.so.0.0.1编译时指定的-soname生成的。也就是说我们在编译动态库时通过-soname指定的名字,已经记载到了动态库的二进制数据里面。不管程序是否按libxxx.so.a.b.c格式命名,但Linux上几乎所有动态库在编译时都指定了-soname,我们可以通过readelf工具查看soname,比如文章开头列举的两个动态库:
$ readelf -d /lib64/libacl.so.1.1.0
Dynamic section at offset 0x6de8 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libattr.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000e (SONAME) Library soname: [libacl.so.1]
这里省略了一部分,可以看到最后一行SONAME为libacl.so.1,所以/lib64才会有一个这样的软连接
也可以查看libhello.so.0.0.1这个文件里面的soname字段。
再看libc-2.12.so文件,该文件并没有采用我们说的命名方式:
$ readelf -d /lib64/libc-2.12.so
Dynamic section at offset 0x18db40 contains 27 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2]
0x000000000000000e (SONAME) Library soname: [libc.so.6]
样可以看到最后一行SONAME为libc.so.6,即便该动态库没有按版本号的方式命名,但仍旧有一个软链指向该动态库,而该软链的名字就是soname指定的名字。
所以关键就是这个soname,它相当于一个中间者,当我们的动态库只是升级一个小版本时,我们可以让它的soname相同,而可执行程序只认soname指定的动态库,这样依赖这个动态库的可执行程序不需重新编译就能使用新版动态库的特性。值得注意的是,如果我们不更改动态库的 SONAME
,只更改动态库文件的文件名,然后指定给链接器更改文件名后的动态库文件,那么在运行时,二进制文件可能会报错:找不到指定的库(因为soname对应的名字实际是软连接连到了改名前的动态库文件)。
我们再写一个测试文件:
#include "hello.h"
int main(int argc, char** argv)
{
hello("jsc");
return 0;
}
因为我们在gcc编译时使用-l只认库名(如libhello.so不认libhello.so.x),所以我们先给libhello.so.0建立一个软链接:
ln -s libhello.so.0 libhello.so
执行:
gcc main.c -L. -lhello
得到a.out可执行文件。
运行a.out发现无法找到libhello.so这个文件,使用ldd a.out也会发现找不到这个文件。但是这个文件明明就在本目录下,为啥还找不到呢?
这是因为gcc编译链接动态库时,很有可能编译通过,但是执行时,找不到动态链接库,那是因为-L选项指定的路径只在编译时有效,编译出来的可执行文件不知道-L选项后面的值,当然找不到。可以用ldd <your_execute>看看是不有 ‘not found’在你链接的库后面,解决方法是通过-Wl,rpath=<your_lib_dir>,使得execute记住链接库的位置。这样的缺点是绑定死了运行库的位置,如果这个库文件移动了位置,就可能无法运行了。
由于种种原因,Linux 下写 c 代码时要用到一些外部库(不属于标准C的库),可是由于没有权限,无法将这写库安装到系统目录,只好安装用户目录下如 /home/youname/lib,可是怎么编译才能让程序正常编译,并且正常运行呢。这样使用gcc:
gcc -I/path/to/include/dir -L/path/to/lib/dir -llibname -Wl,-rpath,/path/to/lib/dir -o test test.c
解释一下,-I ,-L ,-l 这三个经常用,分别表示编译时include目录,库目录和所用的库,而-Wl,-rpath,是什么呢,它就是指定编译好的程序在运行时动态库的目录(可以 man gcc 搜索 -Wl查看),当编译好程序后用 ldd 就可以看到你指定的路径了。
当然也可以不用-Wl,-rpath,而用–static 采用静态编译,这样程序在哪都能正常运行,不过代价是程序要大很多。
还有一种方法是用LD_LIBRARY_PATH,不过很多人不推荐用这个,所以最好的方法还是用 -Wl,-rpath=/path/to/lib/dir。
执行gcc main.c -L. -Wl,-rpath=. -lhello -o test
得到可执行程序test。然后可以在本地执行。
使用readelf -d test查看该文件的属性信息,发现有一个RUNPATH指定了运行时连接的库路径为.,这样如果test换个路径,就无法运行。
如果我们把上面的编译main.c的rpath换成绝对路径:gcc main.c -L. -Wl,-rpath=/home/ok/code/sofileTest/ -lhello -o test
再查看test的信息,发现RUNPATH是一个绝对路径,这样的话,只要libhello.so不换地方,把test换到其他地方,还是能正常运行的。
其实在生成test程序的过程有如下几步:
- 链接器通过编译命令 -L. -lhello 在当前目录查找libhello.so文件
- 读取libhello.so链接指向的实际文件,这里是libhello.so.0.0.1
- 读取libhello.so.0.0.1中的SONAME,这里是libhello.so.0
- 将libhello.so.0记录到test程序的二进制数据里
- 也就是说libhello.so.0是已经存储到test程序的二进制数据里的,不管这个程序在哪里,通过ldd查看它依赖的动态库都是libhello.so.0
而为什么这里ldd查看main显示libhello.so.0为not found呢,因为ldd是从环境变量$LD_LIBRARY_PATH指定的路径里来查找文件的,我们指定环境变量再运行如下
$ export LD_LIBRARY_PATH=. && ldd main
linux-vdso.so.1 => (0x00007fff7bb63000)
libhello.so.0 => ./libhello.so.0 (0x00007f2a3fd39000)
libc.so.6 => /lib64/libc.so.6 (0x00007f2a3f997000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2a3ff3b000)
可执行程序的运行
现在测试目录结果如下这里我们把编译环境和运行环境混在一起了,不过没关系,只要我们知道其中原理,就可以将其理清楚。前面我们已经通过ldd查看了main程序依赖的动态库,并且指定了LD_LIBRARY_PATH变量,现在就可以直接运行了
$ ./test
hello jsc!
看起来很顺利。那么如果我们要部署运行环境,该怎么部署呢。显然,源代码是不需要的,我们只需要动态库和可执行程序。这里新建一个运行目录,并拷贝相关文件,目录结构如下
├── libhello.so.0.0.1
└── test
时运行会main会发现
$ ./test
./test: error while loading shared libraries: libhello.so.0: cannot open shared object file: No such file or directory
报错说libhello.so.0文件找不到,也就是说 程序运行时需要寻找的动态库文件名其实是动态库编译时指定的SONAME,这也和我们用ldd查看的一致。通过 ldconfig -n . 建立链接,如下
├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
└── test
运行程序,结果就会符合预期了。
从上面的测试看出,程序在运行时并不需要知道libxxx.so,而是需要程序本身记载的该动态库的SONAME,所以test程序的运行环境只需要以上三个文件即可
动态库版本更新
假设动态库需要做一个小小的改动,如下
// filename:hello.c
#include <stdio.h>
void hello(const char* name)
{
printf("hello %s, welcome to our world!\n", name);
}
由于改动较小,我们编译动态库时仍然指定相同的soname
gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.0 -o libhello.so.0.0.2
新的动态库拷贝到运行目录,此时运行目录结构如下
├── libhello.so.0 -> libhello.so.0.0.1
├── libhello.so.0.0.1
├── libhello.so.0.0.2
└── test
时目录下有两个版本的动态库,但libhello.so.0指向的是老本版,运行 ldconfig -n . 后我们发现,链接指向了新版本,如下
├── libhello.so.0 -> libhello.so.0.0.2
├── libhello.so.0.0.1
├── libhello.so.0.0.2
└── test
再运行程序
$ ./test
hello jsc, welcom to our world!
没有重新编译就使用上了新的动态库, wonderful!
同样,假如我们的动态库有大的改动,编译动态库时指定了新的soname,如下
gcc hello.c -fPIC -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0.0
动态库文件拷贝到运行目录,并执行 ldconfig -n .,目录结构如下
├── libhello.so.0 -> libhello.so.0.0.2
├── libhello.so.0.0.1
├── libhello.so.0.0.2
├── libhello.so.1 -> libhello.so.1.0.0
├── libhello.so.1.0.0
└── test
这时候发现,生成了新的链接libhello.so.1,而main程序还是使用的libhello.so.0,所以无法使用新版动态库的功能,需要重新编译才行
最后
在实际生产环境中,程序的编译和运行往往是分开的,但只要搞清楚这一系列过程中的原理,就不怕被动态库的版本搞晕。简单来说,按如下方式来做
- 编译动态库时指定-Wl,-soname,libxxx.so.a,设置soname为libxxx.so.a,生成实际的动态库文件libxxx.so.a.b.c
- 编译可执行程序时保证libxx.so存在,如果是软链,必须指向实际的动态库文件libxxx.so.a.b.c
- 运行可执行文件时保证libxxx.so.a.b.c文件存在,通过ldconfig生成libxxx.so.a链接指向libxxx.so.a.b.c
- 设置环境变量LD_LIBRARY_PATH,运行可执行程序
再说一下人为修改so文件名的事情。对于上述生成的libhello.so和libhello.so.0以及libhello.so.0.0.1这3个库文件及软连接。如果修改了libhello.so.0,也就是那个SONAME对应的文件,那么在使用时就会造成not found的错误。具体原因是:libhello.so是一个软链接,它连向的是libhello.so.0。所以后面一定会找libhello.so.0,因此,修改了libhello.so这个文件名没关系,但是修改了libhello.so.0就会造成无法链过去的错误。同理,修改了libhello.so.0.0.1也会造成问题。