电子产业一站式赋能平台

PCB联盟网

搜索
查看: 141|回复: 0
收起左侧

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

[复制链接]

481

主题

481

帖子

2845

积分

三级会员

Rank: 3Rank: 3

积分
2845
发表于 2024-7-9 11:38:00 | 显示全部楼层 |阅读模式
有天老板让我参与分析一个比较棘手的问题,问题不但不好复现,而且涉及到的函数调用非常错综复杂(就像屎山里那堆东西那样)。一整天没有很好的进展,渐渐地对着这堆屎山发起呆来,隐约中似乎被一股气息刺激到了一根神经,在想——是否存在一种技术可以记录C语言函数所有的集成过程?这个问题后面花了很长时间很大精力被找到了。时间过去很久了,但是那个“记录C语言函数调用全过程”这个方法,我一直都没找到。在每个函数里都打印个log吧,十个八个函数其实手动添加进去也没问题,可以像这样,不用的时候,还可以将宏定义定义成空:
  • #if FUNC_RECORD_USE    #define FUNC_RECORD()  printf("Enter function  
    ", __FUNCTION__)#else    #define FUNC_RECORD()#endif
    void func_example(void){    FUNC_RECORD();    // ...}但是对于有成千上万函数的项目呢,特别是出问题的时候,也不好操作。后来,我想研究AUTOSAR中那些BSW组件的函数行为,于是在网上使劲地挖呀挖,突然发现了GCC有这玩意——-finstrument-functions!官网原始描述是这样的

    4jz2dl4l45q64012976808.png

    4jz2dl4l45q64012976808.png

    (截图内容来源于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
    ", this, call);} void __attribute__((no_instrument_function)) __cyg_profile_func_exit(void *this, void *call){    printf("  this: %p, call: %p
    ", this, call);}
    int main(void){    printf("max=%d
    ", 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行的add函数。另外,以上命令行中的-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组件的时候详细介绍这个过程,以及分享这个脚本给大家。
    这么好玩,你不打算试试吗?这文章你扔进收藏夹也是吃灰的,不如点击个转发、在看和赞!
    猜你喜欢:
    常用的 Git 提交规范!
    分享一种灵活性很高的协议格式(附代码例子)
    易懂 | 手把手教你编写你的第一个上位机
  • 回复

    使用道具 举报

    发表回复

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则


    联系客服 关注微信 下载APP 返回顶部 返回列表