Linux 库打桩机制
前言
库打桩(library interpositioning),它允许你截获对共享库函数的调用,取而代之执行自己的代码。使用打桩机制,你可以追踪对某个特殊库函数的调用次数,验证和追踪它的输人和输出值,或者甚至把它替换成一个完全不同的实现。
下面是它的基本思想:
- 给定一个需要打桩的目标函数,创建一个包装函数,它的原型与目标函数完全一样。
- 使用某种特殊的打桩机制,你就可以欺骗系统调用包装函数而不是目标函数了。
- 包装函数通常会执行它自己的逻辑,然后调用目标函数,再将目标函数的返回值传递给调用者。
发生时机:
- 可以发生在编译时、链接时或当程序被加载和执行的运行时。
01 编译时打桩
举个栗子,对 malloc
和 free
追踪:
// int.c
#include <stdio.h>
#include <malloc.h>
int main()
{
int *p = malloc(32);
free(p)
return 0;
}
// malloc.h
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
void *mymalloc(size_t size);
void myfree(void *ptr);
#ifdef COMPILETIME
#include <stdio.h>
#include <malloc.h>
void *mymalloc(size_t size)
{
void *ptr = malloc(size);
printf("malloc(%d)=%p\n", (int)size, ptr);
return ptr;
}
void myfree(void *ptr)
{
free(ptr);
printf("free(%p)\n", ptr);
}
#endif
linux> gcc -DCOMPILETIME -c mymalloc.c
linux> gcc -I. -o intc int.c mymalloc.o
其中
-D
代表给源文件传递一个宏COMPILETIME
-c
在当前目录下生成同名目标文件-I
用于指定头文件的目录,这里是为了让 C 预处理器在搜索通常的系统目录前,现在当前目录查找malloc.h
注:因为只有 int.c
指定头文件搜索当前目录,所以 mymalloc.c
仍然使用的是库文件的 malloc
02 链接时打桩
Linux 支持用 --wrap f
标志进行链接时打桩。这个标志告诉链接器,把对符号 f
的引用解析成 __wrap_f
,还要把对符号 __real_f
的引用解析为 f
。
举个栗子:
// mymalloc.c
#ifdef LINKTIME
#include <stdio.h>
void *__real_malloc(size_t size); // 这个是库函数的 malloc
void __real_free(void *ptr);
void *__wrap_malloc(size_t size) // 这个是包装的 malloc
{
void *ptr = __real_malloc(size);
printf("malloc(%d)=%p\n", (int)size, ptr);
return ptr;
}
void __wrap_free(void *ptr)
{
__real_free(ptr);
printf("free(%p)\n", ptr);
}
linux> gcc -DLINKTIME -c mymalloc.c
linux> gcc -c int.c
linux> gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o
其中
-Wl
后面的东西是作为参数传递给链接器ld
的。比如:gcc -Wl,arg1,arg2
03 运行时打桩
编译时打桩需要能够访问程序的源代码,链接时打桩需要能够访问程序的可重定位对象文件。不过,有一种机制能够在运行时打桩,它只需要能够访问可执行目标文件。
这个机制基于动态链接器的 LD_PRELOAD
环境变量。如果 LD_PRELOAD
环境变量被设置为一个共享库路径名的列表(以空格或分号分隔),那么当你加载和执行一个程序,需要解析未定义的引用时,动态链接器 (LD-LINUX.SO
) 会先搜索 LD_PRELOAD
库,然后才搜索任何其他的库。
有了这个机制,当你加载和执行任意可执行文件时,可以对任何共享库中的任何函数打桩,包括 libc.so
// mymalloc.c
#ifdef RUNTIME
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
void *malloc(size_t size)
{
void *(*mallocp)(size_t size);
char *error;
// 获取 libc 中 malloc 的地址
malloc = dlsym(RTLD_NEXT, "malloc");
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
char *ptr = mallocp(size);
printf("malloc(%d)=%p\n", (int)size, ptr);
return ptr;
}
void free(void *ptr)
{
void (*freep)(void *);
char *error;
// 获取 libc 中 free 的地址
freep = dlsym(RTLD_NEXT, "free");
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
freep(ptr);
printf("free(%p)\n", ptr);
}
# 构建包含这些包装函数的共享库
linux> gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
# 编译主程序
linux> gcc -o intr int.c
# 运行(shell)
linux> LD_PRELOAD="./mymalloc.so" ./intr
# 运行(csh/tcsh)
linux> (setenv LD_PRELOAD "./mymalloc.so"; ./intr; unsetenv LD_PRELOAD)
其中
-shared
生成动态链接库(共享库)-fpic
生成位置无关代码,这是共享库所要求的-ldl
程序中使用了dlsym()
、dlerror()
等函数,需要加入此编译选项
注意:也可以用 LD_PRELOAD
对任何可执行程序的库函数调用打桩
参考资料:
- 《深入理解计算机系统》P526
评论区