Linux 下链接编译和Windows 绝对有很大的区别。

比如应用程序如何加载一个so?为什么我当前目录下有so 加载不上能?怎么着我的应用程序就加载到别的路径的so了呢?

我们从加载讨论起:

影响加载的东西:

首先是ELF 中的变量 RPATH 和 RUNPATH

然后是环境变量 LD_PRELOAD 和 LD_LIBRARY_PATH

LD_PRELOAD 属于强制加载,有点Injection的感觉,这里暂时不讨论,LD_PRELOAD 似乎总是最强势的……

 

我们从一个应用程序 a 链接一个 lib_b.so 开始

下面是 a b 的代码以及编译链接过程

a.c:

复制代码
#include <stdio.h>

void test_a()
{
    test_b();
}

int main()
{
    test_a();
    return 0;
}
复制代码

 b.c:

#include <stdio.h>

void test_b()
{
}

编译:

[root@vm153 01]# gcc b.c -shared -fPIC -o lib/lib_b.so
[root@vm153 01]# gcc -o a a.c -Llib -l_b

因为lib_b.so 在 lib 目录下 所以需要 -Llib

运行结果如下:

[root@vm153 01]# ./a
./a: error while loading shared libraries: lib_b.so: cannot open shared object file: No such file or directory

失败,解决方案如下:

  1. 把 lib_b.so 挪到 /lib 或 /usr/lib 下面,但是这个不太靠谱,毕竟是系统的目录
  2. 使用LD_LIBRARY_PATH 把当前 lib 目录加进来,执行OK
    [root@vm153 01]# LD_LIBRARY_PATH=lib ./a
  3. 重新编译a 加入RPATH,执行 OK,注意用readelf 可以查看RPATH (原来-Wl,-rpath= 的作用就是在目标文件里加入了RPATH)
    [root@vm153 01]# gcc -o a a.c -Llib -l_b -Wl,-rpath=lib
    [root@vm153 01]# ./a
    [root@vm153 01]# readelf -d a | grep PATH
     0x0000000f (RPATH)                      Library rpath: [lib]
    [root@vm153 01]#

 

那么LD_LIBRARY_PATH RPATH 同时提供的情况下,结果会如何呢?

我们要做一个简单的实验,我们尝试提供两个lib_b.so 并且在test_b代码中给出打印。

b1.c 和 b2.c 代码一致 如下:

#include <stdio.h>

void test_b()
{
    printf("test_b in: %s\n", __FILE__);
}

编译过程如下:

[root@localhost 02]# gcc b1.c -shared -fPIC -o lib1/lib_b.so
[root@localhost 02]# gcc b2.c -shared -fPIC -o lib2/lib_b.so
[root@localhost 02]# gcc -o a a.c -Llib1 -l_b -Wl,-rpath=lib1

显然 a 的 RPATH 已经被设置成了 lib1 , 然后我们使用 LD_LIBRARY_PATH 设置为lib2 来看看 a究竟加载那个 lib_b

[root@localhost 02]# LD_LIBRARY_PATH=lib2 ./a
test_b in: b1.c
[root@localhost 02]#

很明显是RPATH 起了作用。其实没有必要加入打印,用ldd 一样可以验证具体加载了哪个 so

[root@localhost 02]# LD_LIBRARY_PATH=lib2 ldd a
        linux-vdso.so.1 =>  (0x00007fffc51c8000)
        lib_b.so => lib1/lib_b.so (0x00007f5b6d268000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f5b6cebe000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f5b6d46a000)
[root@localhost 02]#

 

加上RUNPATH 吧!

再复杂一点的情况, RUNPATH 我们用的不太多,但是有的系统上 -Wl,-rpath 会一起加上RUNPATH,重新编译a 使用--enable-new-dtags 加入RUNPATH

复制代码
[root@localhost 02]# gcc -o a a.c -Llib1 -l_b -Wl,-rpath=lib1,--enable-new-dtags
[root@localhost 02]# readelf -d a | grep PATH
 0x000000000000000f (RPATH)              Library rpath: [lib1]
 0x000000000000001d (RUNPATH)            Library runpath: [lib1]
[root@localhost 02]# LD_LIBRARY_PATH=lib2 ldd a
        linux-vdso.so.1 =>  (0x00007fffa4591000)
        lib_b.so => lib2/lib_b.so (0x00007f8779855000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f87794ab000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f8779a57000)
[root@localhost 02]#
复制代码

恩~~ 这个有点困惑了。 其实逻辑是这样的:加载器首先看有没有 RUNPATH, 如果有那么就尝试从LD_LIBRARY_PATH 加载,如果还是失败再从RUNPATH加载。当然最后如果还是不行,会尝试 /lib 和 /usr/lib。

这个RUNPATH 真的有点二,设置了一个值,但是不会马上启用,而且还干掉了 RPATH 成全了LD_LIBRARY_PATH……

 

如何创建只有RUNPATH 的 ELF 文件?

按照上面的推论,当RPATH 和 RUNPATH 同时存在的话,那么RPATH 实际就没有用了(这点逻辑应该能理解吧),可是ld 链接出来的要么没有 RPATH 和 RUNPATH,要么只有RPATH, 要么两个都有(我想可能是为了兼容一下老版本的loader 吧)。

简单的方案,利用第三方工具 patchelf (http://nixos.org/patchelf.html)。先编译出一个没有RPATH RUNPATH的 ELF 文件,然后 用 patchelf --set-rpath 来添加,这样就只会添加 RNPATH。当然这个工具还有好些个其他选项……

 

小结一下一个ELF 文件自身加载 so的情况:

其实这三者的关系概括起来没有几点:

  1. LD_LIBRARY_PATH 是个环境变量 可以用来指定加载so 的路径,并且优先级高于系统默认的。
  2. RPATH 是 ELF 格式里面的一个数据,他的优先级比 LD_LIBRARY_PATH 还要高
  3. 但是有一个很二 的 RUNPATH,也是 ELF 格式中的,如果他出现了 RPATH 就躲起来了,LD_LIBRARY_PATH 又成了首选

整理出了一张表,供参考

ELF 中 RPATHELF 中 RUNPATHLD_LIBRARY_PATH 变量尝试加载目录的顺序
未设置未设置未设置/lib => /usr/lib 
未设置未设置设置${LD_LIBRARY_PATH} => /lib => /usr/lib
设置未设置未设置${RPATH} => /lib => /usr/lib
设置未设置设置${RPATH} => ${LD_LIBRARY_PATH} => /lib => /usr/lib
设置 或 未设置设置 设置 ${LD_LIBRARY_PATH} => ${RUN_PATH} =>  /lib => /usr/lib
设置 或 未设置设置 未设置 ${RUN_PATH} =>  /lib => /usr/lib 

 

 

 

 

 

 

Logo

更多推荐