linux系统驱动开发之-patchelf修改动态库链接器的方法
1. patchelf 命令熟悉
- 这篇博客中我描述了使用
patchelf
来修改动态库链接器
的方法。列举下 patchelf 的功能,并介绍另外一个实际的应用。
1.1 安装
$ sudo apt install -y patchelf chrpath
1.2 运行
- 运行 patchelf -h 能够得到如下信息:
$ patchelf -h
syntax: patchelf
[--set-interpreter FILENAME] // 设置动态库解析器
[--page-size SIZE] // 设置页大小
[--print-interpreter]
[--print-soname] // Prints 'DT_SONAME' entry of .dynamic section. Raises an error if DT_SONAME doesn't exist
[--set-soname SONAME] // 设置名字
[--set-rpath RPATH] // 设置 rpath
[--remove-rpath] // 删除 rpath
[--shrink-rpath] // 收缩rpath
[--allowed-rpath-prefixes PREFIXES] // 添加允许的 rpath 前缀
[--print-rpath] // 打印 rpath
[--force-rpath] // 强制使用 rpath
[--add-needed LIBRARY] // 添加需要的动态库
[--remove-needed LIBRARY] // 删除需要的动态库
[--replace-needed LIBRARY NEW_LIBRARY] // 替换需要的动态库
[--print-needed] // 打印帮助信息
[--no-default-lib] // 不链接默认的动态库
[--debug] // 输出调试信息
[--version] // 版本号
- 从上面的功能描述中可以看到,patchelf 的主要功能与动态库解析器、rpath、动态库本身相关,可能在解决一些动态库链接程序执行的问题时能够用到。
1.3 应用-使用自定义的动态库目录
patchelf
修改 rpath
以使用自己目录
中的动态库,man ld.so
的翻译 这篇文章中翻译了 ld.so 动态库链接器执行的过程,其中查找动态库的步骤如下:
- 针对 ELF 格式文件,当
DT_RUNPATH
属性不存在的情况下,使用二进制程序dynamic section
中存在的DT_RPATH
属性指定的路径来搜索 。DT_RPATH 已经被弃用。
- 使用环境变量
LD_LIBRARY_PATH
中指定的路径来搜索。(除非可执行文件正在安全执行模式下运行),在这种情况下,它将被忽略。
- 使用二进制文件(如果存在)的
DT_RUNPATH
动态部分属性中指定的目录。搜索此类目录只是为了找到DT_NEEDED(direct dependencies)
条目所需的对象,而不适用于这些对象的子对象,这些子对象本身必须有自己的DT_RUNPATH
运行路径条目。这与DT_RUNPATH
不同,DT_RUNPATH
用于搜索依赖关系树中的所有子集 - 从缓存文件
/etc/ld.so.cache
中查找。如果程序在链接时使用了 -z nodeflib 选项,默认库路径中的库及那个会被跳过。安装到硬件兼容目录中的库将会比其它库优先查找。 - 在默认的
/lib
然后时/usr/lib
中寻找,如果程序在链接时使用了 -z nodeflib 选项,这一步将被跳过 - 可以看到在搜索 LD_LIBRARY_PATH 之前会先以 ELF 文件中存在的 DT_RPATH 属性中指定的路径来搜索动态库,看上去这个问题就出在这里。
1.4 查看rpath信息
- 运行
readelf -a gpsdk
搜索与rpath
相关的内容,如果设定了DT_RUNPATH
这个变量的值,并且指向默认路径,这将导致LD_LIBRARY_PATH
不能生效。
1.5 设置rpath信息
- 通过
patchelf
来实现。运行如下命令,将rpath
的只修改为自定义的动态库目录就解决了这个问题。
patchelf --set-rpath '/home/gpsdk/lib/:/home/gpsdk/threads/lib/' app
2. 同名动态库修改应用
- 在
Linux
应用的开发过程中,在进行多部门合作开发是,大家都会使用第三方库,经常会出现同一个库,不同的版本产生冲突。因为动态库完全一样的名字,这该如何是好? - 具体来演示问题:在编译可执行程序的时候,通过
gcc
编译参数的-lXXX
就可以动态链接一个动态库。但是,现在想链接两个动态库,它们的名字是一样的!比如gpAlgo连接xxx.so最终将自己封装成了gpAlgo.so,但是gpApp也需要使用到xxx.so;
2.1 第一个动态库文件
- 现在,假设我们在开发一个应用程序,需要用到第一个动态库,代码如下。
// 第一个动态库 源文件 mymath.c:
double func0(double arg)
{
double ret = arg + arg;
return ret;
}
double func1(double arg1, double arg2)
{
double ret = arg1 + arg2;
return ret;
}
- 动态库的编译命令是:
$ gcc -m32 -fPIC --shared -o libmymath.so -Wl,--soname,libmymath.so mymath.c
- 以上这些属性都比较常见,请注意其中的
-Wl,--soname,libmymath.so
,它用来指定生成的动态库的 SONAME
,一般用于动态库的版本管理中。执行了gcc
指令之后,就得到了一个动态库文件:libmymath.so
。
- 可以通过
patchelf
这个工具(在Ubuntu
系统中,可以通过apt-get
直接安装),来查看一下这个动态库文件的SONAME
:
$ patchelf --print-soname libmymath.so
libmymath.so // SONAME
- 第2行打印出来的就是所谓的
SONAME
。你也可以测试一下,指定其他的SONAME
,例如:
$ gcc -m32 -fPIC --shared -o libmymath.so -Wl,--soname,libmymath-1.2.3.so mymath.c
$ patchelf --print-soname libmymath.so
libRobotMath-1.2.3.so // SONAME
2.2 应用程序
// 可执行程序 源文件: main.c
extern double func0(double arg);
extern double func1(double arg1, double arg2);
int main(int argc, char *agv[])
{
double arg = 1.1;
double result0 = func0(arg);
printf("result0 = %lf \n", result0);
double arg1 = 1.1, arg2 = 2.2;
double result1 = func1(arg1, arg2);
printf("result1 = %lf \n", result1);
return 0;
}
- 直接编译(假设已经把动态库复制到
main.c
同一个文件夹中了):
$ gcc -m32 -o main main.c -lmymath -L./ -Wl,-rpath=./
- 执行:
$ ./main
result0 = 2.200000
result1 = 3.300000
- 完美!
2.3 第二个动态库文件
问题来了:现在应用程序还需要实现另外一个复杂的算法,本着偷懒的精神。
// 第二个动态库 源文件 mymath.c:
double func2(double arg1, double arg2, double arg3)
{
double ret = arg1 * arg2 * arg3;
return ret;
}
// 编译指令
$ gcc -m32 -fPIC --shared -o libmymath.so -Wl,--soname,libmymath.so mymath.c
-
但是坑爹的是,这个算法库输出的动态库名称居然第一个相同,也是
libmymath.so
!假如: 名字叫 libmyUltra.so,那么只需要直接复制过来,然后在编译执行程序时,直接链接 -lmyUltra
就可以了。 -
错误做法:直接给它改名
既然如此,我们是否可以直接给它改名呢?尝试一下:
$ mv libmymath.so libmymath2.so
然后把libmymath2.so
复制到应用程序的目录下,并在main.c
中,调用这个库中的函数 func2。
extern double func2(double arg1, double arg2, double arg3);
int main(int argc, char *agv[])
{
// 之前的其它代码
// ...
double arg3 = 1.1, arg4 = 2.2, arg5 = 3.3;
double result2 = func2(arg3, arg4, arg5);
printf("result2 = %lf \n", result2);
return 0;
}
- 编译一下试试:
$ gcc -m32 -o main main.c -lmymath -lmymath2 -L./ -Wl,-rpath=./
/tmp/ccDGqFkl.o: In function `main':
main.c:(.text+0xb4): undefined reference to `func2'
collect2: error: ld returned 1 exit status
- 报错:找不到 func2 这个函数,但是
libmymath2.so
这个库中明明已经有这个函数啊,不信你看:
$ readelf -s libmymath2.so | grep func2
8: 0000052a 69 FUNC GLOBAL DEFAULT 11 func2
51: 0000052a 69 FUNC GLOBAL DEFAULT 11 func2
- 为啥 gcc 还找不到呢?看来,很粗鲁地直接给第二个动态库文件强行改名,不是解决问题的正确思路!
2.4 使用patchelf 工具修改
- 还记得在第一个库中,我们使用
patchelf
这个小工具来查看动态库的SONAME
吗?继续用它来查看下被我们改名后的libmymath2.so
:
$ patchelf --print-soname libmymath2.so
libmymath.so
SONAME
依然是原来的名称,说明通过mv
指令改名,只是改变了外表,并没有改变它的内心。如果你熟悉文件系统
,就会知道:mv
指令只是修改了库文件在inode
节点中的名字,而库文件实际内容所存储的block
存储空间中,一点都没有变化。- 动态库是一个
ELF格式的文件
,操作系统在加载动态库的时候,是根据ELF格式的标准,对文件的内容进行一层一层解析的。
可以参考很久之前写的一篇文章:Linux系统中编译、链接的基石-ELF文件:扒开它的层层外衣,从字节码的粒度来探索。
patchelf
这个工具,就提供了这样的功能:查看
或修改动态库文件
的内部信息,包括:SONAME
,依赖的其他动态库
,rpath
路径信息等等。我们可以使用--set-soname
这个参数,来把它的SONAME
修改一下:
$ patchelf --set-soname libmymath2.so libmymath2.so
第一个 libmymath2.so,是设置的 SONAME 名称;
第二个 libmymath2.so,是指定修改哪一个动态库文件的 SONAME;
- 修改之后,再检查一下是否修改正确了:
$ patchelf --print-soname libmymath2.so
libmymath2.so
Bingo!SONAME 已经被正确修改了。
- 再次编译一下可执行程序:
$ gcc -m32 -o main main.c -lmymath -lmymath2 -L./ -Wl,-rpath=./
- 没有报错!执行一下:
$ ./main
result0 = 2.200000
result1 = 3.300000
result2 = 7.986000
- 问题解决了!
- 记得开发一个网关,在硬件出来之前需要在
Ubuntu
(x86)平台上进行模拟。为了便于跨平台,选择了glib
库,但是对其中的小部分源码进行了二次开发。但是Ubuntu
的桌面系统是基于GTK
的(底层使用的就是glib库),也就是说操作系统在启动时已经加载了系统目录下的glib
库。那么我们的应用程序在编译时,的确可以链接到自己二次开发的glib库(放在本地文件夹),但是在执行时,一直加载不成功,就是因为动态库的名字冲突问题导致的。最后没办法,只好利用patchelf
工具,对动态库的名称,包括SONAME
进行改写,这样才解决问题。