代碼中打印C++調用堆棧

基本動因

有時在進行大型項目的開發時,我發現找出調用某些函數或方法的所有位置非常有用。而且,我不僅僅想要直接調用者,而是整個調用棧。這在兩個場景中最有用:

  • 在調試的時候
  • 試圖弄清楚某些代碼如何工作的時候,即學習源代碼的時候

一種可能的解決方案是使用調試器:在調試器中運行程序,在某個特定的地方放置斷點,在停止時檢查調用堆棧。雖然這種做法很有效並且有時非常有用,但我個人更喜歡代碼化的方法:即編寫一個專門用來打印調用堆棧的函數,以便在我感興趣的每個地方打印出調用堆棧。然後我可以使用grep和更復雜的工具來分析調用日誌,從而更好地理解某些代碼的工作原理。

在這篇文章中,我想提出一個相對簡單的方法來做到這一點。它主要針對Linux平臺,但應該在其他Unix(包括OS X)上進行少量修改也可以達到相同的效果。

利用libunwind庫來獲取調用堆棧

我目前知道有三種靠譜且普遍的編程的方法來獲取調用堆棧:

  1. gcc編譯器自帶的宏:__builtin_return_address:這是一種非常粗糙,底層的方式。這個宏將獲得堆棧上每個幀上函數的返回地址。 注意:只是地址,而不是函數名稱。 因此需要額外的處理來獲得函數名稱。
  2. glibc的backtrace和backtrace_symbols:可以獲取調用堆棧上函數的實際符號名稱。
  3. 使用libunwind。

在三者之間,我非常喜歡libunwind庫,因爲它是最時髦,最廣泛和最方便的解決方案。 它也比第二種方法的backtrace更靈活,可以夠提供額外的信息,例如每個堆棧幀的CPU的寄存器值。

此外,在系統編程中,libunwind是最接近你現在可以獲得的“官方詞彙”。 例如,gcc可以使用libunwind實現零成本的C++異常捕捉(當實際拋出異常時需要堆棧展開)[^1]。大名鼎鼎的LLVM還在libc++中重新實現了libunwind接口,該接口用於在基於此庫的LLVM工具鏈中展開調用堆棧。

代碼示例

這是一個使用libunwind庫從代碼中的任意點獲取調用回溯的完整的代碼示例。有關此處調用的API函數的更多詳細信息,請參閱libunwind Document

#define UNW_LOCAL_ONLY
#include <libunwind.h>
#include <stdio.h>

// Call this function to get a backtrace.
void backtrace() {
  unw_cursor_t cursor;
  unw_context_t context;

  // Initialize cursor to current frame for local unwinding.
  unw_getcontext(&context);
  unw_init_local(&cursor, &context);

  // Unwind frames one by one, going up the frame stack.
  while (unw_step(&cursor) > 0) {
    unw_word_t offset, pc;
    unw_get_reg(&cursor, UNW_REG_IP, &pc);
    if (pc == 0) {
      break;
    }
    printf("0x%lx:", pc);

    char sym[256];
    if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
      printf(" (%s+0x%lx)\n", sym, offset);
    } else {
      printf(" -- error: unable to obtain symbol name for this frame\n");
    }
  }
}

void foo() {
  backtrace(); // <-------- backtrace here!
}

void bar() {
  foo();
}

int main(int argc, char **argv) {
  bar();

  return 0;
}

libunwind很容易從源代碼,或者直接從二進制包來安裝。 我只是使用通常的configure,make和make install經典三部曲從源代碼構建它並將其放入/usr/local/lib。

一旦你在編譯器可以找到[2]的地方安裝libunwind,則編譯上面的代碼:

gcc -o libunwind_backtrace -Wall -g libunwind_backtrace.c -lunwind

最後運行:

$ LD_LIBRARY_PATH=/usr/local/lib ./libunwind_backtrace
0x400958: (foo+0xe)
0x400968: (bar+0xe)
0x400983: (main+0x19)
0x7f6046b99ec5: (__libc_start_main+0xf5)
0x400779: (_start+0x29)

因此,我們在調用backtrace的位置獲得完整的調用堆棧。 我們可以獲得函數符號名稱和調用指令的地址(更確切地說,返回地址是下一條指令)。

但是,有時我們不僅需要調用函數的名字,還需要調用函數的位置(即源文件名+行號)。 當一個函數從多個位置調用另一個函數並且我們想要確定哪一個是真正的給定調用堆棧的一部分時,這很有用。 libunwind爲我們提供了調用地址,但沒有任何內容。 幸運的是,它全部在二進制文件的DWARF信息中,並且給定地址我們可以通過多種方式提取確切的調用位置。 最簡單的可能是調用addr2line命令:

