Linux平台下的动态链接库

A brief Introduction of .so.

问题描述

项目依赖于一些第三方编译好的的.so文件,但为了方便打包我们将项目依赖的所有.so以统一格式进行命名,因此不可避免需要重命名某些.so,在项目重新编译后,编译通过,但在运行时,将抛出无法链接到某些库的错误,而且报错的库正是重命名过的库。

 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命令:

 target_link_libraries(iniparser-test iniparse)

编译将成功通过,只是再用ldd查看iniparser-test可以发现其仍然依赖libiniparser.so,并且由于重命名导致该库not found

 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的编译并没有出错:

 /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并没有太多选项,但是查看编译信息时发现在生成库文件的时候编译命令为:

其中有个选项为-shared -Wl,-soname libiniparser.so,已知-o用于产生输出,那么为何会有一个-soname选项并且值与输出的.so文件名相同,于是就查找了-soname相关信息。

shared object

在linux下使用一些库时,会发现其后面带有一些数字,例如:libc.so.1。形如lib<name>.so.x.y.z是有一套命名规则,x表示major versiony表示minor versionz表示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 namesoname,那这个link name其实是不存在的。link name使得用户不用去记住那些数字编号了,直接利用前面的名字就可以找到所使用的.so。编译器其实是根据link name去找到real name,然后提取出soname的。

引入这套规则的目的是为了提供兼容性标准——当升级系统中的一个库时,在soname不变的情况下,不会影响依赖于旧库的程序运行,同时应用程序通过使用soname,来指定所希望库的版本,库作者可以通过保留或改变soname来声明哪些版本是兼容的,这使得程序员摆脱了共享库版本冲突问题的困扰。

对于库的用户来说,可见的只有link name,在非大版本更新(即此次更新兼容旧版本)的情况下,更改的只有real name所表示的真实库文件,这时候库的开发人员不应该修改库的soname,因此用户依赖于旧版本库的应用程序依然可以正常运行,假如没有soname,那么每次库版本更新,用户都需要重新编译应用程序。

ldd命令查看到的是程序所依赖的soname列表。

使用readelf可以查看elf文件信息:

可以看到libiniparser.so文件的SONAMElibiniparse.so,而仅修改.so的文件名时,该.so的SONAME是不会发生变化的。

solution

  • 在无法重新编译库的情况下一种解决方法是使用patchelf工具来修改elf文件信息:

  • 也可以创建以soname命名的软连接,通过link name来进行链接。

CMake的add_library()

CMake编译动态库的默认soname就是add_library()target_name,而且并没有查找到相关的用于设置soname的参数,可以通过先禁用CMake默认的soname再设置自己的编译链接选项来达到设置soname 的效果:

这种方式略显繁琐,甚至不如使用patchelf更加直接,大概是因为Linux平台下.so的命名有固定规则,所以事实上CMake对于.so的编译也只提供了VERSIONSOVERSION两个选项:

这两个参数用于指定构建版本和API版本,当只指定其中一个时,CMake默认两个参数具有相同值。

由此可见真实的库文件为libiniparser.so.1.0.0,即real name;同时创建了两个软连接,其中一个是soname命名的,另外一个是更符合日常使用的target_name,使用readelf查看库的soname可以发现被命名为了带SOVERSIONlibiniparser.so.1

Last updated

Was this helpful?