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 进行改写,这样才解决问题。
Logo

更多推荐