$ addr2line 0x400968 -e libunwind_backtrace
libunwind_backtrace.c:37

我們將函數bar這一幀左側的PC地址傳遞給addr2line並獲取文件名和行號。

或者,我們可以使用pyelftools中的dwarf_decode_address示例來獲取相同的信息:

$ python <path>/dwarf_decode_address.py 0x400968 libunwind_backtrace
Processing file: libunwind_backtrace
Function: bar
File: libunwind_backtrace.c
Line: 37

如果在backtrace調用期間打印出確切位置對你來說很重要,你還可以通過使用libdwarf打開可執行文件並在backtrace調用中從中讀取此信息來達到目的。

C++ 和 mangled function names

上面的代碼示例運行良好,但是現在C++比C代碼使用得更廣泛,因此這個方案存在一些問題。 在C++中,函數和方法的名稱被mangled了,也就是被混淆了。 這個功能對於函數重載,命名空間和模板等C++功能起到至關重要。 假設實際的調用順序是:

namespace ns {

template <typename T, typename U>
void foo(T t, U u) {
  backtrace(); // <-------- backtrace here!
}

}  // namespace ns

template <typename T>
struct Klass {
  T t;
  void bar() {
    ns::foo(t, true);
  }
};

int main(int argc, char** argv) {
  Klass<double> k;
  k.bar();

  return 0;
}

實際的調用堆棧是:

0x400b3d: (_ZN2ns3fooIdbEEvT_T0_+0x17)
0x400b24: (_ZN5KlassIdE3barEv+0x26)
0x400af6: (main+0x1b)
0x7fc02c0c4ec5: (__libc_start_main+0xf5)
0x4008b9: (_start+0x29)

看起來好像不是太美好。當然C++老鳥還是能夠從輕微混亂的名字中找到蛛絲馬跡(就像系統程序員能夠從ascill十六進制碼認出文本),當代碼大面積使用模板,函數名稱將變得更加不堪入目。

有一個方案是使用命令行工具c++filt:

$ c++filt _ZN2ns3fooIdbEEvT_T0_
void ns::foo<double, bool>(double, bool)

但是,如果我們的調用堆棧轉儲器直接打印出去未混淆的名稱會更好。 幸運的是,這很容易做到,使用cxxabi.h API函數,它是libstdc++的一部分(更確切地說,libsupc++)。 libc++還在底層libc++ abi中提供它。 我們需要做的就是調用abi::__cxa_demangle。 下面一個完整的例子:

#define UNW_LOCAL_ONLY
#include <cxxabi.h>
#include <libunwind.h>
#include <cstdio>
#include <cstdlib>

void backtrace() {
  unw_cursor_t cursor;
  unw_context_t context;

  // Initialize cursor to current frame for local unwinding.
  unw_getcontext(&context);
  unw_init_local(&cursor, &context);

  // Unwind frames one by one, going up the frame stack.
  while (unw_step(&cursor) > 0) {
    unw_word_t offset, pc;
    unw_get_reg(&cursor, UNW_REG_IP, &pc);
    if (pc == 0) {
      break;
    }
    std::printf("0x%lx:", pc);

    char sym[256];
    if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
      char* nameptr = sym;
      int status;
      char* demangled = abi::__cxa_demangle(sym, nullptr, nullptr, &status);
      if (status == 0) {
        nameptr = demangled;
      }
      std::printf(" (%s+0x%lx)\n", nameptr, offset);
      std::free(demangled);
    } else {
      std::printf(" -- error: unable to obtain symbol name for this frame\n");
    }
  }
}

namespace ns {

template <typename T, typename U>
void foo(T t, U u) {
  backtrace(); // <-------- backtrace here!
}

}  // namespace ns

template <typename T>
struct Klass {
  T t;
  void bar() {
    ns::foo(t, true);
  }
};

int main(int argc, char** argv) {
  Klass<double> k;
  k.bar();

  return 0;
}

這一次,所有未混淆的函數名稱全被打印出來了:

$ LD_LIBRARY_PATH=/usr/local/lib ./libunwind_backtrace_demangle
0x400b59: (void ns::foo<double, bool>(double, bool)+0x17)
0x400b40: (Klass<double>::bar()+0x26)
0x400b12: (main+0x1b)
0x7f6337475ec5: (__libc_start_main+0xf5)
0x4008b9: (_start+0x29)

參考

Programmatic access to the call stack in C++

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章