专栏名称: 嵌入式微处理器
关注这个时代最火的嵌入式微处理器,你想知道的都在这里。
目录
相关文章推荐
金城江悠然网  ·  “比价神器”上线,买药必看! ·  3 天前  
金城江悠然网  ·  “比价神器”上线,买药必看! ·  3 天前  
51好读  ›  专栏  ›  嵌入式微处理器

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

嵌入式微处理器  · 公众号  ·  · 2024-07-10 14:49

正文

有天老板让我参与分析一个比较棘手的问题,问题不但不好复现,而且涉及到的函数调用非常错综复杂(就像屎山里那堆东西那样)。一整天没有很好的进展,渐渐地对着这堆屎山发起呆来,隐约中似乎被一股气息刺激到了一根神经,在想——是否存在一种技术可以记录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: 0x1004011c5max=456  this: 0x100401184, call: 0x7ffc95ee80c1

呃?这怎么看呢?

将函数地址转换成函数名字,还得借用一个工具——addr2line,像这样:
$ addr2line -e main -a 0x100401080 -fps0x0000000100401080: 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: mainmax=456  this: main, call: ??
对着上面的函数源码看,这下子清晰了吧。

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

好了,完了吗?

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

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

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

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

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







请到「今天看啥」查看全文