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】。文章转载请联系作者。
评论