Bootstrap

CSAPP第三版运行时打桩Segmentation fault

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函数就不一定了。为了简单起见,没有进行更多的修改。

;