库打桩机制
库打桩(library interpositioning)允许你截获对共享库函数的调用,取而代之执行自己的代码。使用打桩机制,可以追踪对某个特殊函数的调用次数,验证和追踪它的输入和输出值,或者甚至把它替换成一个完全不同的实现。
基本思想:给定一个需要打桩的目标函数,创建一个包装函数,它的原型和目标函数完全一样。使用某种特殊的打桩机制,就可以欺骗系统调用包装函数而不是目标函数了。包装函数通常会执行自己的逻辑,然后调用目标函数,再将目标函数的返回值传递给调用者。
打桩可以发生在编译时、链接时或当程序被加载和执行时。
编译时打桩
用C预处理器进行编译时打桩,在当前目录下创建malloc.h
,mymalloc.c
,main.c
本地malloc.h文件:指示预处理器调用对相应包装函数的调用替换掉对目标函数的调用。
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
void *mymalloc(size_t size);
void myfree(void *ptr);
mymalloc.c中的包装器函数:
#ifdef COMPILETIME
#include <stdio.h>
#include <malloc.h> // 使用标准malloc.h头文件编译
/* malloc wrapper function */
void *mymalloc(size_t size) {
static size_t _mymalloc_calltime = 0;
++_mymalloc_calltime;
void *ptr = malloc(size);
printf("malloc(%d) = %p, callTime = %d\n", (int)size, ptr, (int)_mymalloc_calltime);
return ptr;
}
/* free wrapper function */
void myfree(void *ptr) {
static size_t _myfree_calltime = 0;
++_myfree_calltime;
free(ptr);
printf("free(%p), callTime = %d\n", ptr, (int)_myfree_calltime);
}
#endif
示例程序main.c:
#include <stdio.h>
#include <malloc.h>
int main() {
int *p = malloc(32);
free(p);
return 0;
}
使用C预处理器在编译时打桩:
# -D宏名
gcc -DCOMPILETIME -c mymalloc.c
# 因为有-I.参数,所以会打桩,它告诉C预处理器在搜索通常的系统目录之前,先在当前目录中查找malloc.h,main函数中的malloc会变成mymalloc
gcc -I. -o main main.c mymalloc.o
./main
malloc(32) = 0x56192dfc02a0, callTime = 1
free(0x56192dfc02a0), callTime = 1
通过预编译查看main.i: gcc -I. -E -o main.i main.c
,可以看到malloc变成了mymalloc,free变成了myfree
# 2 "main.c" 2
# 1 "./malloc.h" 1
# 4 "./malloc.h"
void *mymalloc(size_t size);
void myfree(void *ptr);
# 3 "main.c" 2
int main() {
int *p = mymalloc(32);
myfree(p);
return 0;
}
编译时打桩需要能够访问程序的源代码。本质上就是先定义自己的包装函数,在包装函数中调用标准的目标方法,并生成可重定位的文件。再定义一个通过宏定义方式转换的头文件(名称与需要打桩的函数所在的头文件相同),gcc编译时优先从本地搜索该同名文件而调用了包装函数。
- 打桩函数内部不要打桩,即在mymalloc函数中要使用原始的malloc函数,在myfree函数中要使用原始的free函数,不然会造成循环调用。
- 通过#define指令,将外部调用malloc的地方都替换为mymalloc,free替换为myfree。
- 分开编译mymalloc.c和外部调用代码main.c,最终链接。
链接时打桩
Linux静态链接器支持用--warp f
标志进行链接时打桩。这个标志告诉链接器,把对符号f的引用解析成__warp_f
(前缀是两个下划线),还要把对符号__real_f
(前缀是两个下划线)的引用解析为f。
在当前目录下创建main.c
,mymalloc.c
main.c:
#include <stdio.h>
#include <malloc.h>
int main() {
int *p = malloc(32);
free(p);
return 0;
}
mymalloc.c:用–wrap标志进行链接时打桩
#ifdef LINKTIME
#include<stdio.h>
void *__real_malloc(size_t size);
void __real_free(void *ptr);
/* malloc wrapper function */
void *__wrap_malloc(size_t size) {
void *ptr = __real_malloc(size); // call libc malloc
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
/* free wrapper function */
void __wrap_free(void *ptr) {
__real_free(ptr); // call libc free
printf("free(%p)\n", ptr);
}
#endif
链接时打桩:
# 把源文件编译成重定位目标文件
gcc -DLINKTIME -c mymalloc.c
gcc -c main.c
# 把目标文件链接成可执行文件,-Wl,option标志把option传递给链接器,option中的每个逗号都要替换为一个空格。所以-Wl,--wrap,malloc就把--wrap malloc传递给链接器,以类似的方式传递-Wl,--wrap,free
# -Wl,--wrap,malloc ==> --wrap malloc ==> __wrap_malloc
# __real_f ==> f
gcc -Wl,--wrap,malloc -Wl,--wrap,free -o main main.o mymalloc.o
./main
malloc(32)= 0x5ef0514752a0
free(0x5ef0514752a0)
链接时打桩需要能够访问程序的可重定位对象文件。
利用链接器的打桩机制,最后在main函数中调用malloc,将会去调用__wrap_malloc
,而__real_malloc
将会被解析成真正的malloc。
运行时打桩
运行时打桩只需要能够访问可执行目标文件。
机制基于动态链接器的LD_PRELOAD
环境变量。如果LD_PRELOAD
环境变量被设置为一个共享库路径名的列表(以空格或分号分隔),那么当你加载和执行一个程序,需要解析未定义的引用时,动态链接器(LD-LINUX.SO)会先搜索LD_PRELOAD
库,然后才搜索任何其它库。通过此机制,可以加载和执行任意可执行文件时,可以对任何共享库中的任何函数打桩,包括libc.so。
在当前目录下创建main.c
,mymalloc.c
main.c:
#include <stdio.h>
#include <malloc.h>
int main() {
int *p = malloc(32);
free(p);
return 0;
}
mymalloc.c:
#ifdef RUNTIME
#define __USE_GNU // 必须有此行,不再是_GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
/* malloc wrapper function */
void *malloc(size_t size) {
void *(*mallocp)(size_t size);
char *error;
mallocp = dlsym(RTLD_NEXT, "malloc"); // get address of libc malloc
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
char *ptr = mallocp(size); // call libc malloc
char buf[512];
sprintf(buf, "malloc(%d) = %p\n", (int)size, ptr);
fputs(buf, stderr);
// printf("malloc(%d) = %p\n", (int)size, ptr); // segment fault
return ptr;
}
/* free wrapper function */
void free(void *ptr) {
void (*freep)(void *) = NULL;
char *error;
if (!ptr) {
return;
}
freep = dlsym(RTLD_NEXT, "free"); // get address of libc free
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
freep(ptr); // get libc free
printf("free(%p)\n", ptr);
}
#endif
运行时打桩:
gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
gcc -o main main.c
LD_PRELOAD="./mymalloc.so" ./main
malloc(32) = 0x56314f86b2a0
malloc(1024) = 0x56314f86b2d0
free(0x56314f86b2a0)
# 使用LD_PRELOAD可以对任何可执行程序的库函数调用打桩
LD_PRELOAD="./mymalloc.so" /usr/bin/uptime
- 要避免打桩函数内部调用打桩函数
参考: