diff --git a/CMakeLists.txt b/CMakeLists.txt index 3abf1c5..f793edb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ option(CPPTRACE_GET_SYMBOLS_WITH_NOTHING "" OFF) option(CPPTRACE_UNWIND_WITH_UNWIND "" OFF) option(CPPTRACE_UNWIND_WITH_EXECINFO "" OFF) option(CPPTRACE_UNWIND_WITH_WINAPI "" OFF) +option(CPPTRACE_UNWIND_WITH_DBGHELP "" OFF) option(CPPTRACE_UNWIND_WITH_NOTHING "" OFF) option(CPPTRACE_DEMANGLE_WITH_CXXABI "" OFF) @@ -123,6 +124,7 @@ if( CPPTRACE_UNWIND_WITH_UNWIND OR CPPTRACE_UNWIND_WITH_EXECINFO OR CPPTRACE_UNWIND_WITH_WINAPI OR + CPPTRACE_UNWIND_WITH_DBGHELP OR CPPTRACE_UNWIND_WITH_NOTHING ) ) @@ -150,7 +152,7 @@ if( message(STATUS "Cpptrace auto config: Using winapi for unwinding") endif() elseif(WIN32) - set(CPPTRACE_UNWIND_WITH_WINAPI On) + set(CPPTRACE_UNWIND_WITH_DBGHELP On) message(STATUS "Cpptrace auto config: Using winapi for unwinding") endif() else() @@ -228,6 +230,7 @@ set( src/unwind/unwind_with_nothing.cpp src/unwind/unwind_with_unwind.cpp src/unwind/unwind_with_winapi.cpp + src/unwind/unwind_with_dbghelp.cpp ) if(CPPTRACE_STATIC) @@ -352,6 +355,11 @@ if(CPPTRACE_UNWIND_WITH_WINAPI) target_compile_definitions(cpptrace PUBLIC CPPTRACE_UNWIND_WITH_WINAPI) endif() +if(CPPTRACE_UNWIND_WITH_DBGHELP) + target_compile_definitions(cpptrace PUBLIC CPPTRACE_UNWIND_WITH_DBGHELP) + target_link_libraries(cpptrace PRIVATE dbghelp) +endif() + if(CPPTRACE_UNWIND_WITH_NOTHING) target_compile_definitions(cpptrace PUBLIC CPPTRACE_UNWIND_WITH_NOTHING) endif() diff --git a/README.md b/README.md index 2814243..3487dc3 100644 --- a/README.md +++ b/README.md @@ -468,7 +468,7 @@ To static link the library set `-DCPPTRACE_STATIC=On`. Cpptrace supports a number of back-ends and middle-ends to produce stack traces. Stack traces are produced in roughly three steps: Unwinding, symbol resolution, and demangling. Cpptrace by default on linux / macos will generate traces with `_Unwind_Backtrace`, libdwarf, and `__cxa_demangle`. On windows traces are generated by default with -`CaptureStackBackTrace` and dbghelp.h (no demangling is needed with dbghelp). Under mingw libdwarf and dbghelp.h are +`StackWalk64` and dbghelp.h (no demangling is needed with dbghelp). Under mingw libdwarf and dbghelp.h are used, along with `__cxa_demangle`. Support for these is the main focus of cpptrace and they should work well. If you want to use a different back-end such as addr2line, however, you can configure the library to do so. @@ -479,10 +479,12 @@ want to use a different back-end such as addr2line, however, you can configure t | libgcc unwind | `CPPTRACE_UNWIND_WITH_UNWIND` | linux, macos, mingw | Frames are captured with libgcc's `_Unwind_Backtrace`, which currently produces the most accurate stack traces on gcc/clang/mingw. Libgcc is often linked by default, and llvm has something equivalent. | | execinfo.h | `CPPTRACE_UNWIND_WITH_EXECINFO` | linux, macos | Frames are captured with `execinfo.h`'s `backtrace`, part of libc on linux/unix systems. | | winapi | `CPPTRACE_UNWIND_WITH_WINAPI` | windows, mingw | Frames are captured with `CaptureStackBackTrace`. | +| dbghelp | `CPPTRACE_UNWIND_WITH_DBGHELP` | windows, mingw | Frames are captured with `StackWalk64`. | | N/A | `CPPTRACE_UNWIND_WITH_NOTHING` | all | Unwinding is not done, stack traces will be empty. | -These back-ends require a fixed buffer has to be created to read addresses into while unwinding. By default the buffer -can hold addresses for 100 frames (beyond the `skip` frames). This is configurable with `CPPTRACE_HARD_MAX_FRAMES`. +Some back-ends (execinfo and `CaptureStackBackTrace`) require a fixed buffer has to be created to read addresses into +while unwinding. By default the buffer can hold addresses for 100 frames (beyond the `skip` frames). This is +configurable with `CPPTRACE_HARD_MAX_FRAMES`. **Symbol resolution** @@ -530,6 +532,7 @@ Back-ends: - `CPPTRACE_UNWIND_WITH_UNWIND=On/Off` - `CPPTRACE_UNWIND_WITH_EXECINFO=On/Off` - `CPPTRACE_UNWIND_WITH_WINAPI=On/Off` +- `CPPTRACE_UNWIND_WITH_DBGHELP=On/Off` - `CPPTRACE_UNWIND_WITH_NOTHING=On/Off` - `CPPTRACE_DEMANGLE_WITH_CXXABI=On/Off` - `CPPTRACE_DEMANGLE_WITH_WINAPI=On/Off` diff --git a/ci/build-in-all-configs.py b/ci/build-in-all-configs.py index a03c97a..c5d3caf 100644 --- a/ci/build-in-all-configs.py +++ b/ci/build-in-all-configs.py @@ -232,6 +232,7 @@ def main(): "std": ["11", "20"], "unwind": [ "CPPTRACE_UNWIND_WITH_WINAPI", + "CPPTRACE_UNWIND_WITH_DBGHELP", "CPPTRACE_UNWIND_WITH_UNWIND", "CPPTRACE_UNWIND_WITH_NOTHING", ], diff --git a/ci/test-all-configs.py b/ci/test-all-configs.py index 86ee462..87c1475 100644 --- a/ci/test-all-configs.py +++ b/ci/test-all-configs.py @@ -395,6 +395,7 @@ def main(): "std": ["11", "20"], "unwind": [ "CPPTRACE_UNWIND_WITH_WINAPI", + "CPPTRACE_UNWIND_WITH_DBGHELP", #"CPPTRACE_UNWIND_WITH_UNWIND", # Broken on github actions for some reason #"CPPTRACE_UNWIND_WITH_NOTHING", ], diff --git a/src/platform/dbghelp_syminit_manager.hpp b/src/platform/dbghelp_syminit_manager.hpp new file mode 100644 index 0000000..c3eaeed --- /dev/null +++ b/src/platform/dbghelp_syminit_manager.hpp @@ -0,0 +1,45 @@ +#ifndef DBGHELP_SYMINIT_MANAGER_HPP +#define DBGHELP_SYMINIT_MANAGER_HPP + +#include "common.hpp" +#include "utils.hpp" + +#include +#include + +#include +#include + +namespace cpptrace { +namespace detail { + struct dbghelp_syminit_manager { + std::unordered_set set; + std::mutex mutex; + + ~dbghelp_syminit_manager() { + for(auto handle : set) { + if(!SymCleanup(handle)) { + ASSERT(false, stringf("Cpptrace SymCleanup failed with code %llu\n", to_ull(GetLastError()))); + } + } + } + + void init(HANDLE proc) { + if(set.count(proc) == 0) { + std::lock_guard lock(mutex); + if(!SymInitialize(proc, NULL, TRUE)) { + throw std::logic_error(stringf("SymInitialize failed %llu", to_ull(GetLastError()))); + } + set.insert(proc); + } + } + }; + + inline dbghelp_syminit_manager& get_syminit_manager() { + static dbghelp_syminit_manager syminit_manager; + return syminit_manager; + } +} +} + +#endif diff --git a/src/symbols/symbols_with_dbghelp.cpp b/src/symbols/symbols_with_dbghelp.cpp index 433339d..e089518 100644 --- a/src/symbols/symbols_with_dbghelp.cpp +++ b/src/symbols/symbols_with_dbghelp.cpp @@ -3,6 +3,7 @@ #include #include "symbols.hpp" #include "../platform/program_name.hpp" +#include "../platform/dbghelp_syminit_manager.hpp" #include #include @@ -397,10 +398,7 @@ namespace dbghelp { // TODO: When does this need to be called? Can it be moved to the symbolizer? SymSetOptions(SYMOPT_ALLOW_ABSOLUTE_SYMBOLS); HANDLE proc = GetCurrentProcess(); - if(!SymInitialize(proc, NULL, TRUE)) { - //TODO? - throw std::logic_error("SymInitialize failed"); - } + get_syminit_manager().init(proc); for(const auto frame : frames) { try { trace.push_back(resolve_frame(proc, frame)); @@ -411,10 +409,6 @@ namespace dbghelp { trace.push_back(null_frame); } } - if(!SymCleanup(proc)) { - //throw std::logic_error("SymCleanup failed"); - } - return trace; } } diff --git a/src/unwind/unwind_with_dbghelp.cpp b/src/unwind/unwind_with_dbghelp.cpp new file mode 100644 index 0000000..55c8ba4 --- /dev/null +++ b/src/unwind/unwind_with_dbghelp.cpp @@ -0,0 +1,127 @@ +#ifdef CPPTRACE_UNWIND_WITH_DBGHELP + +#include +#include "unwind.hpp" +#include "../platform/common.hpp" +#include "../platform/utils.hpp" +#include "../platform/dbghelp_syminit_manager.hpp" + +#include +#include +#include +#include + +#include +#include + +// Fucking windows headers +#ifdef min + #undef min +#endif + +namespace cpptrace { +namespace detail { + CPPTRACE_FORCE_NO_INLINE + std::vector capture_frames(size_t skip, size_t max_depth) { + skip++; + // https://jpassing.com/2008/03/12/walking-the-stack-of-the-current-thread/ + + // Get current thread context + // GetThreadContext cannot be used on the current thread. + // RtlCaptureContext doesn't work on i386 + CONTEXT context; + #ifdef _M_IX86 + ZeroMemory(&context, sizeof(CONTEXT)); + context.ContextFlags = CONTEXT_CONTROL; + __asm { + label: + mov [context.Ebp], ebp; + mov [context.Esp], esp; + mov eax, [label]; + mov [context.Eip], eax; + } + #else + RtlCaptureContext(&context); + #endif + // Setup current frame + STACKFRAME64 frame; + ZeroMemory(&frame, sizeof(STACKFRAME64)); + DWORD machine_type; + #ifdef _M_IX86 + machine_type = IMAGE_FILE_MACHINE_I386; + frame.AddrPC.Offset = context.Eip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context.Ebp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = context.Esp; + frame.AddrStack.Mode = AddrModeFlat; + #elif _M_X64 + machine_type = IMAGE_FILE_MACHINE_AMD64; + frame.AddrPC.Offset = context.Rip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context.Rsp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Offset = context.Rsp; + frame.AddrStack.Mode = AddrModeFlat; + #elif _M_IA64 + machine_type = IMAGE_FILE_MACHINE_IA64; + frame.AddrPC.Offset = context.StIIP; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context.IntSp; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrBStore.Offset= context.RsBSP; + frame.AddrBStore.Mode = AddrModeFlat; + frame.AddrStack.Offset = context.IntSp; + frame.AddrStack.Mode = AddrModeFlat; + #else + #error "Cpptrace: StackWalk64 not supported for this platform yet" + #endif + + std::vector trace; + + // Dbghelp is is single-threaded, so acquire a lock. + static std::mutex mutex; + std::lock_guard lock(mutex); + // For some reason SymInitialize must be called before StackWalk64 + // Note that the code assumes that + // SymInitialize( GetCurrentProcess(), NULL, TRUE ) has + // already been called. + // + HANDLE proc = GetCurrentProcess(); + HANDLE thread = GetCurrentThread(); + get_syminit_manager().init(proc); + while(trace.size() < max_depth) { + if( + !StackWalk64( + machine_type, + proc, + thread, + &frame, + machine_type == IMAGE_FILE_MACHINE_I386 ? NULL : &context, + NULL, + SymFunctionTableAccess64, + SymGetModuleBase64, + NULL + ) + ) { + // Either failed or finished walking + break; + } + if(frame.AddrPC.Offset != 0) { + // Valid frame + if(skip) { + skip--; + } else { + trace.push_back(frame.AddrPC.Offset); + } + } else { + // base + break; + } + } + return trace; + } +} +} + +#endif