-
Notifications
You must be signed in to change notification settings - Fork 249
Integrate libbacktrace for enhanced stack trace resolution #7721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0b4eb13
7603d07
99569c1
df1e569
e2e11a4
701d732
ca8bc8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,10 +3,10 @@ | |
|
|
||
| #include "tasks/worker.h" | ||
|
|
||
| #include <backtrace.h> | ||
| #include <cstdlib> | ||
| #include <cxxabi.h> | ||
| #include <dlfcn.h> | ||
| #include <execinfo.h> | ||
| #include <memory> | ||
| #include <sstream> | ||
|
|
||
|
|
@@ -34,66 +34,129 @@ namespace ccf::tasks | |
| } | ||
| }; | ||
|
|
||
| struct FreePtrArrayDeleter | ||
| // Lazily initialise and return the process-wide backtrace_state. | ||
| // backtrace_create_state allocates resources that cannot be freed, so | ||
| // we call it at most once and cache the result. | ||
| backtrace_state* get_backtrace_state() | ||
| { | ||
| void operator()(char** p) const | ||
| static backtrace_state* state = backtrace_create_state( | ||
| nullptr, // let libbacktrace find the executable | ||
| 1, // threaded = true | ||
| nullptr, // ignore errors during init | ||
| nullptr); | ||
| return state; | ||
| } | ||
|
|
||
| // Demangle a C++ mangled symbol name. Returns the original string | ||
| // unchanged if demangling fails. | ||
| std::string demangle(const char* name) | ||
| { | ||
| if (name == nullptr) | ||
| { | ||
| // NOLINTNEXTLINE(cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory,bugprone-multi-level-implicit-pointer-conversion) | ||
| free(p); | ||
| return "<unknown>"; | ||
| } | ||
|
|
||
| int status = 0; | ||
| std::unique_ptr<char, FreeDeleter> demangled( | ||
| abi::__cxa_demangle(name, nullptr, nullptr, &status)); | ||
| if (status == 0 && demangled != nullptr) | ||
| { | ||
| return demangled.get(); | ||
| } | ||
| return name; | ||
| } | ||
|
|
||
| // Data passed through libbacktrace callbacks to build up each frame's | ||
| // description. | ||
| struct PcinfoResult | ||
| { | ||
| bool resolved = false; | ||
| std::string function; | ||
| std::string filename; | ||
| int lineno = 0; | ||
| }; | ||
|
|
||
| std::string demangle_symbol(const char* raw) | ||
| // Called by backtrace_pcinfo for each source location (may be called | ||
| // multiple times per PC when inlined calls are present). | ||
| int pcinfo_callback( | ||
| void* data, | ||
| uintptr_t /*pc*/, | ||
| const char* filename, | ||
| int lineno, | ||
| const char* function) | ||
| { | ||
| // backtrace_symbols format: "binary(mangled+0xoffset) [0xaddr]" | ||
| // Try to extract and demangle the symbol name between '(' and '+'/')' | ||
| std::string entry(raw); | ||
| auto open = entry.find('('); | ||
| auto plus = entry.find('+', open != std::string::npos ? open : 0); | ||
| auto close = entry.find(')', open != std::string::npos ? open : 0); | ||
|
|
||
| if ( | ||
| open != std::string::npos && close != std::string::npos && | ||
| close > open + 1) | ||
| auto* result = static_cast<PcinfoResult*>(data); | ||
| if (function != nullptr) | ||
| { | ||
| auto end = (plus != std::string::npos && plus < close) ? plus : close; | ||
| std::string mangled = entry.substr(open + 1, end - open - 1); | ||
|
|
||
| if (!mangled.empty()) | ||
| { | ||
| int status = 0; | ||
| std::unique_ptr<char, FreeDeleter> demangled( | ||
| abi::__cxa_demangle(mangled.c_str(), nullptr, nullptr, &status)); | ||
| if (status == 0 && demangled != nullptr) | ||
| { | ||
| std::string rest = entry.substr(end); | ||
| entry = entry.substr(0, open + 1) + demangled.get() + rest; | ||
| } | ||
| } | ||
| result->resolved = true; | ||
| result->function = demangle(function); | ||
| result->filename = (filename != nullptr) ? filename : ""; | ||
| result->lineno = lineno; | ||
| } | ||
|
Comment on lines
+81
to
95
|
||
| return 0; // continue | ||
| } | ||
|
|
||
| return entry; | ||
| // Called by backtrace_syminfo when DWARF info is unavailable but the | ||
| // dynamic symbol table has an entry. | ||
| void syminfo_callback( | ||
| void* data, | ||
| uintptr_t /*pc*/, | ||
| const char* symname, | ||
| uintptr_t /*symval*/, | ||
| uintptr_t /*symsize*/) | ||
| { | ||
| auto* result = static_cast<PcinfoResult*>(data); | ||
| if (symname != nullptr) | ||
| { | ||
| result->resolved = true; | ||
| result->function = demangle(symname); | ||
| } | ||
| } | ||
|
|
||
| // Format a demangled stack trace as a string. Note: backtrace_symbols only | ||
| // resolves symbols exported to the dynamic symbol table (e.g. via | ||
| // -rdynamic). Static/internal functions will appear as raw addresses. For | ||
| // broader coverage, consider integrating libbacktrace (reads DWARF | ||
| // directly) or invoking addr2line at runtime. | ||
| // Silently ignore libbacktrace errors in individual frame resolution — | ||
| // we fall back to printing the raw PC address. | ||
| void error_callback(void* /*data*/, const char* /*msg*/, int /*errnum*/) {} | ||
|
|
||
| // Format a stack trace using libbacktrace for DWARF-aware symbol, | ||
| // file and line resolution. This works in all build configurations | ||
| // without requiring -rdynamic. | ||
| std::string format_stacktrace(void** frames, int num_frames) | ||
achamayou marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| std::ostringstream oss; | ||
| // NOLINTNEXTLINE(cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory,bugprone-multi-level-implicit-pointer-conversion) | ||
| std::unique_ptr<char*, FreePtrArrayDeleter> symbols( | ||
| backtrace_symbols(frames, num_frames)); | ||
| if (symbols == nullptr) | ||
| { | ||
| // If memory allocation fails, return a message indicating the issue | ||
| return " (failed to allocate memory for backtrace symbols)\n"; | ||
| } | ||
| auto* state = get_backtrace_state(); | ||
|
|
||
| for (int i = 0; i < num_frames; ++i) | ||
| { | ||
| oss << " #" << i << ": " << demangle_symbol(symbols.get()[i]) << "\n"; | ||
| auto pc = reinterpret_cast<uintptr_t>(frames[i]); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this? |
||
| PcinfoResult result; | ||
|
|
||
| if (state != nullptr) | ||
| { | ||
| // Try DWARF-based resolution first (gives file + line + function) | ||
| backtrace_pcinfo(state, pc, pcinfo_callback, error_callback, &result); | ||
|
|
||
| // If DWARF info wasn't available, try the symbol table | ||
| if (!result.resolved) | ||
| { | ||
| backtrace_syminfo( | ||
| state, pc, syminfo_callback, error_callback, &result); | ||
| } | ||
| } | ||
|
|
||
| oss << " #" << i << ": "; | ||
| if (result.resolved) | ||
| { | ||
| oss << result.function; | ||
| if (!result.filename.empty()) | ||
| { | ||
| oss << " at " << result.filename << ":" << result.lineno; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| oss << "0x" << std::hex << pc << std::dec; | ||
| } | ||
| oss << "\n"; | ||
| } | ||
| return oss.str(); | ||
| } | ||
|
|
@@ -131,10 +194,29 @@ extern "C" | |
| void __cxa_throw( | ||
| void* thrown_exception, std::type_info* tinfo, void (*dest)(void*)) | ||
| { | ||
| // Capture the backtrace at the throw site | ||
| // Capture the backtrace at the throw site using libbacktrace's own | ||
| // unwinder. glibc backtrace() can lose frames whose return address | ||
| // falls exactly at the end of a .eh_frame FDE range; libbacktrace's | ||
| // DWARF unwinder handles this correctly. | ||
| auto& trace = ccf::tasks::current_throw_trace; | ||
| trace.num_frames = | ||
| backtrace(trace.frames, ccf::tasks::throw_trace_max_frames); | ||
| trace.num_frames = 0; | ||
| auto* bt_state = ccf::tasks::get_backtrace_state(); | ||
| if (bt_state != nullptr) | ||
| { | ||
| backtrace_simple( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we backtrace_full() depending on NDEBUG? It's not obvious from the doc to what extent full is nicer than simple, but it sounds like it might be? |
||
| bt_state, | ||
| 0, // skip = 0, capture from here | ||
| [](void* data, uintptr_t pc) -> int { | ||
| auto* t = static_cast<ccf::tasks::ThrowTrace*>(data); | ||
| if (t->num_frames < ccf::tasks::throw_trace_max_frames) | ||
| { | ||
| t->frames[t->num_frames++] = reinterpret_cast<void*>(pc); // NOLINT | ||
| } | ||
| return 0; | ||
| }, | ||
|
Comment on lines
+206
to
+216
|
||
| nullptr, // ignore errors | ||
| &trace); | ||
| } | ||
|
Comment on lines
201
to
+219
|
||
|
|
||
| // Forward to the real __cxa_throw | ||
| static auto real_cxa_throw = | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not omitting frame pointers still seems useful?