7.13 库打桩机制
Linux 链接器支持一个很强大的技术,称为库打桩(library interpositioning),它允许你截获对共享库函数的调用,取而代之执行自己的代码。使用打桩机制,你可以追踪对某个特殊库函数的调用次数,验证和追踪它的输入和输出值,或者甚至把它替换成一个完全不同的实现。
下面是它的基本思想:给定一个需要打桩的目标函数,创建一个包装函数,它的原型与目标函数完全一样。使用某种特殊的打桩机制,你就可以欺骗系统调用包装函数而不是目标函数了。包装函数通常会执行它自己的逻辑,然后调用目标函数,再将目标函数的返回值传递给调用者。
打桩可以发生在编译时、链接时或当程序被加载和执行的运行时。要研究这些不同的机制,我们以图 7-20a 中的示例程序作为运行例子。它调用 C 标准库(libc.so)中的 malloc 和 free 函数。对 malloc 的调用从堆中分配一个 32 字节的块,并返回指向该块的指针。对 free 的调用把块还回到堆,供后续的 malloc 调用使用。我们的目标是用打桩来追踪程序运行时对 malloc 和 free 的调用。
7.13.1 编译时打桩
图 7-20 展示了如何使用 C 预处理器在编译时打桩。mymalloc.c 中的包装函数(图 7-20c)调用目标函数,打印追踪记录,并返回。本地的 malloc.h 头文件(图 7-20b)指示预处理器用对相应包装函数的调用替换掉对目标函数的调用。
#include <stdio.h>
#include <malloc.h>
int main()
{
int *p = malloc(32);
free(p);
return(0);
}a) 示例程序 int.c
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
void *mymalloc(size_t size);
void myfree(void *ptr);b) 本地 malloc.h 文件
c) mymalloc.c 中的包装函数
图 7-20 用 C 预处理进行编译时打桩
像下面这样编译和链接这个程序:
运行这个程序会得到如下的追踪信息:
7.13.2 链接时打桩
Linux 静态链接器支持用 --wrap f 标志进行链接时打桩。这个标志告诉链接器,把对符号 f 的引用解析成 wrap_f(前缀是两个下划线),还要把对符号 __real_f(前缀是两个下划线)的引用解析为 f。图 7-21 给出我们示例程序的包装函数。
图 7-21 用 --wrap 标志进行链接时打桩
用下述方法把这些源文件编译成可重定位目标文件:
然后把目标文件链接成可执行文件:
-Wl,option 标志把 option 传递给链接器。option 中的每个逗号都要替换为一个空格。所以 -Wl,--wrap,malloc 就把 --wrap malloc 传递给链接器,以类似的方式传递 -Wl,--wrap,free。
运行该程序会得到如下追踪信息:
7.13.3 运行时打桩
编译时打桩需要能够访问程序的源代码,链接时打桩需要能够访问程序的可重定位对象文件。不过,有一种机制能够在运行时打桩,它只需要能够访问可执行目标文件。这个很厉害的机制基于动态链接器的 LD_PRELOAD 环境变量。
如果 LD_PRELOAD 环境变量被设置为一个共享库路径名的列表(以空格或分号分隔),那么当你加载和执行一个程序,需要解析未定义的引用时,动态链接器(LD-LINUX.SO)会先搜索 LD_PRELOAD 库,然后才搜索任何其他的库。有了这个机制,当你加载和执行任意可执行文件时,可以对任何共享库中的任何函数打桩,包括 libc.so。
图 7-22 展示了 malloc 和 free 的包装函数。每个包装函数中,对 dlsym 的调用返回指向目标 libc 函数的指针。然后包装函数调用目标函数,打印追踪记录,再返回。
图 7-22 用 LD_PRELOAD 进行运行时打桩
下面是如何构建包含这些包装函数的共享库的方法:
这是如何编译主程序:
下面是如何从 bash shell 中运行这个程序:
下面是如何在 csh 或 tcsh 中运行这个程序:
请注意,你可以用 LD_PRELOAD 对任何可执行程序的库函数调用打桩!
最后更新于