Linux平台下的动态链接库 A brief Introduction of .so.
问题描述
项目依赖于一些第三方编译好的的.so文件,但为了方便打包我们将项目依赖的所有.so以统一格式进行命名,因此不可避免需要重命名某些.so,在项目重新编译后,编译通过,但在运行时,将抛出无法链接到某些库的错误,而且报错的库正是重命名过的库。
Copy ts@ts-OptiPlex-7070:~/workSpace/AIDemo/thirdparty/iniparser/test/build$ ldd iniparser-test
linux-vdso.so.1 => (0x00007ffc52dee000)
libiniparser.so => /home/ts/workSpace/AIDemo/thirdparty/iniparser/test/../lib/libiniparser.so (0x00007f4572b59000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f457278f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4572d5f000)
未重命名前使用ldd
查看可执行文件iniparser-test
可以看到其正确链接到了所有依赖的动态库,这时我将libiniparser.so
重命名为libiniparse.so
并且修改CMakeLists.txt
中的link命令:
Copy target_link_libraries(iniparser-test iniparse)
编译将成功通过,只是再用ldd
查看iniparser-test
可以发现其仍然依赖libiniparser.so
,并且由于重命名导致该库not found
:
Copy ts@ts-OptiPlex-7070:~/workSpace/AIDemo/thirdparty/iniparser/test/build$ ldd iniparser-test
linux-vdso.so.1 => (0x00007ffd0b3fd000)
libiniparser.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff9ab45a000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff9ab824000)
打开verbose选项查看make输出可以看到,编译时的-l
选项并没有出错,因此可以确定iniparser-test
的编译并没有出错:
Copy /usr/bin/cc CMakeFiles/iniparser-test.dir/main.c.o -o iniparser-test -L/home/ts/workSpace/AIDemo/thirdparty/iniparser/test/../lib -Wl,-rpath,/home/ts/workSpace/AIDemo/thirdparty/iniparser/test/../lib -liniparse
这时除了重新编译libiniparser.so
,并在编译该库时将库名改成libiniparse.so
能够让iniparser-test
正确查找到依赖,并没有找到其他解决方案。
查资料的过程中有看到一个2010年的帖子提到在Windows平台下修改.dll文件名的同时需要修改.dll内部的相关信息,因此猜测编译出的.so内部存在某个固定字段用于链接校验。
iniparser.so
是使用CMake构建的普通library目标,CMake对于normal library 并没有太多选项,但是查看编译信息时发现在生成库文件的时候编译命令为:
Copy [ 25%] Linking C shared library ../lib/libiniparser.so
/usr/bin/x86_64-linux-gnu-gcc-5 -fPIC -g -shared -Wl,-soname,libiniparser.so -o ../lib/libiniparser.so CMakeFiles/iniparser.dir/src/dictionary.c.o CMakeFiles/iniparser.dir/src/iniparser.c.o CMakeFiles/iniparser.dir/src/ini_conf.c.o
其中有个选项为-shared -Wl,-soname libiniparser.so
,已知-o
用于产生输出,那么为何会有一个-soname
选项并且值与输出的.so文件名相同,于是就查找了-soname
相关信息。
shared object
在linux下使用一些库时,会发现其后面带有一些数字,例如:libc.so.1
。形如lib<name>.so.x.y.z
是有一套命名规则,x
表示major version
,y
表示minor version
,z
表示release version
。
Linux平台下的.so有三种名字:
real name
:其命名规则为lib<name>.so.x.y.z
,在它的开头,包含有soname
信息。程序运行时真正调用的.so,也就是里面是真正含有代码的,也即.so的文件名。
soname(short name of shared object)
:其命名规则为lib<name>.so.x
,应用程序在链接时,所找到的库。它的信息是写在real name
中,在编译时,从real name
中读取出soname
,写入应用程序中 ,应用程序在运行时链接阶段再通过soname
找到real name
。
link name
:其命名规则为lib<name>.so
就是我们在链接时,所使用的名字,比如-lc
,这样,编译器就会去寻找libc.so.x.y.z
,如果有多个,编译器去寻找最新的。当然,用户也可以直接指定全名,比如,lib*.so.1.1.2
,这个link name
其实是一个虚拟的,如果存在real name
或soname
,那这个link name
其实是不存在的。link name
使得用户不用去记住那些数字编号了,直接利用前面的名字就可以找到所使用的.so。编译器其实是根据link name
去找到real name
,然后提取出soname
的。
引入这套规则的目的是为了提供兼容性标准——当升级系统中的一个库时,在soname
不变的情况下,不会影响依赖于旧库的程序运行,同时应用程序通过使用soname
,来指定所希望库的版本,库作者可以通过保留或改变soname
来声明哪些版本是兼容的,这使得程序员摆脱了共享库版本冲突问题的困扰。
对于库的用户来说,可见的只有link name
,在非大版本更新(即此次更新兼容旧版本)的情况下,更改的只有real name
所表示的真实库文件,这时候库的开发人员不应该修改库的soname
,因此用户依赖于旧版本库的应用程序依然可以正常运行,假如没有soname
,那么每次库版本更新,用户都需要重新编译应用程序。
ldd
命令查看到的是程序所依赖的soname
列表。
使用readelf
可以查看elf文件信息:
Copy ts@ts-OptiPlex-7070:~/workSpace/AIDemo/thirdparty/iniparser/build$ readelf -d ../lib/libiniparser.so
Dynamic section at offset 0x4e00 contains 25 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000e (SONAME) Library soname: [libiniparser.so]
0x000000000000000c (INIT) 0x14d0
0x000000000000000d (FINI) 0x3ff8
0x0000000000000019 (INIT_ARRAY) 0x204de8
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x204df0
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x1f0
0x0000000000000005 (STRTAB) 0xa58
0x0000000000000006 (SYMTAB) 0x350
0x000000000000000a (STRSZ) 992 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x205000
0x0000000000000002 (PLTRELSZ) 1200 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x1020
0x0000000000000007 (RELA) 0xf30
0x0000000000000008 (RELASZ) 240 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0xed0
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0xe38
0x000000006ffffff9 (RELACOUNT) 4
0x0000000000000000 (NULL) 0x0
可以看到libiniparser.so
文件的SONAME
为libiniparse.so
,而仅修改.so的文件名时,该.so的SONAME
是不会发生变化的。
solution
在无法重新编译库的情况下一种解决方法是使用patchelf 工具来修改elf文件信息:
Copy # patchelf --set-soname <modify_name> <so_path>
patchelf --set-soname libiniparse.so libiniparser.so
也可以创建以soname
命名的软连接,通过link name
来进行链接。
CMake的add_library()
CMake编译动态库的默认soname
就是add_library()
的target_name
,而且并没有查找到相关的用于设置soname
的参数,可以通过先禁用CMake默认的soname
再设置自己的编译链接选项来达到设置soname
的效果:
Copy set_target_properties(iniparser PROPERTIES NO_SONAME ON LINK_OPTIONS "-Wl,-soname,iniparse.so")
这种方式略显繁琐,甚至不如使用patchelf
更加直接,大概是因为Linux平台下.so的命名有固定规则,所以事实上CMake对于.so的编译也只提供了VERSION
和SOVERSION
两个选项:
Copy set_target_properties(iniparser PROPERTIES VERSION 1.0.0 SOVERSION 1)
这两个参数用于指定构建版本和API版本,当只指定其中一个时,CMake默认两个参数具有相同值。
Copy drwxrwxr-x 2 ts ts 4096 Aug 19 19:16 ./
drwxrwxr-x 7 ts ts 4096 Jul 26 10:08 ../
lrwxrwxrwx 1 ts ts 17 Aug 19 19:16 libiniparser.so -> libiniparser.so.1*
lrwxrwxrwx 1 ts ts 21 Aug 19 19:16 libiniparser.so.1 -> libiniparser.so.1.0.0*
-rwxrwxr-x 1 ts ts 41320 Aug 19 19:16 libiniparser.so.1.0.0*
由此可见真实的库文件为libiniparser.so.1.0.0
,即real name
;同时创建了两个软连接,其中一个是soname
命名的,另外一个是更符合日常使用的target_name
,使用readelf
查看库的soname
可以发现被命名为了带SOVERSION
的libiniparser.so.1
。
Copy ts@ts-OptiPlex-7070:~/workSpace/AIDemo/thirdparty/iniparser/build$ readelf -d ../lib/libiniparser.so
Dynamic section at offset 0x4e00 contains 25 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000e (SONAME) Library soname: [libiniparser.so.1]
0x000000000000000c (INIT) 0x14d0
0x000000000000000d (FINI) 0x3ff8
0x0000000000000019 (INIT_ARRAY) 0x204de8
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x204df0
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x1f0
0x0000000000000005 (STRTAB) 0xa58
0x0000000000000006 (SYMTAB) 0x350
0x000000000000000a (STRSZ) 994 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x205000
0x0000000000000002 (PLTRELSZ) 1200 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x1020
0x0000000000000007 (RELA) 0xf30
0x0000000000000008 (RELASZ) 240 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0xed0
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0xe3a
0x000000006ffffff9 (RELACOUNT) 4
0x0000000000000000 (NULL) 0x0