CSAPP第三版7.13.3节提到了运行时打桩机制,它可以在运行时将程序中对共享库函数的调用进行截获,替换为执行自己的代码。这个机制基于动态链接器的LD_PRELOAD环境变量。如果LD_PRELOAD环境变量被设置为一个共享路径名的列表(以空格或分号分隔),那么当加载和执行一个程序,需要解析未定义的引用时,动态链接器(ld-linux.so)会先搜索LD_PRELOAD库,然后才搜索任何其他的库。有了这个机制,当加载和执行任意可执行文件时,可以对共享库中的任何函数打桩,包括libc.so。
书中给出的自己编写的malloc和free的包装函数如下。在每个包装函数中,对dlsym的调用返回指向目标libc函数的指针。然后包装函数调用目标函数,打印追踪记录,再返回。
#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;
mallocp = 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 *) = NULL;
char *error;
if (!ptr)
return;
freep = dlsym(RTLD_NEXT, "free");
if ((error = dlerror()) != NULL)
{
fputs(error, stderr);
exit(1);
}
freep(ptr);
printf("free(%p)\n", ptr);
}
#endif
其编译指令为:
gcc –DRUNTIME –shared –fpic –o mymalloc.so mymalloc.c –ldl
主程序代码如下:
#include <stdio.h>
#include <malloc.h>
int main(int argc, char **argv[])
{
int *p = malloc(32);
free(p);
return 0;
}
其编译指令为:
gcc –o intr int.c
则在bash中使用运行时打桩机制运行该程序的指令如下:
LD_PRELOAD=”./mymalloc.so” ./intr
很不幸,这样做会出错(至少在我的机器上),错误如下:
Segmentation fault (core dumped)
发生了一个段错误,我们使用gdb来看看出了什么问题(先要加上-g选项使用gcc对库和程序重新编译,为了区分后面修改过的代码,我把包装函数的代码的文件名改成了badmalloc.c,动态库的名字改成了badmalloc.so):
gdb ./intr
设置LD_PRELOAD环境变量:
(gdb) set environment LD_PRELOAD=./badmalloc.so
直接开始执行程序:
(gdb) r
再次出现段错误:
Program received signal SIGSEGV, Segmentation fault.
0x00007ffff78591c3 in vfprintf () from /lib/x86_64-linux-gnu/libc.so.6
看看调用堆栈:
(gdb) bt
你会得到一个很长很长调用堆栈(反正我翻到了两万多行还没结束):
#0 0x00007ffff78591c3 in vfprintf () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x00007ffff7861849 in printf () from /lib/x86_64-linux-gnu/libc.so.6
#2 0x00007ffff7bd582e in malloc (size=1024) at badmalloc.c:19
#3 0x00007ffff7879185 in _IO_file_doallocate () from /lib/x86_64-linux-gnu/libc.so.6
#4 0x00007ffff78874c4 in _IO_doallocbuf () from /lib/x86_64-linux-gnu/libc.so.6
#5 0x00007ffff7886828 in _IO_file_overflow () from /lib/x86_64-linux-gnu/libc.so.6
#6 0x00007ffff78851bd in _IO_file_xsputn () from /lib/x86_64-linux-gnu/libc.so.6
#7 0x00007ffff7859201 in vfprintf () from /lib/x86_64-linux-gnu/libc.so.6
#8 0x00007ffff7861849 in printf () from /lib/x86_64-linux-gnu/libc.so.6
#9 0x00007ffff7bd582e in malloc (size=1024) at badmalloc.c:19
#10 0x00007ffff7879185 in _IO_file_doallocate () from /lib/x86_64-linux-gnu/libc.so.6
#11 0x00007ffff78874c4 in _IO_doallocbuf () from /lib/x86_64-linux-gnu/libc.so.6
#12 0x00007ffff7886828 in _IO_file_overflow () from /lib/x86_64-linux-gnu/libc.so.6
#13 0x00007ffff78851bd in _IO_file_xsputn () from /lib/x86_64-linux-gnu/libc.so.6
#14 0x00007ffff7859201 in vfprintf () from /lib/x86_64-linux-gnu/libc.so.6
#15 0x00007ffff7861849 in printf () from /lib/x86_64-linux-gnu/libc.so.6
#16 0x00007ffff7bd582e in malloc (size=1024) at badmalloc.c:19
#17 0x00007ffff7879185 in _IO_file_doallocate () from /lib/x86_64-linux-gnu/libc.so.6
#18 0x00007ffff78874c4 in _IO_doallocbuf () from /lib/x86_64-linux-gnu/libc.so.6
#19 0x00007ffff7886828 in _IO_file_overflow () from /lib/x86_64-linux-gnu/libc.so.6
#20 0x00007ffff78851bd in _IO_file_xsputn () from /lib/x86_64-linux-gnu/libc.so.6
#21 0x00007ffff7859201 in vfprintf () from /lib/x86_64-linux-gnu/libc.so.6
#22 0x00007ffff7861849 in printf () from /lib/x86_64-linux-gnu/libc.so.6
#23 0x00007ffff7bd582e in malloc (size=1024) at badmalloc.c:19
#24 0x00007ffff7879185 in _IO_file_doallocate () from /lib/x86_64-linux-gnu/libc.so.6
#25 0x00007ffff78874c4 in _IO_doallocbuf () from /lib/x86_64-linux-gnu/libc.so.6
---Type <return> to continue, or q <return> to quit---
需要注意,从#9到#2(调用关系要倒着看):
malloc->printf->vfprintf->_IO_file_xsputn->_IO_file_overflow->_IO_doallocbuf->_IO_file_doallocate->malloc
我们的malloc函数中调用了printf函数,printf函数又调用了我们的malloc函数,malloc函数又会调用printf函数……这产生了一个调用死循环,调用层次足够深,栈就溢出了。
那么如何打破这个死循环呢?首先要尽量避免在自己写的malloc函数中调用其他标准库函数,毕竟不清楚标准库函数的内部实现机制。但是为了输出一些信息,printf函数还是要保留的,那么怎么办呢?首先考虑单线程的情况,如果在我们自己写的malloc函数中发生了循环调用自己malloc的情况,唯一的可能就是printf调用了malloc。我们可以设置一个静态计数变量count,每次完成执行malloc函数后将count清零,每次进入malloc函数后count自增1,如果count=1,说明现在调用栈上只有对自定义malloc函数的一次调用,这时可以调用printf输出信息;如果count=2,说明此时调用栈上对malloc函数发生了第二次调用,即一个malloc函数还没有执行完,就又进行了一次malloc函数调用,我们认为这个问题出在printf上,此时我们就不再调用printf了。那么多线程情况呢?这个使用__thread修饰符将静态变量设置为thread local的就可以了。最后的malloc函数代码如下:
void *malloc(size_t size)
{
static __thread int print_times = 0;
print_times++;
void *(*mallocp)(size_t size);
char *error;
mallocp = dlsym(RTLD_NEXT, "malloc");
if ((error = dlerror()) != NULL)
{
fputs(error, stderr);
exit(1);
}
char *ptr = mallocp(size);
if (print_times == 1)
{
printf("malloc(%d) = %p\n", (int)size, ptr);
}
print_times = 0;
return ptr;
}
然后我将int.c改成了这样,验证printf是否会调用malloc:
#include <stdio.h>
#include <malloc.h>
int main(int argc, char **argv[])
{
printf("hello, world!\n");
return 0;
}
程序输出如下:
malloc(1024) = 0x22b0010
free(0x22b0420)
hello, world!
也就是说printf确实是调用了malloc的。
实际上dlsym、dlerror和fputs这些函数也是有可能调用了malloc函数的,但是程序正确运行了,说明在这种场景下这些函数没有调用malloc函数,在其他场景下是否会调用malloc函数就不一定了。为了简单起见,没有进行更多的修改。