当前位置: 首页 > 科技观察

C++全链路跟踪方案有点高端

时间:2023-03-12 22:27:27 科技观察

背景:本人主要做C++SDK开发,需要在业务端集成。集成过程中可能会出现一些功能性的bug,也就是想要的结果。那怎么调试呢?分析:这种问题其实调试起来有点难度。这不像是崩溃。当发生崩溃时,可以获取堆栈信息进行分析。堆栈信息。因为不是本地的,没办法用编译器调试。这个想法是记录。一种方式是考虑在SDK内部的关键路径下打印详细的日志,出现问题时获取日志进行分析。但是,老是有漏洞的时候,谁能保证日志一定很全,没有日志的情况下很有可能问题出在了函数上。解决方案:基于以上背景和问题分析,考虑是否可以做一个全链路跟踪方案,打印出整个SDK的调用路径,从哪个函数进入,从哪个函数退出等。思路一:可以考虑在SDK的每个接口中添加一个context结构体参数来记录函数的调用路径。这可能是一个更通用、更有效的解决方案,但是SDK接口已经固定了,想要更改接口非常困难,业务方基本不会同意,所以这个方案不适合我们现在的情况。当然也可以考虑一个从0开始的中间件和SDK。思路二:有没有在不改变接口的情况下跟踪函数调用路径的方案?沿着这个思路继续继续研究,发现了gcc和clang编译器的一个编译参数:-finstrument-functions,在编译时加入这个参数,会在函数的入口和出口处触发一个固定的回调函数,即:__cyg_profile_func_enter(void*callee,void*caller);__cyg_profile_func_exit(void*callee,void*caller);参数是callee和caller的地址,那么如何将地址解析成对应的函数名呢?您可以使用dladdr函数:intdladdr(constvoid*addr,Dl_info*info);看下面的代码://tracing.cc#include#include//fordladdr#include#include#include#ifndefNO_INSTRUMENT#defineNO_INSTRUMENT__attribute__((no_instrument_function))#endifextern"C"__attribute__((no_instrument_function))void__vocyg_profile_func_entvoid*caller){Dl_infoinfo;if(dladdr(callee,&info)){intstatus;constchar*name;char*demangled=abi::__cxa_demangle(info.dli_sname,NULL,0,&status);if(status==0){name=demangled?demangled:[notdemangled]";}else{name=info.dli_sname?info.dli_sname:"[nodli_snamendstd]";}printf("输入%s(%s)\n",name,info.dli_fname);if(demangled){free(demangled);demangled=NULL;}}}extern"C"__attribute__((no_instrument_function))void__cyg_profile_func_exit(void*callee,void*caller){Dl_infoinfo;if(dladdr(callee,&info)){intstatus;constchar*name;char*demangled=abi::__cxa_demangle(info.dli_sname,NULL,0,&status);if(status==0){name=demangled?demangled:"[notdemangled]";}else{name=info.dli_sname?info.dli_sname:[nodli_snameandstd]";}printf("exit%s(%s)\n",name,info.dli_fname);if(demangled){free((void*)demangled);demangled=NULL;}}}这里是测试文件://test_trace.ccvoidfunc1(){}voidfunc(){func1();}intmain(){func();同时编译链接test_trace.cc和tracing.cc文件,达到链接跟踪的目的:g++test_trace.cctracing.cc-std=c++14-finstrument-functions-rdynamic-ldl;。/A。输出输出:entermain(./a.out)enterfunc()(./a.out)enterfunc1()(./a.out)exitfunc1()(./a.out)exitfunc()(./a.out)exitmain(./a.out)如果在func()中调用了一些其他函数怎么办?#include#includevoidfunc1(){}voidfunc(){std::vectorv{1,2,3};std::cout<::allocator()(./a.out)enter__gnu_cxx::new_allocator::new_allocator()(./a.out)exit__gnu_cxx::new_allocator::new_allocator()(./a.out)exitstd::allocator::allocator()(./a.out)enterstd::vector>::vector(std::initializer_list,std::allocatorconst&)(./a.out)enterstd::_Vector_base>::_Vector_base(std::allocatorconst&)(./a.out)enterstd::_Vector_base>::_Vector_impl::_Vector_impl(std::allocatorconst&)(./a.out)enterstd::allocator::allocator(std::allocatorconst&)(./a.out)enter__gnu_cxx::new_allocator::new_allocator(__gnu_cxx::new_allocatorconst&)(./a.out)exit__gnu_cxx::new_allocator::new_allocator(__gnu_cxx::new_allocatorconst&)(./a.out)exitstd::allocator::allocator(std::allocatorconst&)(./a.out)exitstd::_Vector_base>::_Vector_impl::_Vector_impl(std::allocatorconst&)(./a.out)exitstd::_Vector_base>::_Vector_base(std::allocatorconst&)(./a.out)上面我只贴了一部分信息,这显然不是我们想要的。我们只想显示自定义的函数调用路径,其他都想过滤掉怎么办?这里可以为所有的自定义函数添加一个统一的前缀,打印时只打印带有前缀的符号。这个个人认为是比较通用的解决方案。这是我过滤掉std和gnu子串的代码:,info.dli_fname);}if(!strcasetr(name,"std")&&!strcasetr(name,"gnu")){printf("exit%s(%s)\n",name,info.dli_fname);}重新编译后会输出我想要的结果:g++test_trace.cctracing.cc-std=c++14-finstrument-functions-rdynamic-ldl;./a.outoutput:entermain(./a.out)enterfunc()(./a.out)enterfunc1()(./a.out)exitfunc1()(./a.out)exitfunc()(./a.out)exitmain(./a.out)out)另一种方法是在编译时使用以下参数:-finstrument-functions-exclude-file-list可以排除不想被跟踪的文件,但是这个参数只在gcc中有,在clang中没有,所以上面的字符串过滤方法比较通用。上面只能获取到函数名,不能定位到具体的文件和行号。如果想获取更多信息,需要结合libunwind的bfd系列参数(bfd_find_nearest_line)。你可以继续学习。..tips1:这是一篇介绍性文章。我不是后端开发人员。据我所知,后端C++有很多成熟的trace方案。如果你有更好的解决方案,欢迎留言分享。tips2:上面的方案可以达到链接跟踪的目的,但是我最终没有应用到项目中,因为我做的项目对性能要求比较高。使用该方案会严重降低整个SDK的性能,无法满足正常需求。跑步。所以我暂时放弃了链接追踪的想法。这篇文章的知识点还是值得理解的,大家可能会用到。在研究的过程中,我还发现了一个基于该方案的开源项目(call-stack-logger)。如果你有兴趣,你也可以了解一下。