GCC Function instrumentation机制可以用来
跟踪
函数的调用关系,在gcc中对应的选项为“-finstrument-functions”。可查看gcc的man page来获取更详细信息。
编译时如果为gcc加上“-finstrument-functions”选项,那在每个函数的入口和出口处会各增加一个额外的hook函数的调用,增加的这两个函数分别为:
这是什么意思呢?例如我们写了一个函数func_test(),定义如下:
例如下面这段代码:
编译代码:
如果不想跟踪某个函数,可以给该函数指定“no_instrument_function”属性。需要注意的是,__cyg_profile_func_enter()和__cyg_profile_func_exit()这两个hook函数是一定要加上“no_instrument_function”属性的,不然,自己 跟踪 自己就会无限循环导致程序崩溃,当然,也不能在这两个hook 函数 中调用其他需要被跟踪的函数。
得到一系列的地址看起来不太直观,我们更希望看到函数名,幸运的是,addr2line工具为我们提供了这种可能。我们先看一下addr2line的使用方法:
同样是上面的程序,我们加上-g选项再编译一次:
另外,我们知道__builtin_return_address(level)宏可以获得不同层级的函数返回地址,但是在某些体系架构(如mips)中,__builtin_return_address(level)只能获得当前函数的直接调用者的地址,即level只能是0,那这时,就可使用上述方法来 跟踪 函数调用关系(mips中竟然能用,确实有些小吃惊)。
接下来可以看一下gcc是如何将hook函数嵌入各个函数中的,以反汇编代码中的do_multi()函数为例(这是mips的汇编代码),在mips中,ra 寄存器 用来 存储 返回地址,a0-a3用来做函数参数。
编译时如果为gcc加上“-finstrument-functions”选项,那在每个函数的入口和出口处会各增加一个额外的hook函数的调用,增加的这两个函数分别为:
void __cyg_profile_func_enter (void *this_fn, void *call_site);void __cyg_profile_func_exit (void *this_fn, void *call_site);其中第一个参数为当前函数的起始地址,第二个参数为返回地址,即caller函数中的地址。
这是什么意思呢?例如我们写了一个函数func_test(),定义如下:
static void func_test(v){ /* your code... */}那通过-finstrument-functions选项编译后,这个函数的定义就变成了:
static void func_test(v){ __cyg_profile_func_enter(this_fn, call_site); /* your code... */ __cyg_profile_func_exit(this_fn, call_site);}我们可以按照自己的需要去实现这两个hook函数,这样我们就可以利用this_fn和call_site这两个参数大做文章。
例如下面这段代码:
instrfunc.c: #include <stdio.h>#define DUMP(func, call) / printf("%s: func = %p, called by = %p/n", __FUNCTION__, func, call)void __attribute__((no_instrument_function))__cyg_profile_func_enter(void *this_func, void *call_site){ DUMP(this_func, call_site);}void __attribute__((no_instrument_function))__cyg_profile_func_exit(void *this_func, void *call_site){ DUMP(this_func, call_site);}int do_multi(int a, int b){ return a * b;}int do_calc(int a, int b){ return do_multi(a, b);}int main(){ int a = 4, b = 5; printf("result: %d/n", do_calc(a, b)); return 0;}这段代码中实现了两个hook函数,即打印出所在函数的函数地址以及返回地址。
编译代码:
[zhenfg@ubuntu]code:$ gcc -finstrument-functions instrfunc.c -o instrfunc[zhenfg@ubuntu]code:$ ./instrfunc __cyg_profile_func_enter: func = 0x8048521, called by = 0xb75554e3__cyg_profile_func_enter: func = 0x80484d8, called by = 0x8048562__cyg_profile_func_enter: func = 0x804849a, called by = 0x8048504__cyg_profile_func_exit: func = 0x804849a, called by = 0x8048504__cyg_profile_func_exit: func = 0x80484d8, called by = 0x8048562result: 20__cyg_profile_func_exit: func = 0x8048521, called by = 0xb75554e3通过反汇编的代码(objdump -D instrfunc)可以看到,这些地址和函数的对应关系为:
__cyg_profile_func_enter: func = 0x8048521(main), called by = 0xb75554e3__cyg_profile_func_enter: func = 0x80484d8(do_calc), called by = 0x8048562(main)__cyg_profile_func_enter: func = 0x804849a(do_multi), called by = 0x8048504(do_calc)__cyg_profile_func_exit: func = 0x804849a(do_multi), called by = 0x8048504(do_calc)__cyg_profile_func_exit: func = 0x80484d8(do_calc), called by = 0x8048562(main)result: 20__cyg_profile_func_exit: func = 0x8048521(main), called by = 0xb75554e3实际上这就给出了函数的调用关系。
如果不想跟踪某个函数,可以给该函数指定“no_instrument_function”属性。需要注意的是,__cyg_profile_func_enter()和__cyg_profile_func_exit()这两个hook函数是一定要加上“no_instrument_function”属性的,不然,自己 跟踪 自己就会无限循环导致程序崩溃,当然,也不能在这两个hook 函数 中调用其他需要被跟踪的函数。
得到一系列的地址看起来不太直观,我们更希望看到函数名,幸运的是,addr2line工具为我们提供了这种可能。我们先看一下addr2line的使用方法:
[zhenfg@ubuntu]code:$ addr2line --helpUsage: addr2line [option(s)] [addr(s)] Convert addresses into line number/file name pairs. If no addresses are specified on the command line, they will be read from stdin The options are: @<file> Read options from <file> -a --addresses Show addresses -b --target=<bfdname> Set the binary file format -e --exe=<executable> Set the input file name (default is a.out) -i --inlines Unwind inlined functions -j --section=<name> Read section-relative offsets instead of addresses -p --pretty-print Make the output easier to read for humans -s --basenames Strip directory names -f --functions Show function names -C --demangle[=style] Demangle function names -h --help Display this information -v --version Display the program's version首先要注意,使用addr2line工具时,需要用gcc的“-g”选项编译程序增加调试信息。
同样是上面的程序,我们加上-g选项再编译一次:
[zhenfg@ubuntu]code:$ gcc -g -finstrument-functions instrfunc.c -o instrfunc[zhenfg@ubuntu]code:$ ./instrfunc __cyg_profile_func_enter: func = 0x8048521, called by = 0xb757d4e3__cyg_profile_func_enter: func = 0x80484d8, called by = 0x8048562__cyg_profile_func_enter: func = 0x804849a, called by = 0x8048504__cyg_profile_func_exit: func = 0x804849a, called by = 0x8048504__cyg_profile_func_exit: func = 0x80484d8, called by = 0x8048562result: 20__cyg_profile_func_exit: func = 0x8048521, called by = 0xb757d4e3使用addr2line尝试查找0x8048504地址所在的函数:
[zhenfg@ubuntu]code:$ addr2line -e instrfunc -a 0x8048504 -fp -s0x08048504: do_calc at instrfunc.c:25
这样一来,就可以通过gcc的“-finstrument-functions”选项结合addr2
line
工具,方便的对一个程序中的
函数
进行跟踪。并且既然我们可以自己实现hook函数,那不仅仅可以用来跟踪函数调用关系,你可以在hook函数中添加自己想做的事情,例如添加一些统计信息。
另外,我们知道__builtin_return_address(level)宏可以获得不同层级的函数返回地址,但是在某些体系架构(如mips)中,__builtin_return_address(level)只能获得当前函数的直接调用者的地址,即level只能是0,那这时,就可使用上述方法来 跟踪 函数调用关系(mips中竟然能用,确实有些小吃惊)。
接下来可以看一下gcc是如何将hook函数嵌入各个函数中的,以反汇编代码中的do_multi()函数为例(这是mips的汇编代码),在mips中,ra 寄存器 用来 存储 返回地址,a0-a3用来做函数参数。
004006c8 <do_multi>: 4006c8: 27bdffd8 addiu sp,sp,-40 4006cc: afbf0024 sw ra,36(sp) ;;存储ra寄存器(返回地址)的值 4006d0: afbe0020 sw s8,32(sp) 4006d4: afb1001c sw s1,28(sp) 4006d8: afb00018 sw s0,24(sp) 4006dc: 03a0f021 move s8,sp 4006e0: 03e08021 move s0,ra ;;s0 = ra 4006e4: afc40028 sw a0,40(s8) 4006e8: afc5002c sw a1,44(s8) 4006ec: 02001021 move v0,s0 ;;v0 = s0 4006f0: 3c030040 lui v1,0x40 4006f4: 246406c8 addiu a0,v1,1736 ;;将本函数的地址赋值给a0寄存器 4006f8: 00402821 move a1,v0 ;;将返回地址ra的值赋值给a1寄存器 4006fc: 0c100188 jal 400620 <__cyg_profile_func_enter> ;;调用hook函数 400700: 00000000 nop 400704: 8fc30028 lw v1,40(s8) 400708: 8fc2002c lw v0,44(s8) 40070c: 00000000 nop 400710: 00620018 mult v1,v0 400714: 00008812 mflo s1 400718: 02001021 move v0,s0 40071c: 3c030040 lui v1,0x40 400720: 246406c8 addiu a0,v1,1736 ;;将本函数的地址赋值给a0寄存器 400724: 00402821 move a1,v0 ;;将返回地址ra的值赋值给a1寄存器 400728: 0c10019d jal 400674 <__cyg_profile_func_exit> ;;调用hook函数 40072c: 00000000 nop 400730: 02201021 move v0,s1 400734: 03c0e821 move sp,s8 400738: 8fbf0024 lw ra,36(sp) ;;恢复ra寄存器(返回地址)的值 40073c: 8fbe0020 lw s8,32(sp) 400740: 8fb1001c lw s1,28(sp) 400744: 8fb00018 lw s0,24(sp) 400748: 27bd0028 addiu sp,sp,40 40074c: 03e00008 jr ra 400750: 00000000 nop
上述反汇编的代码中,使用“-finstrument-functions”选项编译程序所增加的指令都已注释出来,实现没什么复杂的,在函数中获得自己的地址和上一级caller的地址并不是什么难事,然后将这两个地址传给__cyg_profile_func_enter和__cyg_profile_func_exit就好了。