你是否想多改变库代码的工作方式,不替换整个库或者重新编译它。例如,你想包裹一层malloc
和free
函数来记录分配的日志,为了查找内存泄露。你可以重写那段使用了malloc/free
的代码,或者修改libc
,这两者听起来都不是很吸引人的方式
这个教程将告诉你用自己实现的wrapper来代替库中的函数,这被叫做函数打桩(function interposition),它可以在任何程序中完成,而无需重新编译程序或库。
首先,一些背景知识:动态链接。当一个程序使用动态链接库编译时,有一列表记录未定义的符号(symbol)包含在二进制中的,还有一个列表记录程序所链接的库(library)。符号和库两者没什么关联,这两个列表仅仅用来告诉loader那些库需要被加载、那些符号需要被解析。在运行时(Runtime),每个符号用提供的第一个库来解析,这意味着如果我们将包含我们wrapper函数的库在其他库的前面加载,程序中的未定义符号表将解析到我们的wrapper函数,而不是真正的函数
我们如何让一个程序加载它没有链接的库?幸运的是,这是最简单的部分。环境变量LD_PRELOAD
为加载器提供了一个库列表,以便在其他任何操作之前加载。假设我们有一个名为libjmalloc
的共享库。因此包括malloc
和free
的替换。我们想在程序foo
中使用它,所以我们像这样运行它:
LD_PRELOAD=/home/jay/libjmalloc.so ./foo
这个loader将表现的像foo
链接了libmalloc.so
一样,我们给它一个到库的绝对路径,这样它就不会去搜索/usr/lib
这样的普通位置。如果要预加载多个库,请使用冒号分隔它们的名称。
到目前为止都很好,但是如果我们想在自己实现的版本中使用原版的malloc
呢?例如,我们仅仅想在调用malloc/free
时打印一条信息,但是内存管理仍然用原来的malloc/free
。然而我们不能在wrapper中直接调用libc
版的malloc
,因为编译器会将它解释为对wrapper本身的递归调用。解决方案是使用dlsym
动态加载指向malloc
的指针:
#define _GNU_SOURCE #include <stdio.h> #include <stdint.h> #include <dlfcn.h> void* malloc(size_t size) { static void* (*real_malloc)(size_t) = NULL; if (!real_malloc) real_malloc = dlsym(RTLD_NEXT, "malloc"); void *p = real_malloc(size); fprintf(stderr, "malloc(%d) = %p\n", size, p); return p; }
我们编译一下:
gcc -shared -ldl -fPIC jmalloc.c -o libjmalloc.so
dlfcn.h
申明函数将动态加载符号表,而不是链接到程序中。这些函数的一个主要用法就是加载plugins。例如在这里,我们认为libc
是一个提供了malloc
(我们把它赋给real_malloc
)的插件。我们使用dlsym
加载这个符号,它有两个参数:(a library handle, a symbol name)。通常情况下,我们使用dlopen
获取一个有效的库句柄,但是因为malloc
所在的libc
库默认就是被链接,所以我们只需要传一个RTLD_NEXT
,它告诉动态链接器:在下一个支持它的库中解析符号(而不是在当前调用dlsyum
的这个库),RTLD_NEXT
是GNU
特有的,所以记得在include dlfcn.h
之前定义_GNU_SOURCE
此时,您可以替换大部分的库函数。但是,有一些函数不能使用此方法插入。例如,如果您想为dlsym本身创建一个包装器,该怎么办?您还不能在内部包装任何库函数dlsym调用。
如果你确实需要包装这些函数,GNU链接器提供了一个有用的选项 --wrap
。如果你给它一个符号dlsym
,它会将所有的dlsym
调用替换成__wrap_dlsym
,用real_dlsym
调用真正的dlsym
,这种方法的缺点是,您需要重新链接使用wrapper任何程序。上面的例子可以被写成这样:
#include <stdint.h> #include <stdio.h> void* __real_malloc(size_t); void* __wrap_malloc(size_t size) { void *p = __real_malloc(size); fprintf(stderr, "malloc(%d) = %p\n"); return p; }
写在最后的一点东西。首先,LD_PRELOAD
会因为安全原因被SUID权限的程序忽略,由于函数插入可以让程序执行任何您想要的操作,因此Linux阻止您修改代表其他用户或组运行的程序的行为。其次,您不能插入静态链接库的函数调用,因为这些调用在运行时之前已解析。例如,如果libc中的某个函数调用malloc,它将永远不会调用其他库中的包装函数。
除了这些限制之外,函数插入是一种非常强大的技术,可用于监控程序或修改其行为。愉快的介入!
ref:
https://jayconrod.com/posts/23/tutorial-function-interposition-in-linux