Linux 环境变量之 LD_PRELOAD & LD_LIBRARY_PATH)
环境变量
Linux 系统提供了多种方法来改变动态库连接器装载共享库路径的方法。通过使用此类方法,我们可以实现一些特殊的需求,如:动态库的调试、改变应用程序的行为方式等。
Linux 查看环境变量的 2 种方式:
$ env #environment的简写
$ export
下面主要描述 4 种常用的修改程序运行时环境变量的方式:
1、 LD_LIBRARY_PATH
LD_LIBRARY_PATH
可以临时改变应用程序的共享库(如:动态库)查找路径,而不会影响到系统中的其他程序。
$ echo ${LD_LIBRARY_PATH}
在 Linux 系统中,LD_LIBRARY_PATH
是一个由若干个路径组成的环境变量,每个路径之间由冒号隔开。默认情况下 LD_LIBRARY_PATH
为空。如果我们为某个进程设置了 LD_LIBRARY_PATH
,那么进程启动时,动态链接器会优先查找 LD_LIBRARY_PATH
指定的目录。
动态链接器转载或是查找共享库(如:动态库、静态库)的顺序为:
- 环境变量
LD_LIBRARY_PATH
指定的路径; - 路径缓存文件
/etc/ld.so.cache
指定的路径; - 默认共享库目录,先
/usr/lib
,然后/lib
。
举例:
$ gcc -I/home/myTest/envTest -o envTest envTest.c -L/home/myTest/envTest -lshowBytes
$ export LD_LIBRARY_PATH=/home/myTest/envTest
$ ./envTest
此时,我们检查下 envTest
的动态库依赖关系如下:
ldd envTest
linux-vdso.so.1 (0x00007ffef30dd000)
libshowBytes.so => /home/myTest/envTest/libshowBytes.so (0x00007f977735a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9777161000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9777366000)
Linux 中还有另外一种方法可以实现与 LD_LIBRARY_PATH
类似的功能,即直接运行动态链接器来启动程序,如:
# 示例中是 x86_64 环境
$ /lib64/ld-linux-x86-64.so.2 --library-path /home/myTest/envTest/ /home/myTest/envTest/envTest
或者编译的时候,采用下面的命令
gcc -I/home/myTest/envTest -o envTest envTest.c ./libshowBytes.so
这样运行也是没有问题的。同时,我们重新检查下 envTest
的动态库依赖关系如下:
ldd envTest
linux-vdso.so.1 (0x00007fff299ba000)
./libshowBytes.so (0x00007f08e37e0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f08e35e7000)
/lib64/ld-linux-x86-64.so.2 (0x00007f08e37ec000)
注意
:
- 尽管
LD_LIBRARY_PATH
对于共享库的开发和测试来说十分方便,但是不应该被滥用。随意修改LD_LIBRARY_PATH
并且将其导出至全局范围,将可能引起其他应用程序运行出现问题。 - 此外,
LD_LIBRARY_PATH
也会影响GCC 编译时查找库的路径,它里面包含的路径相当于链接时GCC的"-L"
参数。
同样,为了避免对其他运行程序和后续测试产生影响,需要及时取消该环境变量设置:
unset LD_LIBRARY_PATH
2、LD_PRELOAD
在 LD_PRELOAD
中指定的文件会在动态链接器按照固定规则搜索共享库之前装载,他比 LD_LIBRARY_PATH
所指定的目录中的共享库还要优先。无论时否依赖它们,LD_PRELOAD
中指定的共享库或目标文件都会被装载。
此外,由于全局符号介入机制的存在,LD_PRELOAD
中指定的共享库或目标文件中的全局符号就会覆盖后面加载的同名全局符号,这使得我们可以很方便的做到改写标准 C 库中的某个或某几个函数而不影响其他函数,对于程序的调试或测试非常有用。
用来做测试非常方便、简单:
export LD_PRELOAD=./libshowBytes.so
./envTest
此时,检查下动态了依赖关系如下:
ldd envTest
linux-vdso.so.1 (0x00007ffc6efad000)
./libshowBytes.so (0x00007fcee5ab8000)
libshowBytes.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcee58bf000)
/lib64/ld-linux-x86-64.so.2 (0x00007fcee5ac4000)
其中 ./libshowBytes.so (0x00007fcee5ab8000)
就是刚刚添加的共享库。
但是另外我们也发现了,尽快我们已经添加了一个./libshowBytes.so
共享库(其位置是位于libshowBytes.so
之前),但是另外的一个 libshowBytes.so
还是显示的 => not found
。
请仔细观察,并比较 LD_PRELOAD
环境变量与 LD_LIBRARY_PATH
的不同。
注意
:
- 使用
LD_PRELOAD
环境变量最好仅仅只是用于测试,因为会影响到全局符号; LD_PRELOAD
环境变量优先级要比LD_LIBRARY_PATH
更高;LD_PRELOAD
环境变量与LD_LIBRARY_PATH
环境变量的使用是不同的。- 正常程序的使用,应该尽量避免使用
LD_PRELOAD
,特别是发布版本的程序运行不应该依赖LD_PRELOAD
同样,为了避免对其他运行程序和后续测试产生影响,需要及时取消该环境变量设置:
unset LD_PREALOD
3、LD_DEBUG
该环境变量可以打开动态链接器的调试功能,当我们设置该变量时,动态链接器会在运行时打印出各种有用的信息,对于我们开发和调试共享库有很大的帮助。
LD_DEBUG
可以设置的值有:
- “files”,显示整个装载过程;
- “libs”,显示共享库查找过程;
- “symbols”,显示符号的查找过程;
- “bindings”,显示动态链接的符号绑定过程;
- “versions”,显示符号的版本依赖关系;
- “reloc”,显示重定位信息;
例如,查看整个装载过程:
LD_DEBUG=files ./envTest
18721:
18721: file=./libshowBytes.so [0]; needed by ./envTest [0]
18721: file=./libshowBytes.so [0]; generating link map
18721: dynamic: 0x00007f6e2c8ede20 base: 0x00007f6e2c8ea000 size: 0x0000000000004040
18721: entry: 0x00007f6e2c8eb0a0 phdr: 0x00007f6e2c8ea040 phnum: 11
18721:
18721:
18721: file=libc.so.6 [0]; needed by ./envTest [0]
18721: file=libc.so.6 [0]; generating link map
18721: dynamic: 0x00007f6e2c8dcb80 base: 0x00007f6e2c6f1000 size: 0x00000000001f1660
18721: entry: 0x00007f6e2c7151f0 phdr: 0x00007f6e2c6f1040 phnum: 14
18721:
18721:
18721: calling init: /lib/x86_64-linux-gnu/libc.so.6
18721:
18721:
18721: calling init: ./libshowBytes.so
18721:
18721:
18721: initialize program: ./envTest
18721:
18721:
18721: transferring control: ./envTest
18721:
calling show_twocomp
39 30
c7 cf
18721:
18721: calling fini: ./envTest [0]
18721:
18721:
18721: calling fini: ./libshowBytes.so [0]
18721:
或者查看依赖共享库的查找过程:
LD_DEBUG=libs ./envTest
18493: find library=libc.so.6 [0]; searching
18493: search path=/home/myTest/envTest/tls/haswell/x86_64:/home/myTest/envTest/tls/haswell:/home/myTest/envTest/tls/x86_64:/home/myTest/envTest/tls:/home/myTest/envTest/haswell/x86_64:/home/myTest/envTest/haswell:/home/myTest/envTest/x86_64:/home/myTest/envTest (LD_LIBRARY_PATH)
18493: trying file=/home/myTest/envTest/tls/haswell/x86_64/libc.so.6
18493: trying file=/home/myTest/envTest/tls/haswell/libc.so.6
18493: trying file=/home/myTest/envTest/tls/x86_64/libc.so.6
18493: trying file=/home/myTest/envTest/tls/libc.so.6
18493: trying file=/home/myTest/envTest/haswell/x86_64/libc.so.6
18493: trying file=/home/myTest/envTest/haswell/libc.so.6
18493: trying file=/home/myTest/envTest/x86_64/libc.so.6
18493: trying file=/home/myTest/envTest/libc.so.6
18493: search cache=/etc/ld.so.cache
18493: trying file=/lib/x86_64-linux-gnu/libc.so.6
18493:
18493:
18493: calling init: /lib/x86_64-linux-gnu/libc.so.6
18493:
18493:
18493: calling init: ./libshowBytes.so
18493:
18493:
18493: initialize program: ./envTest
18493:
18493:
18493: transferring control: ./envTest
18493:
calling show_twocomp
39 30
c7 cf
18493:
18493: calling fini: ./envTest [0]
18493:
18493:
18493: calling fini: ./libshowBytes.so [0]
18493:
另外还可以显示符号的查找过程:
LD_DEBUG=symbols ./envTest
18868: symbol=__vdso_clock_gettime; lookup in file=linux-vdso.so.1 [0]
18868: symbol=__vdso_gettimeofday; lookup in file=linux-vdso.so.1 [0]
18868: symbol=__vdso_time; lookup in file=linux-vdso.so.1 [0]
18868: symbol=__vdso_getcpu; lookup in file=linux-vdso.so.1 [0]
18868: symbol=__vdso_clock_getres; lookup in file=linux-vdso.so.1 [0]
18868: symbol=_res; lookup in file=./envTest [0]
18868: symbol=_res; lookup in file=./libshowBytes.so [0]
........................
18868: symbol=_dl_signal_error; lookup in file=./envTest [0]
18868: symbol=_dl_signal_error; lookup in file=./libshowBytes.so [0]
18868: symbol=_dl_signal_error; lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
18868: symbol=_dl_catch_error; lookup in file=./envTest [0]
18868: symbol=_dl_catch_error; lookup in file=./libshowBytes.so [0]
18868: symbol=_dl_catch_error; lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
18868:
18868: calling init: /lib/x86_64-linux-gnu/libc.so.6
18868:
18868:
18868: calling init: ./libshowBytes.so
18868:
18868:
18868: initialize program: ./envTest
18868:
18868:
18868: transferring control: ./envTest
18868:
18868: symbol=_dl_find_dso_for_object; lookup in file=./envTest [0]
18868: symbol=_dl_find_dso_for_object; lookup in file=./libshowBytes.so [0]
18868: symbol=_dl_find_dso_for_object; lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
18868: symbol=_dl_find_dso_for_object; lookup in file=/lib64/ld-linux-x86-64.so.2 [0]
18868: symbol=__tunable_get_val; lookup in file=./envTest [0]
18868: symbol=__tunable_get_val; lookup in file=./libshowBytes.so [0]
18868: symbol=__tunable_get_val; lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
18868: symbol=__tunable_get_val; lookup in file=/lib64/ld-linux-x86-64.so.2 [0]
calling show_twocomp
18868: symbol=printf; lookup in file=./envTest [0]
18868: symbol=printf; lookup in file=./libshowBytes.so [0]
18868: symbol=printf; lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
18868: symbol=putchar; lookup in file=./envTest [0]
18868: symbol=putchar; lookup in file=./libshowBytes.so [0]
18868: symbol=putchar; lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
39 30
c7 cf
18868:
18868: calling fini: ./envTest [0]
18868:
18868:
18868: calling fini: ./libshowBytes.so [0]
18868:
4、rpath 路径
rpath 路径需要在编译时指定,因为这些信息会被写入到了 ELF 文件中。
gcc showBytes.c -fPIC -shared -o libshowBytes.so -I. -L. -lshowBytes -g -Wall -Wl,-rpath-link $(pwd)
gcc -o envTest envTest.c -I. -L . -lshowBytes -g -Wall -Wl,-rpath $(pwd)
此时,检查下动态了依赖关系如下:
ldd envTest
linux-vdso.so.1 (0x00007fffe4fa3000)
libshowBytes.so => /home/myTest/envTest/libshowBytes.so (0x00007fe8017ae000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe8015b5000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe8017ba000)
可以看出共享库(如:动态库)是可以直接找到的。
5、小结
基于以上描述,我们可以总结出共享库(如:动态库)搜索顺序如下:
LD_PRELOAD
环境变量指定的共享库路径;LD_LIBRARY_PATH
环境变量指定的共享库路径;-rpath
链接时指定的共享库路径;/etc/ld.so.conf
配置文件指定的共享库路径;- 默认共享库路径,
/usr/lib
,lib
; - 此外,
LD_PRELOAD
环境变量与LD_LIBRARY_PATH
环境变量是不同的。
疑问:以上这些查找路径如何验证他们的优先级呢?
答疑:
比较简单的做法就是在这几个位置分别放置同名,但是不同作用的库。通过程序运行结果来看看到底优先使用哪个路径下的库。
文中源码
Linux环境
$ uname -a
Linux lm 5.4.0-100-generic #113-Ubuntu SMP Thu Feb 3 18:43:29 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none:hsa
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 9.4.0-1ubuntu1~20.04.1' --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-Av3uEd/gcc-9-9.4.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
所有代码存放路径为: /home/myTest/envTest
$ tree
.
├── envTest
├── envTest.c
├── libshowBytes.so
├── showBytes.c
└── showBytes.h
showBytes.h
/* $begin show-bytes */
#ifndef _SHOW_BYTES_H_
#define _SHOW_BYTES_H_
#include <stdio.h>
typedef unsigned char *byte_pointer;
void show_bytes(byte_pointer start, size_t len);
void show_int(int x);
void show_float(float x);
void show_pointer(void *x);
/* $end show-bytes */
#endif
showBytes.c
/* $begin show-bytes */
#include <showBytes.h>
void show_bytes(byte_pointer start, size_t len) {
size_t i;
for (i = 0; i < len; i++)
printf(" %.2x", start[i]);
printf("\n");
}
void show_int(int x) {
show_bytes((byte_pointer) &x, sizeof(int));
}
void show_float(float x) {
show_bytes((byte_pointer) &x, sizeof(float));
}
void show_pointer(void *x) {
show_bytes((byte_pointer) &x, sizeof(void *));
}
/* $end show-bytes */
envTest.c
/* $begin environment Test */
#include <showBytes.h>
#include <stdlib.h>
void test_show_bytes(int val) {
int ival = val;
float fval = (float) ival;
int *pval = &ival;
show_int(ival);
show_float(fval);
show_pointer(pval);
}
void show_twocomp()
{
short x = 12345;
short mx = -x;
show_bytes((byte_pointer) &x, sizeof(short));
show_bytes((byte_pointer) &mx, sizeof(short));
}
int main(int argc, char *argv[])
{
int l_val = 12345;
if (argc > 1) {
if (argc > 1) {
l_val = strtol(argv[1], NULL, 0);
}
printf("calling test_show_bytes\n");
test_show_bytes(l_val);
} else {
printf("calling show_twocomp\n");
show_twocomp();
}
return 0;
}
/* $end environment Test */
GCC命令
需要注意的是,动态共享库的名字必须是 libXXXX.so
开头的。本篇中例子是 libshowBytes.so
gcc -g -Wall -I/home/myTest/envTest -fPIC -shared -o libshowBytes.so showBytes.c
gcc -g -Wall -I/home/myTest/envTest -o envTest envTest.c -L/home/myTest/envTest -lshowBytes
gcc -g -Wall -I/home/myTest/envTest -o envTest envTest.c ./showBytes.so
其中最后的两条命令是等价的。即可以通过 -L 指定路径,或者是直接将所需的 so 库加载进去(直接运行程序即可,无需再配置环境变量)。