C++的全鏈路追蹤方案,稍微有點高端
背景:本人主要在做C++ SDK的開發,需要給到業務端去集成,在集成的過程中可能會出現某些功能性bug,即沒有得到想要的結果。那怎么調試?
分析:這種問題其實調試起來稍微有點困難,它不像crash,當發生crash時還能拿到堆棧信息去分析,然而功能性bug沒有crash,也就沒法捕捉對應到當時的堆棧信息。因為不是在本地,也沒法用編譯器debug。那思路就剩log了,一種方式是考慮在SDK內部的關鍵路徑下打印詳細的log,當出現問題時拿到log去分析。然而總有漏的時候,誰能保證log一定打的很全面,很有可能問題就出現在沒有log的函數中。
解決:基于上面的背景和問題分析,考慮是否能做一個全鏈路追蹤的方案,把打印出整個SDK的調用路徑,從哪個函數進入,從哪個函數退出等。
想法1:可以考慮在SDK的每個接口都加一個context結構體參數,記錄下來函數的調用路徑,這可能是比較通用有效的方案,但是SDK接口已經固定了,更改接口要面臨的困難很大,業務端基本不會同意,所以這種方案不適合我們現有情況,當然一個從0開始建設的中間件和SDK可以考慮考慮。
想法2:有沒有一種不用改接口,還能追蹤到函數調用路徑的方案?
繼續沿著這個思路繼續調研,我找到了gcc和clang編譯器的一個編譯參數:-finstrument-functions,編譯時添加此參數會在函數的入口和出口處觸發一個固定的回調函數,即:
- __cyg_profile_func_enter(void *callee, void *caller);
- __cyg_profile_func_exit(void *callee, void *caller);
參數就是callee和caller的地址,那怎么將地址解析成對應函數名?可以使用dladdr函數:
- int dladdr(const void *addr, Dl_info *info);
看下下面的代碼:
- // tracing.cc
- #include <cxxabi.h>
- #include <dlfcn.h> // for dladdr
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #ifndef NO_INSTRUMENT
- #define NO_INSTRUMENT __attribute__((no_instrument_function))
- #endif
- extern "C" __attribute__((no_instrument_function)) void __cyg_profile_func_enter(void *callee, void *caller) {
- Dl_info info;
- if (dladdr(callee, &info)) {
- int status;
- const char *name;
- char *demangled = abi::__cxa_demangle(info.dli_sname, NULL, 0, &status);
- if (status == 0) {
- name = demangled ? demangled : "[not demangled]";
- } else {
- name = info.dli_sname ? info.dli_sname : "[no dli_sname nd std]";
- }
- printf("enter %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_info info;
- if (dladdr(callee, &info)) {
- int status;
- const char *name;
- char *demangled = abi::__cxa_demangle(info.dli_sname, NULL, 0, &status);
- if (status == 0) {
- name = demangled ? demangled : "[not demangled]";
- } else {
- name = info.dli_sname ? info.dli_sname : "[no dli_sname and std]";
- }
- printf("exit %s (%s)\n", name, info.dli_fname);
- if (demangled) {
- free((void *)demangled);
- demangled = NULL;
- }
- }
- }
這是測試文件:
- // test_trace.cc
- void func1() {}
- void func() { func1(); }
- int main() { func(); }
- 將test_trace.cc和tracing.cc文件同時編譯鏈接,即可達到鏈路追蹤的目的:
- g++ test_trace.cc tracing.cc -std=c++14 -finstrument-functions -rdynamic -ldl;./a.out
- 輸出:enter main (./a.out)
- enter func() (./a.out)
- enter func1() (./a.out)
- exit func1() (./a.out)
- exit func() (./a.out)
- exit main (./a.out)
如果在func()中調用了一些其他的函數呢?
- #include <iostream>
- #include <vector>
- void func1() {}
- void func() {
- std::vector<int> v{1, 2, 3};
- std::cout << v.size();
- func1();
- }
- int main() { func(); }
再重新編譯后輸出會是這樣:
- enter [no dli_sname nd std] (./a.out)
- enter [no dli_sname nd std] (./a.out)
- exit [no dli_sname and std] (./a.out)
- exit [no dli_sname and std] (./a.out)
- enter main (./a.out)
- enter func() (./a.out)
- enter std::allocator<int>::allocator() (./a.out)
- enter __gnu_cxx::new_allocator<int>::new_allocator() (./a.out)
- exit __gnu_cxx::new_allocator<int>::new_allocator() (./a.out)
- exit std::allocator<int>::allocator() (./a.out)
- enter std::vector<int, std::allocator<int> >::vector(std::initializer_list<int>, std::allocator<int> const&) (./a.out)
- enter std::_Vector_base<int, std::allocator<int> >::_Vector_base(std::allocator<int> const&) (./a.out)
- enter std::_Vector_base<int, std::allocator<int> >::_Vector_impl::_Vector_impl(std::allocator<int> const&) (./a.out)
- enter std::allocator<int>::allocator(std::allocator<int> const&) (./a.out)
- enter __gnu_cxx::new_allocator<int>::new_allocator(__gnu_cxx::new_allocator<int> const&) (./a.out)
- exit __gnu_cxx::new_allocator<int>::new_allocator(__gnu_cxx::new_allocator<int> const&) (./a.out)
- exit std::allocator<int>::allocator(std::allocator<int> const&) (./a.out)
- exit std::_Vector_base<int, std::allocator<int> >::_Vector_impl::_Vector_impl(std::allocator<int> const&) (./a.out)
- exit std::_Vector_base<int, std::allocator<int> >::_Vector_base(std::allocator<int> const&) (./a.out)
上面我只貼出了部分信息,這顯然不是我們想要的,我們只想要顯示自定義的函數調用路徑,其他的都想要過濾掉,怎么辦?
這里可以將自定義的函數都加一個統一的前綴,在打印時只打印含有前綴的符號,這種個人認為是比較通用的方案。
下面是我過濾掉std和gnu子串的代碼:
- if (!strcasestr(name, "std") && !strcasestr(name, "gnu")) {
- printf("enter %s (%s)\n", name, info.dli_fname);
- }
- if (!strcasestr(name, "std") && !strcasestr(name, "gnu")) {
- printf("exit %s (%s)\n", name, info.dli_fname);
- }
重新編譯后就會輸出我想要的結果:
- g++ test_trace.cc tracing.cc -std=c++14 -finstrument-functions -rdynamic -ldl;./a.out
- 輸出:enter main (./a.out)
- enter func() (./a.out)
- enter func1() (./a.out)
- exit func1() (./a.out)
- exit func() (./a.out)
- exit main (./a.out)
還有一種方式是在編譯時使用下面的參數:
- -finstrument-functions-exclude-file-list
它可以排除不想要做trace的文件,但是這個參數只在gcc中可用,在clang中卻不支持 ,所以上面的字符串過濾方式更通用一些。
上面只能拿到函數的名字,不能定位到具體的文件和行號,如果想要獲得更多信息,需要結合bfd系列參數(bfd_find_nearest_line)和libunwind一起使用,大家可以繼續研究。。。
tips1:這是一篇拋磚引玉的文章,本人不是后端開發,據我所知后端C++中有很多成熟的trace方案,大家有更好的方案可以留言,分享一波。
tips2:上面的方案可以達到鏈路追蹤的目的,但本人最后沒有應用到項目中,因為本人在做的項目對性能要求較高,使用此種方案會使整個SDK性能下降嚴重,無法滿足需求正常運行。于是暫時放棄了鏈路追蹤的這個想法。
本文的知識點還是值得了解一下的,大家或許會用得到。在研究的過程中我也發現了一個基于此種方案的開源項目(call-stack-logger),感興趣的也可以去了解了解。