diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a7a152..658264c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -205,6 +205,23 @@ if(LIBCPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE) target_compile_definitions(cpptrace PUBLIC LIBCPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE) endif() +if(LIBCPPTRACE_GET_SYMBOLS_WITH_LIBDL) + target_compile_definitions(cpptrace PUBLIC LIBCPPTRACE_GET_SYMBOLS_WITH_LIBDL) +endif() + +if(LIBCPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE) + target_compile_definitions(cpptrace PUBLIC LIBCPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE) +endif() + +if(LIBCPPTRACE_GET_SYMBOLS_WITH_DBGHELP) + target_compile_definitions(cpptrace PUBLIC LIBCPPTRACE_GET_SYMBOLS_WITH_DBGHELP) + target_link_libraries(cpptrace PRIVATE dbghelp) +endif() + +if(LIBCPPTRACE_GET_SYMBOLS_WITH_NOTHING) + target_compile_definitions(cpptrace PUBLIC LIBCPPTRACE_GET_SYMBOLS_WITH_NOTHING) +endif() + # Unwinding if(LIBCPPTRACE_UNWIND_WITH_EXECINFO) if(NOT HAS_EXECINFO) @@ -217,6 +234,10 @@ if(LIBCPPTRACE_UNWIND_WITH_WINAPI) target_compile_definitions(cpptrace PUBLIC LIBCPPTRACE_UNWIND_WITH_WINAPI) endif() +if(LIBCPPTRACE_UNWIND_WITH_NOTHING) + target_compile_definitions(cpptrace PUBLIC LIBCPPTRACE_UNWIND_WITH_NOTHING) +endif() + # Demangling if(LIBCPPTRACE_DEMANGLE_WITH_CXXABI) if(NOT HAS_CXXABI) @@ -235,12 +256,12 @@ endif() # ====================================================================================================================== -target_link_libraries( - cpptrace - PRIVATE - $<$:dbghelp> - ${CMAKE_DL_LIBS} -) +#target_link_libraries( +# cpptrace +# PRIVATE +# #$<$:dbghelp> +# #${CMAKE_DL_LIBS} +#) if(CMAKE_BUILD_TYPE STREQUAL "") message(FATAL_ERROR "Setting CMAKE_BUILD_TYPE is required") diff --git a/README.md b/README.md index a487fa1..2798f1e 100644 --- a/README.md +++ b/README.md @@ -48,28 +48,28 @@ also manually set which back-end you want used. | Library | CMake config | Windows | Linux | Info | |---------|--------------|---------|-------|------| -| execinfo.h | `LIBCPPTRACE_UNWIND_WITH_EXECINFO` | | ✔️ | Frames are captured with `execinfo.h`'s `backtrace`, part of libc. | -| winapi | `LIBCPPTRACE_UNWIND_WITH_WINAPI` | ✔️ | | Frames are captured with `CaptureStackBackTrace`. | +| execinfo.h | `LIBCPPTRACE_UNWIND_WITH_EXECINFO` | ❌ | ✔️ | Frames are captured with `execinfo.h`'s `backtrace`, part of libc. | +| winapi | `LIBCPPTRACE_UNWIND_WITH_WINAPI` | ✔️ | ❌ | Frames are captured with `CaptureStackBackTrace`. | | N/A | `LIBCPPTRACE_UNWIND_WITH_NOTHING` | ✔️ | ✔️ | Unwinding is not done, stack traces will be empty. | **Symbol resolution** | Library | CMake config | Windows | Linux | Info | |---------|--------------|---------|-------|------| -| libbacktrace | `LIBCPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE` | ❌ | ✔️ | Libbacktrace is already installed on most systems, or available through the compiler directly. | -| libdl | `LIBCPPTRACE_GET_SYMBOLS_WITH_LIBDL` | | ✔️ | Libdl uses dynamic export information. Compiling with `-rdynamic` is often needed. | -| addr2line | `LIBCPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE` | | ✔️ | Symbols are resolved by invoking `addr2line` via `fork()`. | +| libbacktrace | `LIBCPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE` | ❌ | ✔️ | Libbacktrace is already installed on most systems, or available through the compiler directly. If it is installed but backtrace.h is not already in the include path (this can happen when using clang when backtrace lives in gcc's include folder), `LIBCPP_BACKTRACE_PATH` can be used to specify where the library should be looked for. | +| libdl | `LIBCPPTRACE_GET_SYMBOLS_WITH_LIBDL` | ❌ | ✔️ | Libdl uses dynamic export information. Compiling with `-rdynamic` is often needed. | +| addr2line | `LIBCPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE` | ❌ | ✔️ | Symbols are resolved by invoking `addr2line` via `fork()`. | | dbghelp.h | `LIBCPPTRACE_GET_SYMBOLS_WITH_DBGHELP` | ✔️ | ❌ | Dbghelp.h allows access to symbols via debug info. | | N/A | `LIBCPPTRACE_GET_SYMBOLS_WITH_NOTHING` | ✔️ | ✔️ | Don't attempt to resolve symbols. | -Lastly, C++ symbol demangling is done with ``. Under dbghelp.h this is not needed, the symbols aren't mangled -when they are first extracted. +Lastly, on unix systems symbol demangling is done with ``. On windows symbols extracted with dbghelp.h aren't +mangled. Libbacktrace can generate a full stack trace itself, both unwinding and resolving symbols, and this can be chosen with `LIBCPPTRACE_FULL_TRACE_WITH_LIBBACKTRACE`. There are plenty more libraries that can be used for unwinding, parsing debug information, and demangling. In the future -more options may be added. +more back-ends can be added. Ideally this library can "just work" on systems, without additional installation work. ## License diff --git a/src/symbols/symbols_with_dbghelp.cpp b/src/symbols/symbols_with_dbghelp.cpp index e69de29..3942a30 100644 --- a/src/symbols/symbols_with_dbghelp.cpp +++ b/src/symbols/symbols_with_dbghelp.cpp @@ -0,0 +1,401 @@ +#ifdef LIBCPPTRACE_GET_SYMBOLS_WITH_DBGHELP + +#include +#include "libcpp_symbols.hpp" +#include "../platform/libcpp_program_name.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace cpptrace { + namespace detail { + // SymFromAddr only returns the function's name. In order to get information about parameters, + // important for C++ stack traces where functions may be overloaded, we have to manually use + // Windows DIA to walk debug info structures. Resources: + // https://web.archive.org/web/20201027025750/http://www.debuginfo.com/articles/dbghelptypeinfo.html + // https://web.archive.org/web/20201203160805/http://www.debuginfo.com/articles/dbghelptypeinfofigures.html + // https://github.com/DynamoRIO/dynamorio/blob/master/ext/drsyms/drsyms_windows.c#L1370-L1439 + // TODO: Currently unable to detect rvalue references + // TODO: Currently unable to detect const + enum class SymTagEnum { + SymTagNull, SymTagExe, SymTagCompiland, SymTagCompilandDetails, SymTagCompilandEnv, + SymTagFunction, SymTagBlock, SymTagData, SymTagAnnotation, SymTagLabel, SymTagPublicSymbol, + SymTagUDT, SymTagEnum, SymTagFunctionType, SymTagPointerType, SymTagArrayType, + SymTagBaseType, SymTagTypedef, SymTagBaseClass, SymTagFriend, SymTagFunctionArgType, + SymTagFuncDebugStart, SymTagFuncDebugEnd, SymTagUsingNamespace, SymTagVTableShape, + SymTagVTable, SymTagCustom, SymTagThunk, SymTagCustomType, SymTagManagedType, + SymTagDimension, SymTagCallSite, SymTagInlineSite, SymTagBaseInterface, SymTagVectorType, + SymTagMatrixType, SymTagHLSLType, SymTagCaller, SymTagCallee, SymTagExport, + SymTagHeapAllocationSite, SymTagCoffGroup, SymTagMax + }; + + enum class IMAGEHLP_SYMBOL_TYPE_INFO { + TI_GET_SYMTAG, TI_GET_SYMNAME, TI_GET_LENGTH, TI_GET_TYPE, TI_GET_TYPEID, TI_GET_BASETYPE, + TI_GET_ARRAYINDEXTYPEID, TI_FINDCHILDREN, TI_GET_DATAKIND, TI_GET_ADDRESSOFFSET, + TI_GET_OFFSET, TI_GET_VALUE, TI_GET_COUNT, TI_GET_CHILDRENCOUNT, TI_GET_BITPOSITION, + TI_GET_VIRTUALBASECLASS, TI_GET_VIRTUALTABLESHAPEID, TI_GET_VIRTUALBASEPOINTEROFFSET, + TI_GET_CLASSPARENTID, TI_GET_NESTED, TI_GET_SYMINDEX, TI_GET_LEXICALPARENT, TI_GET_ADDRESS, + TI_GET_THISADJUST, TI_GET_UDTKIND, TI_IS_EQUIV_TO, TI_GET_CALLING_CONVENTION, + TI_IS_CLOSE_EQUIV_TO, TI_GTIEX_REQS_VALID, TI_GET_VIRTUALBASEOFFSET, + TI_GET_VIRTUALBASEDISPINDEX, TI_GET_IS_REFERENCE, TI_GET_INDIRECTVIRTUALBASECLASS, + TI_GET_VIRTUALBASETABLETYPE, TI_GET_OBJECTPOINTERTYPE, IMAGEHLP_SYMBOL_TYPE_INFO_MAX + }; + + enum class BasicType { + btNoType = 0, btVoid = 1, btChar = 2, btWChar = 3, btInt = 6, btUInt = 7, btFloat = 8, + btBCD = 9, btBool = 10, btLong = 13, btULong = 14, btCurrency = 25, btDate = 26, + btVariant = 27, btComplex = 28, btBit = 29, btBSTR = 30, btHresult = 31 + }; + + // SymGetTypeInfo utility + template + auto get_info(ULONG type_index, HANDLE proc, ULONG64 modbase) { + T info; + if(!SymGetTypeInfo(proc, modbase, type_index, static_cast<::IMAGEHLP_SYMBOL_TYPE_INFO>(SymType), &info)) { + if(FAILABLE) { + return (T)-1; + } else { + throw std::logic_error( + std::string("SymGetTypeInfo failed: ") + + std::system_error(GetLastError(), std::system_category()).what() + ); + } + } + return info; + } + + template + auto get_info_wchar(ULONG type_index, HANDLE proc, ULONG64 modbase) { + WCHAR* info; + if(!SymGetTypeInfo(proc, modbase, type_index, static_cast<::IMAGEHLP_SYMBOL_TYPE_INFO>(SymType), &info)) { + throw std::logic_error( + std::string("SymGetTypeInfo failed: ") + + std::system_error(GetLastError(), std::system_category()).what() + ); + } + // special case to properly free a buffer and convert string to narrow chars, only used for TI_GET_SYMNAME + static_assert(SymType == IMAGEHLP_SYMBOL_TYPE_INFO::TI_GET_SYMNAME); + std::wstring wstr(info); + std::string str; + str.reserve(wstr.size()); + for(const auto c : wstr) { + str.push_back(static_cast(c)); + } + LocalFree(info); + return str; + } + + // Translate basic types to string + static std::string get_basic_type(ULONG type_index, HANDLE proc, ULONG64 modbase) { + auto basic_type = get_info( + type_index, + proc, + modbase + ); + //auto length = get_info(type_index, proc, modbase); + switch(basic_type) { + case BasicType::btNoType: + return ""; + case BasicType::btVoid: + return "void"; + case BasicType::btChar: + return "char"; + case BasicType::btWChar: + return "wchar_t"; + case BasicType::btInt: + return "int"; + case BasicType::btUInt: + return "unsigned int"; + case BasicType::btFloat: + return "float"; + case BasicType::btBool: + return "bool"; + case BasicType::btLong: + return "long"; + case BasicType::btULong: + return "unsigned long"; + default: + return ""; + } + } + + static std::string resolve_type(ULONG type_index, HANDLE proc, ULONG64 modbase); + + struct class_name_result { + bool has_class_name; + std::string name; + }; + // Helper for member pointers + static class_name_result lookup_class_name(ULONG type_index, HANDLE proc, ULONG64 modbase) { + DWORD class_parent_id = get_info( + type_index, + proc, + modbase + ); + if(class_parent_id == (DWORD)-1) { + return {false, ""}; + } else { + return {true, resolve_type(class_parent_id, proc, modbase)}; + } + } + + struct type_result { + std::string base; + std::string extent; + }; + // Resolve more complex types + // returns [base, extent] + static type_result lookup_type(ULONG type_index, HANDLE proc, ULONG64 modbase) { + auto tag = get_info(type_index, proc, modbase); + switch(tag) { + case SymTagEnum::SymTagBaseType: + return {get_basic_type(type_index, proc, modbase), ""}; + case SymTagEnum::SymTagPointerType: { + DWORD underlying_type_id = get_info( + type_index, + proc, + modbase + ); + bool is_ref = get_info( + type_index, + proc, + modbase + ); + std::string pp = is_ref ? "&" : "*"; // pointer punctuator + auto class_name_res = lookup_class_name(type_index, proc, modbase); + if(class_name_res.has_class_name) { + pp = class_name_res.name + "::" + pp; + } + const auto type = lookup_type(underlying_type_id, proc, modbase); + if(type.extent.empty()) { + return {type.base + (pp.size() > 1 ? " " : "") + pp, ""}; + } else { + return {type.base + "(" + pp, ")" + type.extent}; + } + } + case SymTagEnum::SymTagArrayType: { + DWORD underlying_type_id = get_info( + type_index, + proc, + modbase + ); + DWORD length = get_info( + type_index, + proc, + modbase + ); + const auto type = lookup_type(underlying_type_id, proc, modbase); + return {type.base, "[" + std::to_string(length) + "]" + type.extent}; + } + case SymTagEnum::SymTagFunctionType: { + DWORD return_type_id = get_info( + type_index, + proc, + modbase + ); + DWORD n_children = get_info( + type_index, + proc, + modbase + ); + DWORD class_parent_id = get_info( + type_index, + proc, + modbase + ); + int n_ignore = class_parent_id != (DWORD)-1; // ignore this param + n_children -= n_ignore; // this must be ignored before TI_FINDCHILDREN_PARAMS::Count is set, else error + // return type + const auto return_type = lookup_type(return_type_id, proc, modbase); + if(n_children == 0) { + return {return_type.base, "()" + return_type.extent}; + } else { + // alignment should be fine + size_t sz = sizeof(TI_FINDCHILDREN_PARAMS) + + (n_children) * sizeof(TI_FINDCHILDREN_PARAMS::ChildId[0]); + TI_FINDCHILDREN_PARAMS* children = (TI_FINDCHILDREN_PARAMS*) new char[sz]; + children->Start = 0; + children->Count = n_children; + if( + !SymGetTypeInfo( + proc, modbase, type_index, + static_cast<::IMAGEHLP_SYMBOL_TYPE_INFO>(IMAGEHLP_SYMBOL_TYPE_INFO::TI_FINDCHILDREN), + children + ) + ) { + throw std::logic_error( + std::string("SymGetTypeInfo failed: ") + + std::system_error(GetLastError(), std::system_category()).what() + ); + } + // get children type + std::string extent = "("; + if(children->Start != 0) { + throw std::logic_error("Error: children->Start == 0"); + } + for(std::size_t i = 0; i < n_children; i++) { + extent += (i == 0 ? "" : ", ") + resolve_type(children->ChildId[i], proc, modbase); + } + extent += ")"; + delete[] (char*) children; + return {return_type.base, extent + return_type.extent}; + } + } + case SymTagEnum::SymTagFunctionArgType: { + DWORD underlying_type_id = + get_info(type_index, proc, modbase); + return {resolve_type(underlying_type_id, proc, modbase), ""}; + } + case SymTagEnum::SymTagTypedef: + case SymTagEnum::SymTagEnum: + case SymTagEnum::SymTagUDT: + case SymTagEnum::SymTagBaseClass: + return {get_info_wchar(type_index, proc, modbase), ""}; + default: + return { + "::type>(tag)) + ">", + "" + }; + }; + } + + static std::string resolve_type(ULONG type_index, HANDLE proc, ULONG64 modbase) { + const auto type = lookup_type(type_index, proc, modbase); + return type.base + type.extent; + } + + struct function_info { + HANDLE proc; + ULONG64 modbase; + int counter; + int n_children; + int n_ignore; + std::string str; + }; + + // Enumerates function parameters + static BOOL __stdcall enumerator_callback( + PSYMBOL_INFO symbol_info, + ULONG, + PVOID data + ) { + function_info* ctx = (function_info*)data; + if(ctx->counter++ >= ctx->n_children) { + return false; + } + if(ctx->n_ignore-- > 0) { + return true; // just skip + } + ctx->str += resolve_type(symbol_info->TypeIndex, ctx->proc, ctx->modbase); + if(ctx->counter < ctx->n_children) { + ctx->str += ", "; + } + return true; + } + + // TODO: Handle backtrace_pcinfo calling the callback multiple times on inlined functions + struct symbolizer::impl { + bool good = true; + HANDLE proc; + + impl() { + // TODO: When does this need to be called? Can it be moved to the symbolizer? + SymSetOptions(SYMOPT_ALLOW_ABSOLUTE_SYMBOLS); + proc = GetCurrentProcess(); + if(!SymInitialize(proc, NULL, TRUE)) { + good = false; + } + } + + ~impl() { + if(!SymCleanup(proc)) { + //throw std::logic_error("SymCleanup failed"); + } + } + + stacktrace_frame resolve_frame(void* addr) { + alignas(SYMBOL_INFO) char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)]; + SYMBOL_INFO* symbol = (SYMBOL_INFO*)buffer; + symbol->SizeOfStruct = sizeof(SYMBOL_INFO); + symbol->MaxNameLen = MAX_SYM_NAME; + union { DWORD64 a; DWORD b; } displacement; + IMAGEHLP_LINE64 line; + bool got_line = SymGetLineFromAddr64(proc, (DWORD64)addr, &displacement.b, &line); + if(SymFromAddr(proc, (DWORD64)addr, &displacement.a, symbol)) { + if(got_line) { + IMAGEHLP_STACK_FRAME frame; + frame.InstructionOffset = symbol->Address; + // https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symsetcontext + // "If you call SymSetContext to set the context to its current value, the + // function fails but GetLastError returns ERROR_SUCCESS." + // This is the stupidest fucking api I've ever worked with. + if(SymSetContext(proc, &frame, nullptr) == FALSE && GetLastError() != ERROR_SUCCESS) { + fprintf(stderr, "Stack trace: Internal error while calling SymSetContext\n"); + return { + reinterpret_cast(addr), + int(line.LineNumber), + -1, + line.FileName, + symbol->Name + }; + } + DWORD n_children = get_info( + symbol->TypeIndex, + proc, + symbol->ModBase + ); + DWORD class_parent_id = get_info( + symbol->TypeIndex, + proc, + symbol->ModBase + ); + function_info fi { proc, symbol->ModBase, 0, int(n_children), class_parent_id != (DWORD)-1, "" }; + SymEnumSymbols(proc, 0, nullptr, enumerator_callback, &fi); + std::string signature = symbol->Name + std::string("(") + fi.str + ")"; + // There's a phenomina with DIA not inserting commas after template parameters. Fix them here. + static std::regex comma_re(R"(,(?=\S))"); + signature = std::regex_replace(signature, comma_re, ", "); + return { + reinterpret_cast(addr), + int(line.LineNumber), + -1, + line.FileName, + signature + }; + } else { + return { + reinterpret_cast(addr), + -1, + -1, + "", + symbol->Name + }; + } + } else { + return { + reinterpret_cast(addr), + -1, + -1, + "", + "" + }; + } + } + }; + + symbolizer::symbolizer() : pimpl{new impl} {} + symbolizer::~symbolizer() = default; + + stacktrace_frame symbolizer::resolve_frame(void* addr) { + return pimpl->resolve_frame(addr); + } + } +} + +#endif diff --git a/src/unwind/unwind_with_winapi.cpp b/src/unwind/unwind_with_winapi.cpp index 3ea3a2b..aa2fefa 100644 --- a/src/unwind/unwind_with_winapi.cpp +++ b/src/unwind/unwind_with_winapi.cpp @@ -10,10 +10,6 @@ namespace cpptrace { namespace detail { std::vector capture_frames() { - // 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)) return {}; std::vector addrs(hard_max_frames, nullptr); int frames = CaptureStackBackTrace(0, hard_max_frames, addrs.data(), NULL); addrs.resize(frames); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 34fac2f..043d471 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -10,7 +10,20 @@ project( LANGUAGES CXX ) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${OUTPUT_DIRECTORY}") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIRECTORY}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${OUTPUT_DIRECTORY}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIRECTORY}") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${OUTPUT_DIRECTORY}") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${OUTPUT_DIRECTORY}") + add_executable(test test.cpp) target_link_libraries(test PRIVATE cpptrace) target_include_directories(test PRIVATE ../include) target_link_directories(test PRIVATE ../build) + +file(GLOB_RECURSE CPPTRACE_PATHS ../build/cpptrace.dll ../build/libcpptrace.so ../build/**/cpptrace.dll ../build/**/libcpptrace.so) +list(POP_BACK CPPTRACE_PATHS CPPTRACE_PATH) +#find_library(CPPTRACE_PATH NAMES "cpptrace.dll" "libcpptrace.so" PATHS ../build PATH_SUFFIXES Debug Release) +message(STATUS "Copying lib from " ${CPPTRACE_PATH}) +file(COPY ${CPPTRACE_PATH} DESTINATION .)