由于主程序和它依赖的共享库是由不同的开发者开发的。共享库的开发者会不停地更新共享库的版本,以修正bug,增加功能或改进性能。版本多了之后,就可能会引起不兼容问题。在Windows上,这叫做“DLL HELL"。Linux下同样也会遇到相似问题,Linux对动态库采用如“libname.so.x.y.z”的命名规则来解决兼容问题。其中x代表主版本号,y代表次版本号,z代表发行号。根据约定,只要主版本号x相同,就不会引起兼容问题,即libname.so.x,Linux称之为SO-NAME。
程序会把它依赖的共享库的名字和主版本号(即SO-NAME)记录在dynamic段中。程序加载时,动态加载器会去dynamic段中寻找这些SONAME,以加载适当版本的依赖库,从而解决兼容问题。可以通过readelf -d programname 或者 ldd programname查看。
/*
* filename:hello.c
* version:1.0.0
*/
#include
void hello(void)
{
printf("HelloWorld\n");
}
/*
* filename:hello.h
* version:1.0.0
*/
#ifndef HELLO_H_
#define HELLO_H_
void hello(void);
#endif
$ gcc -shared -fPIC -Wl,-soname,libhello.so.1 -o libhello.so.1.0.0 hello.c
此时通过readelf查看新生成的目标文件,如下图所示,可以发现SONAME已经被保存到了目标文件中。
$ readelf -d libhello.so.1.0.0
Dynamic section at offset 0xf04 contains 25 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000e (SONAME) Library soname: [libhello.so.1]
......
此时当前文件夹下的文件为
$ tree
.
├── hello.c
├── hello.h
└── libhello.so.1.0.0
0 directories, 3 files
共享库创建完成后,我们需将它安装到系统中,以便其他程序能够使用它。通常有两种方法
我们这里使用的是方法2。得到如下结果。可以发现生成了一个符号链接,指向我们的共享库。动态加载时(注意不是链接时)依靠的就是这个SONAME。
$ tree
.
├── hello.c
├── hello.h
├── libhello.so.1 -> libhello.so.1.0.0
└── libhello.so.1.0.0
0 directories, 4 files
现在我们再写一个简单的代码,使用一下我们刚刚创建的共享库。
/*
* filename:test.c
* version:1.0.0
*/
#include "hello.h"
int main(void)
{
hello();
return 0;
}
然后编译链接该测试程序
$ gcc test.c -L. -lhello -o test
/usr/bin/ld: cannot find -lhello
collect2: error: ld returned 1 exit status
其中“-L. -lhello"表示在当前目录下查找libhello.so。当前文件夹下确实没有libhello.so,所以出错。这时我们需要通过ln -s 命令创建一个符号链接,让它指向我们的共享库。
$ ln -s libhello.so.1.0.0 libhello.so
$ ldd test
linux-gate.so.1 => (0xb7795000)
libhello.so.1 => not found
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75be000)
/lib/ld-linux.so.2 (0x80040000)
可以发现libhello.so.1状态是not found,所以此时如果运行程序,一定会出错。
$ ./test
./test: error while loading shared libraries: libhello.so.1:
cannot open shared object file: No such file or directory
解决方法
我们这只是测试,所以使用的是临时方法。此时,我们运行测试程序,就会输出正确结果了。
我们将libhello.so.1.0.0和test.c 拷贝到另一个文件夹中。因为程序加载动态库时会寻找符号链接libhello.so.1,所以我们需要执行一下 "ldconfig -n .",在新文件夹下生成一个符号链接。此时如果运行报错,有可能就是你把终端关了,重新开一个新终端,那么我们就需要再次执行一下刚才那个导出LD_LIBRARY_PATH命令,然后再运行程序。
/*
* filename:hello.c
* version:1.1.0
*/
#include
void hello(void)
{
printf("Minor Version Number was modified!\n");
}
然后执行
$ gcc -shared -fPIC -Wl,-soname,libhello.so.1 -o libhello.so.1.1.0 hello.c
注意:我们这里把libhello.so.1.0.0升级到了libhello.so.1.1.0。前面我们曾经提到,只要libname.x,y.z中,x不变,用户程序就不必修改,即我们这里的test.c无需修改。
再把新生成的libhello.so.1.1.0拷贝到那个模拟发布目录下。把终端切换到模拟发布目录下,执行“ldconfig -n .",重新生成符号链接。此时的该目录下的文件结构如下:
$ tree
.
├── libhello.so.1 -> libhello.so.1.1.0
├── libhello.so.1.0.0
├── libhello.so.1.1.0
└── test
0 directories, 4 files
执行程序,输出修改后的结果。
5. 更新版本库 升级主版本号
再次修改hello.c
/*
* filename:hello.c
* version:2.0.0
*/
#include
void hello(void)
{
printf("Major Version Number was modified!\n");
}
再次编译共享库
$ gcc -shared -fPIC -Wl,-soname,libhello.so.2 -o libhello.so.2.0.0 hello.c
然后把libhello.so.2.0.0拷贝到那个模拟发行目录,执行ldconfig -n .,然后再查看此时的文件结构
$ tree
.
├── libhello.so.1 -> libhello.so.1.1.0
├── libhello.so.1.0.0
├── libhello.so.1.1.0
├── libhello.so.2 -> libhello.so.2.0.0
├── libhello.so.2.0.0
└── test
0 directories, 6 files
可以发现生成了两个符号链接,但此时执行程序的话,仍然输出“Minor Version Number was modified!",这是因为我们并没有重新编译test.c,它保存的SONAME仍然是”libhello.so.1",而不是“libhello.so.2"。
本博客参考了以下两个资源,在此致敬