LD_Preload的技巧是什么?
1. 概述
** LD_PRELOAD技巧是一种有用的技术,可以在运行时影响共享库的链接和符号(函数)的解析**。为了解释LD_PRELOAD,让我们先讨论一下 Linux 系统中的库。
简而言之,库是编译函数的集合。我们可以在我们的程序中使用这些功能,而无需重写相同的功能。这可以通过在我们的程序中包含库代码(静态库 )或通过在运行时动态链接(共享库 )来实现。
使用静态库,我们可以构建独立的程序。另一方面,使用共享库构建的程序需要运行时链接器/加载器支持。出于这个原因,在执行程序之前,所有需要的符号都被加载并且程序准备好执行。
2. 运行时执行环境
** LD_PRELOAD技巧在程序执行准备阶段派上用场。** Linux 系统程序ld.so 和ld-linux.so (动态链接器/加载器)使用LD_PRELOAD加载指定的共享库。特别是,在任何其他库之前,动态加载器将首先加载位于LD_PRELOAD 中的共享库。
**需要注意的是,在安全执行模式下,可以忽略LD_PRELOAD中的条目。**当路径中出现斜杠时会发生这种情况 - 这意味着文件不在默认搜索路径中。在这种情况下,动态加载器将仅在库设置了setuid 位时加载系统库。
3. LD_PRELOAD的用例
在本节中,我们将尝试LD_PRELOAD技巧的一些用例。首先,我们将了解如何覆盖库。稍后,我们将使用LD_PRELOAD 进行插入。
**由于LD_PRELOAD是一个环境变量,它只影响当前进程。**因此,我们将只使用绝对路径。
在开始使用LD_PRELOAD之前,让我们首先使用ldd 命令。
** ldd命令对于列出二进制程序或共享库的运行时依赖关系很有用**:
$ ldd /usr/bin/pigz
linux-vdso.so.1 (0x00007ffd559d7000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fa48bcc1000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fa48bc9e000)
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007fa48bc82000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa48ba90000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa48c065000)
使用*ldd,*我们可以看到pigz 程序依赖于几个共享库。 我们还有其他选项可以查看使用过的共享库 。
3.1. 使用LD_PRELOAD替换库
现在让我们回到终端,看看LD_PRELOAD技巧的实际效果:
$ LD_PRELOAD=/data/preload/lib/libz.so.1.2.7 ldd /usr/bin/pigz
linux-vdso.so.1 (0x00007ffc1d9c4000)
/data/preload/lib/libz.so.1.2.7 (0x00007f33877d9000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f3387674000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f3387651000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f338745f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3387a32000)
现在,我们可以看到pigz程序的zlib ( libz ) 依赖项发生了变化,从系统默认位置更改为我们在LD_PRELOAD中设置的位置。
如果我们要使用LD_PRELOAD运行多个程序,最好分别使用export 和unset 设置和清除环境变量。现在让我们这样做并通过调用pigz程序本身来确认更改:
$ pigz -vV
pigz 2.4
zlib 1.2.11
$ export LD_PRELOAD=/data/preload/lib/libz.so.1.2.7
$ pigz -vV
pigz 2.4
zlib 1.2.7
$ unset LD_PRELOAD
$ pigz -vV
pigz 2.4
zlib 1.2.11
在这里,我们使用*-vV选项调用pigz* ,以便它报告pigz和 zlib版本。
3.2. 使用LD_PRELOAD 进行插入
让我们使用一个 interposer 库来改变一个系统函数的行为。对于这个例子,我们将使用一个虚构的库来向malloc 添加一些会计功能。
使用LD_PRELOAD技巧,我们可以改变任何依赖于外部共享库的程序的行为。例如,ls 程序依赖于libc,它提供了许多系统功能,包括使用malloc分配内存:
$ ls -lh
total 2,8G
-rw-rw-r-- 1 blogdemo_user blogdemo_user 3,6K Jul 4 20:16 BVF_Density.ipynb
-rwxrwxr-x 1 blogdemo_user blogdemo_user 2,8G Jul 5 15:59 cuda_11.0.1_450.36.06_linux.run
drwxrwxr-x 2 blogdemo_user blogdemo_user 25 Jul 8 16:12 h5store
drwxrwxr-x 2 blogdemo_user blogdemo_user 30 Jul 5 18:47 pandas_processing
drwxrwxr-x 4 blogdemo_user blogdemo_user 142 Jul 19 15:59 preload
$ LD_PRELOAD=/data/preload/lib/malloc_interpose.so ls -lh
malloc(20000) call number: 223
malloc(32816) call number: 226
total 2,8G
-rw-rw-r-- 1 blogdemo_user blogdemo_user 3,6K Jul 4 20:16 BVF_Density.ipynb
-rwxrwxr-x 1 blogdemo_user blogdemo_user 2,8G Jul 5 15:59 cuda_11.0.1_450.36.06_linux.run
drwxrwxr-x 2 blogdemo_user blogdemo_user 25 Jul 8 16:12 h5store
drwxrwxr-x 2 blogdemo_user blogdemo_user 30 Jul 5 18:47 pandas_processing
drwxrwxr-x 4 blogdemo_user blogdemo_user 142 Jul 19 15:59 preload
预加载我们的 interposer 库后,ls命令的输出会有所不同。
这是因为现在我们定制库中的 malloc 函数优先于标准malloc函数。在这个插入器示例中,malloc函数在某些内存分配调用中打印大小和调用计数。
可以在LD_PRELOAD变量中指定多个库。为此,我们可以提供一个库列表,使用冒号或空格分隔:
$ LD_PRELOAD="/data/preload/lib/malloc_interpose.so:/data/preload/lib/free_interpose.so" ls -lh
malloc(20000) call number: 223
malloc(32816) call number: 226
total 2,8G
free((nil)) call number: 174
free((nil)) call number: 175
free((nil)) call number: 178
-rw-rw-r-- 1 blogdemo_user blogdemo_user 3,6K Jul 4 20:16 BVF_Density.ipynb
-rwxrwxr-x 1 blogdemo_user blogdemo_user 2,8G Jul 5 15:59 cuda_11.0.1_450.36.06_linux.run
drwxrwxr-x 2 blogdemo_user blogdemo_user 25 Jul 8 16:12 h5store
drwxrwxr-x 2 blogdemo_user blogdemo_user 30 Jul 5 18:47 pandas_processing
drwxrwxr-x 4 blogdemo_user blogdemo_user 142 Jul 19 15:59 preload
free((nil)) call number: 180
现在,我们向LD_PRELOAD变量添加了另一个虚构的插入器库,该变量报告对系统函数free 的某些调用,其中传递了一个NULL指针。
4. 什么时候有用?
LD_PRELOAD技巧在某些情况下可能会有所帮助。
例如,考虑两个库导出相同符号并且我们的程序链接到错误的符号的情况。在这种情况下,可以使用LD_PRELOAD预加载具有正确符号的库。这样做将导致正确符号的分辨率。
另一个用例是应该首选库函数的优化或自定义实现。我们可以在不更改原始库的情况下预加载此优化或自定义实现。此外,我们还可以通过提供不同的版本来替换整个库。
此外,各种分析和监控工具广泛使用LD_PRELOAD 来检测代码。例如,性能分析应用程序将插入关键系统功能。这使分析器能够从用户应用程序收集相关数据。
5.替代LD_PRELOAD
除了LD_PRELOAD技巧之外,还有一个使用*/etc/ld.so.preload*文件的替代方法。但是,这是系统范围的设置。在大多数系统中,默认情况下甚至不存在此文件,并且必须由系统管理员创建它。
现在让我们创建这个文件并选择一个旧版本的zlib进行预加载,然后我们将再次测试pigz程序:
$ pigz -vV
pigz 2.4
zlib 1.2.11
$ sudo -i
# echo "/data/preload/lib/libz.so.1.2.8" > /etc/ld.so.preload
# exit
logout
$ pigz -vV
pigz 2.4
zlib 1.2.8