linux 库打桩

linux库打桩
库打桩,就是在Windows下常说的API Hook,通过替换一些系统的API调用从而进行一些监控、统计以及不可告人的目的~~
假设我们有个程序:
编译运行:
输出结果:
现在我们想监控一下其中的内存分配函数malloc和内存释放函数free的调用。这里提供三种方法。
编译期打桩
我们先写一版我们自己的malloc和free,需要注意的是,我们的文件名称需要与源文件中引入的malloc.h相同,用以欺骗编译器。
接着我们实现our_malloc和our_free:
代码中有个SRC_FLAG宏需要注意,当定义这个宏的时候,malloc和free不会被替换,当未定义这个宏的时候malloc会被替换为our_malloc,free会被替换为our_free。因为main.c中未定义SRC_FLAG,所以会发生宏替换,而malloc.c中不会发生替换。
接下来我们编译代码(重要):
注意这两个编译命令,第一个直接编译malloc.c,第二个编译并链接main.c和malloc.o,注意第二句编译命令,其中有-I.参数指定头文件搜索路径为当前目录,此时编译器会优先搜索当前目录,而不是去系统库目录寻找头文件,因此,编译器会使用我们的malloc.h,而不是系统的malloc.h。
使用了我们的malloc.h,malloc和free就会发生宏替换,从而实现打桩的功能。
第二句编译命令没有使用-I参数,是因为我们本意就是使用系统的malloc.h,如果指定了-I.,那么malloc.c中就会包含我们自己写的malloc.h,就无法调用C库malloc和free了。
运行一下,查看结果:
输出:
内存地址在不同机器上表现不同。
可以看到,我们对源文件main.c并没有任何改动,就可以通过宏和编译参数在编译期实现了“打桩”操作。
此方法适合还没有发生任何编译的时期。
接下来我们看一下当只有对象文件(.o)的时候如何打桩。
链接期打桩
这里我们只有main.o文件,而没有main.c文件,此时如何打桩呢?
gcc有个编译参数:-Wl注意,这里的l是L的小写字母,表示linker,而不是i的大写或者数字1.
使用方法为:
-Wl,--wrap,func表示链接的时候将符号func修改为__wrap_func,并将__real_func修改为func。
有了这个操作,我们开始编写代码:
这个源代码中,我们定义了函数__wrap_malloc和__wrap_free,由上面的解释我们可以知道,在目标文件中的malloc和free会被替换为这两个函数。这两个函数中调用了__real_malloc和__real_free,链接时会被替换为malloc和free,执行真正的内存操作。
接下来我们编译代码:
我们假设main.o是由文章开始时的main.c使用gcc -c main.c编译出来的。
那么链接成功后运行结果为:
这就是在没有源码但是有目标文件时的链接期打桩。
但是,如果如果我们连目标文件都没有了,只有最后的可执行文件,是否还可以打桩呢?当然可以!
运行期打桩
程序运行后,当遇到未解析的符号时,系统会去搜索LD_PRELOAD环境变量包含的库,查找符号,没有找到符号时才会去其他位置查找,我们可以使用这个特性来进行运行期打桩。
编写我们的库代码:
接下来我对代码做一下讲解:
dlsym函数用于导出符号,类似于Windows下的GetProcAddress,其第一个参数是一个共享库的句柄,可以通过dlopen函数获取到,本次我们传递了RTLD_NEXT参数,这个参数表示从下一个位置搜索符号,什么是下一个位置呢?对于一个符号,dlsym会首先从本地搜索,即当前模块(对应RTLD_DEFAULT),然后搜索其他(比如C库、其他已导入的动态库等),由于我们本模块中已定义了malloc和free,而我们需要使用C库中的malloc和free,我们自然需要跳过当前模块,即RTLD_NEXT。RTLD_NEXT在_GNU_SOURCE被定义时才生效,其原型为:
另外,注意到我们定义了一个静态变量call_printf,这主要是因为printf中会调用malloc,这时就会发生无限递归,所以我们设置了一个条件来终止递归(事实上添加了这个条件后无法打印printf中的malloc信息)。
接下来我们来编译动态库:
其中-fPIC表示生成位置无关代码(具体含义自行搜索,只要记住linux下的动态库基本都需要使用此参数就 ok 啦),-ldl表示链接dl库,这个库包含对共享库的操作,dlsym就实现在这个库中。
编译后得到libruntime.so。
设置环境变量LD_PRELOAD(具体的shell设置环境变量的方式可能不同,此处以bash举例,而且,因为LD_PRELOAD影响较大,千万不要设置系统环境变量,最好只设置当前shell或者当前命令范围内的环境变量)并调用main:
得到结果:
这样就完成了运行时的“打桩”。
我们的动态库不止可以在main中打桩,还可以在任意程序中打桩,如:
总结
使用宏和
-I编译参数对程序进行编译期打桩。使用
-Wl,--wrap,func对程序进行链接期打桩。使用
LD_PRELOAD环境变量对程序进行运行期打桩。
于是~~我们就成为了“打桩机”…… ^_^
版权声明: 本文为 InfoQ 作者【SkyFire】的原创文章。
原文链接:【http://xie.infoq.cn/article/ec70212ec7f5dd8ed307b2565】。文章转载请联系作者。











评论