C,你的函数过程全被我看见了!

文摘   科技互联网   2024-07-08 09:21   广东  
有天老板让我参与分析一个比较棘手的问题,问题不但不好复现,而且涉及到的函数调用非常错综复杂(就像屎山里那堆东西那样)。一整天没有很好的进展,渐渐地对着这堆屎山发起呆来,隐约中似乎被一股气息刺激到了一根神经,在想——是否存在一种技术可以记录C语言函数所有的集成过程?
这个问题后面花了很长时间很大精力被找到了。时间过去很久了,但是那个“记录C语言函数调用全过程”这个方法,我一直都没找到。
在每个函数里都打印个log吧,十个八个函数其实手动添加进去也没问题,可以像这样,不用的时候,还可以将宏定义定义成空:
#if FUNC_RECORD_USE    #define FUNC_RECORD()  printf("Enter function <s%> \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 <stdio.h>
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("<Enter> this: %p, call: %p\n", this, call);} void __attribute__((no_instrument_function)) __cyg_profile_func_exit(void *this, void *call){ printf("<Exit> 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<Enter> this: 0x100401184, call: 0x7ffc95ee80c1<Enter> this: 0x100401080, call: 0x1004011b9<Exit>  this: 0x100401080, call: 0x1004011b9<Enter> this: 0x1004010cf, call: 0x1004011c5<Exit>  this: 0x1004010cf, call: 0x1004011c5max=456<Exit>  this: 0x100401184, call: 0x7ffc95ee80c1
呃?这怎么看呢?
将函数地址转换成函数名字,还得借用一个工具——addr2line,像这样:
$ addr2line -e main -a 0x100401080 -fps0x0000000100401080: add at main.c:4
也就是说0x100401080表示main.c4行的add函数。

另外,以上命令行中的-e后接要查地址的可执行程序,案例就是编译后生成的main,-a后接要查询的地址了。而-fps呢?嘿嘿,我讲你也记不住,还不如自己逐个去掉它试试。

好了,以上的输出转换成函数后就是这样的了

<Enter> this: main, call: ??<Enter> this: add, call: main<Exit>  this: add, call: add<Enter> this: max, call: main<Exit>  this: max, call: mainmax=456<Exit>  this: main, call: ??

对着上面的函数源码看,这下子清晰了吧。

不知你有没有发现,我上面的案例源码中有两个__attribute__((no_instrument_function)),这表示告诉编译器这两个函数不要记录那个entry和exit的调用。

好了,完了吗?

还没,因为每个地址都靠addr2line一个个查,着实很麻烦。当然,程序员很喜欢写脚本的,写个shell或者python脚本遍历一遍这些地址不就完了么。

是的,领导给你指导解决问题的时候也许是这么认为的。但是当你要搞一堆很复杂的函数调用的时候,你会发现更深层次的问题。例如发现这玩意在Windows上(Windows上的Cygwin)非常慢,并不能麻烦完成领导给你的任务。

我测试了十几万行的log地址转换的时候,等得快要崩溃了!我换到了Linux上搞,好了那么一点,但还是很慢啊!我要的是秒上转换!

我突然又想到了一个办法,就是通过MAP文件查找。后续打算在我分析AUTOSAR BSW组件的时候详细介绍这个过程,以及分享这个脚本给大家。

这么好玩,你不打算试试吗?

这文章你扔进收藏夹也是吃灰的,不如点击个转发、在看和赞!


嵌入式软件实战派
专注嵌入式软件开发领域知识传授,包括C语言精粹,RTOS原理与使用,MCU驱动开发,AUTOSAR搭建,软件架构方法设计等