有天老板让我参与分析一个比较棘手的问题,问题不但不好复现,而且涉及到的函数调用非常错综复杂(就像屎山里那堆东西那样)。一整天没有很好的进展,渐渐地对着这堆屎山发起呆来,隐约中似乎被一股气息刺激到了一根神经,在想——是否存在一种技术可以记录C语言函数所有的集成过程?
这个问题后面花了很长时间很大精力被找到了。时间过去很久了,但那个“记录C语言函数调用全过程”这个方法,我一直都没找到。
在每个函数里都打印个log吧,十个八个函数其实手动添加进去也没问题,可以像这样,不用的时候,还可以将宏定义定义成空:
#if FUNC_RECORD_USE
#define FUNC_RECORD() printf("Enter function \n", __FUNCTION__)
#else
#define FUNC_RECORD()
#endif
void func_example(void)
{
FUNC_RECORD();
}
但是,对于有成千上万函数的项目呢,特别是出问题的时候,也不好操作。
后来,我想研究AUTOSAR中那些BSW组件的函数行为,于是在网上使劲地挖呀挖,突然发现了GCC有这玩意——
-finstrument-functions
!
(截图内容来源于https://gcc.gnu.org/)
也就是说,在gcc编译的命令里添加
-finstrument-functions
这个选项,就会产生函数entry和exit时的指令调用,即对应以下两个函数。
void __cyg_profile_func_enter (void *this_fn, void *call_site);
void __cyg_profile_func_exit (void *this_fn, void *call_site);
其中,参数
this_fn
表示当前进入的函数地址,
call_site
表示调用该函数的那个函数的地址。
#include
int add(int a, int b)
{
return a+b;
}
int max(int a, int b)
{
return a>b? a:b;
}
void __attribute__((no_instrument_function)) __cyg_profile_func_enter(void *this, void *call)
{
printf(" this: %p, call: %p\n", this, call);
}
void __attribute__((no_instrument_function)) __cyg_profile_func_exit(void *this, void *call)
{
printf(" this: %p, call: %p\n", this, call);
}
int main(void)
{
printf("max=%d\n", max(add(123,321),456));
return 0;
}
接下来,命令行输入
gcc -finstrument-functions -g main.c -o main
,回车即可。
$ ./main
this: 0x100401184, call: 0x7ffc95ee80c1
this: 0x100401080, call: 0x1004011b9
this: 0x100401080, call: 0x1004011b9
this: 0x1004010cf, call: 0x1004011c5
this: 0x1004010cf, call: 0x1004011c5
max=456
this: 0x100401184, call: 0x7ffc95ee80c1
呃?这怎么看呢?
将函数地址转换成函数名字,还得借用一个工具——addr2line,像这样:
$ addr2line -e main -a 0x100401080 -fps
0x0000000100401080: add at main.c:4
也就是说
0x100401080
表示
main.c
第
4
行的
ad
d
函数。
另外,以上命令行中的-e后接要查地址的可执行程序,案例就是编译后生成的main,-a后接要查询的地址了。而
-fps
呢?嘿嘿,我讲你也记不住,还不如自己逐个去掉它试试。
好了,以上的输出转换成函数后就是这样的了:
this: main, call: ??
this: add, call: main
this: add, call: add
this: max, call: main
this: max, call: main
max=456
this: main, call: ??
不知你有没有发现,我上面的案例源码中有两个
__attribute__((no_instrument_function))
,这表示告诉编译器这两个函数不要记录那个entry和exit的调用。
好了,完了吗?
还没,因为每个地址都靠
addr2line
一个个查,着实很麻烦。当然,程序员很喜欢写脚本的,写个shell或者python脚本遍历一遍这些地址不就完了么。
是的,领导给你指导解决问题的时候也许是这么认为的。但是当你要搞一堆很复杂的函数调用的时候,你会发现更深层次的问题。例如发现这玩意在Windows上(Windows上的Cygwin)非常慢,并不能麻烦完成领导给你的任务。
我测试了十几万行的log地址转换的时候,等得快要崩溃了!我换到了Linux上搞,好了那么一点,但还是很慢啊!我要的是秒上转换!
我突然又想到了一个办法,就是通过MAP文件查找。后续打算在我分析AUTOSAR BSW组件的时候详细介绍这个过程,以及分享这个脚本给大家。
这么好玩,你不打算试试吗?