From d7a5eb54fd4e7df0460687e9047910b69496a19b Mon Sep 17 00:00:00 2001 From: Jeremy Rifkin <51220084+jeremy-rifkin@users.noreply.github.com> Date: Fri, 21 Jul 2023 22:33:56 -0400 Subject: [PATCH] Add baseline mingw support (#13) --- .github/workflows/build.yml | 10 ++ .github/workflows/test.yml | 10 ++ CMakeLists.txt | 22 +++- ci/build-in-all-configs.py | 52 ++++++++-- ci/test-all-configs.py | 78 +++++++++++--- src/platform/cpptrace_common.hpp | 90 ++++++++++++++++ src/symbols/symbols_with_addr2line.cpp | 138 +++++++++++++++++++++++-- 7 files changed, 368 insertions(+), 32 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 561dbd3..212d1d0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,3 +43,13 @@ jobs: run: | pip3 install colorama python3 ci/build-in-all-configs.py --clang-only + build-windows-mingw: + runs-on: windows-2019 + steps: + - uses: actions/checkout@v2 + - name: Enable Developer Command Prompt + uses: ilammy/msvc-dev-cmd@v1.10.0 + - name: build + run: | + pip3 install colorama + python3 ci/build-in-all-configs.py --mingw-only diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84bf238..09a4ad9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,3 +45,13 @@ jobs: run: | pip3 install colorama python3 ci/test-all-configs.py --clang-only + build-windows-mingw: + runs-on: windows-2019 + steps: + - uses: actions/checkout@v2 + - name: Enable Developer Command Prompt + uses: ilammy/msvc-dev-cmd@v1.10.0 + - name: build + run: | + pip3 install colorama + python3 ci/test-all-configs.py --mingw-only diff --git a/CMakeLists.txt b/CMakeLists.txt index f6f0492..6dc280a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,7 +133,7 @@ if( if(HAS_STACKTRACE AND NOT WIN32) # Our trace is better than msvc's set(CPPTRACE_FULL_TRACE_WITH_LIBBACKTRACE On) message(STATUS "Cpptrace auto config: Using C++23 for the full trace") - elseif(HAS_BACKTRACE) + elseif(HAS_BACKTRACE AND NOT WIN32) # Mingw libbacktrace doesn't seem to be working set(CPPTRACE_FULL_TRACE_WITH_LIBBACKTRACE On) message(STATUS "Cpptrace auto config: Using libbacktrace for the full trace") endif() @@ -163,6 +163,17 @@ if( set(CPPTRACE_UNWIND_WITH_NOTHING On) message(FATAL_ERROR "Cpptrace auto config: No unwinding back-end seems to be supported, stack tracing will not work. To compile anyway set CPPTRACE_UNWIND_WITH_NOTHING.") endif() + elseif(WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # mingw + if(HAS_UNWIND) + set(CPPTRACE_UNWIND_WITH_UNWIND On) + message(STATUS "Cpptrace auto config: Using libgcc unwind for unwinding") + elseif(HAS_EXECINFO) + set(CPPTRACE_UNWIND_WITH_EXECINFO On) + message(STATUS "Cpptrace auto config: Using execinfo.h for unwinding") + else() + set(CPPTRACE_UNWIND_WITH_WINAPI On) + message(STATUS "Cpptrace auto config: Using winapi for unwinding") + endif() elseif(WIN32) set(CPPTRACE_UNWIND_WITH_WINAPI On) message(STATUS "Cpptrace auto config: Using winapi for unwinding") @@ -189,11 +200,14 @@ if( set(CPPTRACE_GET_SYMBOLS_WITH_LIBDL ON) elseif(UNIX) if(HAS_BACKTRACE) - set(CPPTRACE_FULL_TRACE_WITH_LIBBACKTRACE On) + set(CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE On) message(STATUS "Cpptrace auto config: Using libbacktrace for symbols") else() message(FATAL_ERROR "Cpptrace auto config: No symbol back-end could be automatically configured. To compile anyway set CPPTRACE_GET_SYMBOLS_WITH_NOTHING.") endif() + elseif(WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") # mingw + set(CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE On) + message(STATUS "Cpptrace auto config: Using addr2line for symbols") elseif(WIN32) set(CPPTRACE_GET_SYMBOLS_WITH_DBGHELP On) message(STATUS "Cpptrace auto config: Using dbghelp for symbols") @@ -260,7 +274,9 @@ endif() if(CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE) target_compile_definitions(cpptrace PUBLIC CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE) - target_link_libraries(cpptrace PRIVATE dl) + if(UNIX) + target_link_libraries(cpptrace PRIVATE dl) + endif() endif() if(CPPTRACE_GET_SYMBOLS_WITH_DBGHELP) diff --git a/ci/build-in-all-configs.py b/ci/build-in-all-configs.py index 899aa0b..1edc16b 100644 --- a/ci/build-in-all-configs.py +++ b/ci/build-in-all-configs.py @@ -54,7 +54,7 @@ def build(matrix): if succeeded: run_command("make", "-j") else: - succeeded = run_command( + args = [ "cmake", "..", f"-DCMAKE_BUILD_TYPE={matrix['target']}", @@ -63,9 +63,15 @@ def build(matrix): f"-D{matrix['unwind']}=On", f"-D{matrix['symbols']}=On", f"-D{matrix['demangle']}=On" - ) + ] + if matrix["compiler"] == "g++": + args.append("-GUnix Makefiles") + succeeded = run_command(*args) if succeeded: - run_command("msbuild", "cpptrace.sln") + if matrix["compiler"] == "g++": + run_command("make", "-j") + else: + run_command("msbuild", "cpptrace.sln") os.chdir("..") print() @@ -103,10 +109,14 @@ def build_full_or_auto(matrix): ] if matrix["config"] != "": args.append(f"{matrix['config']}") - print(args) + if matrix["compiler"] == "g++": + args.append("-GUnix Makefiles") succeeded = run_command(*args) if succeeded: - run_command("msbuild", "cpptrace.sln") + if matrix["compiler"] == "g++": + run_command("make", "-j") + else: + run_command("msbuild", "cpptrace.sln") os.chdir("..") print() @@ -188,13 +198,19 @@ def main(): "--msvc-only", action="store_true" ) + parser.add_argument( + "--mingw-only", + action="store_true" + ) args = parser.parse_args() - compilers = ["cl", "clang++"] + compilers = ["cl", "clang++", "g++"] if args.clang_only: compilers = ["clang++"] if args.msvc_only: compilers = ["cl"] + if args.mingw_only: + compilers = ["g++"] matrix = { "compiler": compilers, @@ -202,10 +218,12 @@ def main(): "std": ["11", "20"], "unwind": [ "CPPTRACE_UNWIND_WITH_WINAPI", + "CPPTRACE_UNWIND_WITH_UNWIND", "CPPTRACE_UNWIND_WITH_NOTHING", ], "symbols": [ "CPPTRACE_GET_SYMBOLS_WITH_DBGHELP", + "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE", "CPPTRACE_GET_SYMBOLS_WITH_NOTHING", ], "demangle": [ @@ -217,7 +235,27 @@ def main(): { "demangle": "CPPTRACE_DEMANGLE_WITH_CXXABI", "compiler": "cl" - } + }, + { + "unwind": "CPPTRACE_UNWIND_WITH_UNWIND", + "compiler": "cl" + }, + { + "unwind": "CPPTRACE_UNWIND_WITH_UNWIND", + "compiler": "clang++" + }, + { + "symbols": "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE", + "compiler": "cl" + }, + { + "symbols": "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE", + "compiler": "clang++" + }, + { + "symbols": "CPPTRACE_GET_SYMBOLS_WITH_DBGHELP", + "compiler": "g++" + }, ] run_matrix(matrix, exclude, build) matrix = { diff --git a/ci/test-all-configs.py b/ci/test-all-configs.py index 8bc6876..b2d9620 100644 --- a/ci/test-all-configs.py +++ b/ci/test-all-configs.py @@ -161,7 +161,7 @@ def build(matrix): if succeeded: return run_command("make", "-j") else: - succeeded = run_command( + args = [ "cmake", "..", f"-DCMAKE_BUILD_TYPE={matrix['target']}", @@ -172,9 +172,15 @@ def build(matrix): f"-D{matrix['demangle']}=On", "-DCPPTRACE_BUILD_TEST=On", "-DBUILD_SHARED_LIBS=On" - ) + ] + if matrix["compiler"] == "g++": + args.append("-GUnix Makefiles") + succeeded = run_command(*args) if succeeded: - return run_command("msbuild", "cpptrace.sln") + if matrix["compiler"] == "g++": + run_command("make", "-j") + else: + run_command("msbuild", "cpptrace.sln") def build_full_or_auto(matrix): if platform.system() != "Windows": @@ -205,10 +211,14 @@ def build_full_or_auto(matrix): ] if matrix["config"] != "": args.append(f"{matrix['config']}") - print(args) + if matrix["compiler"] == "g++": + args.append("-GUnix Makefiles") succeeded = run_command(*args) if succeeded: - return run_command("msbuild", "cpptrace.sln") + if matrix["compiler"] == "g++": + run_command("make", "-j") + else: + run_command("msbuild", "cpptrace.sln") def test(matrix): if platform.system() != "Windows": @@ -217,10 +227,16 @@ def test(matrix): (matrix["compiler"], matrix["unwind"], matrix["symbols"], matrix["demangle"]) ) else: - run_test( - f".\\{matrix['target']}\\test.exe", - (matrix["compiler"], matrix["unwind"], matrix["symbols"], matrix["demangle"]) - ) + if matrix["compiler"] == "g++": + run_test( + f".\\test.exe", + (matrix["compiler"], matrix["unwind"], matrix["symbols"], matrix["demangle"]) + ) + else: + run_test( + f".\\{matrix['target']}\\test.exe", + (matrix["compiler"], matrix["unwind"], matrix["symbols"], matrix["demangle"]) + ) def test_full_or_auto(matrix): if platform.system() != "Windows": @@ -229,10 +245,16 @@ def test_full_or_auto(matrix): (matrix["compiler"],) ) else: - run_test( - f".\\{matrix['target']}\\test.exe", - (matrix["compiler"],) - ) + if matrix["compiler"] == "g++": + run_test( + f".\\test.exe", + (matrix["compiler"],) + ) + else: + run_test( + f".\\{matrix['target']}\\test.exe", + (matrix["compiler"],) + ) def build_and_test(matrix): print(f"{Fore.BLUE}{Style.BRIGHT}{'=' * 10} Running build and test with config {', '.join(matrix.values())} {'=' * 10}{Style.RESET_ALL}") @@ -341,13 +363,19 @@ def main(): "--msvc-only", action="store_true" ) + parser.add_argument( + "--mingw-only", + action="store_true" + ) args = parser.parse_args() - compilers = ["cl", "clang++"] + compilers = ["cl", "clang++", "g++"] if args.clang_only: compilers = ["clang++"] if args.msvc_only: compilers = ["cl"] + if args.mingw_only: + compilers = ["g++"] matrix = { "compiler": compilers, @@ -355,10 +383,12 @@ def main(): "std": ["11", "20"], "unwind": [ "CPPTRACE_UNWIND_WITH_WINAPI", + "CPPTRACE_UNWIND_WITH_UNWIND", #"CPPTRACE_UNWIND_WITH_NOTHING", ], "symbols": [ "CPPTRACE_GET_SYMBOLS_WITH_DBGHELP", + "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE", #"CPPTRACE_GET_SYMBOLS_WITH_NOTHING", ], "demangle": [ @@ -370,6 +400,26 @@ def main(): { "demangle": "CPPTRACE_DEMANGLE_WITH_CXXABI", "compiler": "cl" + }, + { + "unwind": "CPPTRACE_UNWIND_WITH_UNWIND", + "compiler": "cl" + }, + { + "unwind": "CPPTRACE_UNWIND_WITH_UNWIND", + "compiler": "clang++" + }, + { + "symbols": "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE", + "compiler": "cl" + }, + { + "symbols": "CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE", + "compiler": "clang++" + }, + { + "symbols": "CPPTRACE_GET_SYMBOLS_WITH_DBGHELP", + "compiler": "g++" } ] run_matrix(matrix, exclude, build_and_test) diff --git a/src/platform/cpptrace_common.hpp b/src/platform/cpptrace_common.hpp index 27025fc..750e4a1 100644 --- a/src/platform/cpptrace_common.hpp +++ b/src/platform/cpptrace_common.hpp @@ -19,9 +19,44 @@ #include #include #include +#include #include #include +#define IS_WINDOWS 0 +#define IS_LINUX 0 +#define IS_APPLE 0 + +#if defined(_WIN32) + #undef IS_WINDOWS + #define IS_WINDOWS 1 +#elif defined(__linux) + #undef IS_LINUX + #define IS_LINUX 1 +#elif defined(__APPLE__) + #undef IS_APPLE + #define IS_APPLE 1 +#else + #error "Unexpected platform" +#endif + +#define IS_CLANG 0 +#define IS_GCC 0 +#define IS_MSVC 0 + +#if defined(__clang__) + #undef IS_CLANG + #define IS_CLANG 1 +#elif defined(__GNUC__) || defined(__GNUG__) + #undef IS_GCC + #define IS_GCC 1 +#elif defined(_MSC_VER) + #undef IS_MSVC + #define IS_MSVC 1 +#else + #error "Unsupported compiler" +#endif + // Lightweight std::source_location. struct source_location { // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) @@ -128,6 +163,61 @@ static std::string to_hex(uintptr_t addr) { return std::move(sstream).str(); } +CPPTRACE_MAYBE_UNUSED +static bool is_little_endian() { + uint16_t num = 0x1; + auto* ptr = (uint8_t*)# + return ptr[0] == 1; +} + +// Modified from +// https://stackoverflow.com/questions/105252/how-do-i-convert-between-big-endian-and-little-endian-values-in-c +template +struct byte_swapper; + +template +struct byte_swapper { + T operator()(T val) { + return val; + } +}; + +template +struct byte_swapper { + T operator()(T val) { + return ((((val) >> 8) & 0xff) | (((val) & 0xff) << 8)); + } +}; + +template +struct byte_swapper { + T operator()(T val) { + return ((((val) & 0xff000000) >> 24) | + (((val) & 0x00ff0000) >> 8) | + (((val) & 0x0000ff00) << 8) | + (((val) & 0x000000ff) << 24)); + } +}; + +template +struct byte_swapper { + T operator()(T val) { + return ((((val) & 0xff00000000000000ull) >> 56) | + (((val) & 0x00ff000000000000ull) >> 40) | + (((val) & 0x0000ff0000000000ull) >> 24) | + (((val) & 0x000000ff00000000ull) >> 8 ) | + (((val) & 0x00000000ff000000ull) << 8 ) | + (((val) & 0x0000000000ff0000ull) << 24) | + (((val) & 0x000000000000ff00ull) << 40) | + (((val) & 0x00000000000000ffull) << 56)); + } +}; + +template::value, int>::type = 0> +T byteswap(T value) { + return byte_swapper{}(value); +} + #ifdef _MSC_VER #pragma warning(pop) #endif diff --git a/src/symbols/symbols_with_addr2line.cpp b/src/symbols/symbols_with_addr2line.cpp index 98bafda..db2eca6 100644 --- a/src/symbols/symbols_with_addr2line.cpp +++ b/src/symbols/symbols_with_addr2line.cpp @@ -12,11 +12,15 @@ #include #include -#include -#include -// NOLINTNEXTLINE(misc-include-cleaner) -#include -#include +#if IS_LINUX || IS_APPLE + #include + #include + // NOLINTNEXTLINE(misc-include-cleaner) + #include + #include +#elif IS_WINDOWS + #include +#endif namespace cpptrace { namespace detail { @@ -27,6 +31,7 @@ namespace cpptrace { uintptr_t raw_address = 0; }; + #if IS_LINUX || IS_APPLE // aladdr queries are needed to get pre-ASLR addresses and targets to run addr2line on std::vector backtrace_frames(const std::vector& addrs) { // reference: https://github.com/bminor/glibc/blob/master/debug/backtracesyms.c @@ -41,7 +46,7 @@ namespace cpptrace { // but we don't really need dli_saddr frame.obj_path = info.dli_fname; frame.obj_base = reinterpret_cast(info.dli_fbase); - frame.symbol = info.dli_sname ?: "?"; + frame.symbol = info.dli_sname ?: ""; } frames.push_back(frame); } @@ -113,6 +118,118 @@ namespace cpptrace { return output; } + uintptr_t get_module_image_base(const dlframe &entry) { + (void)entry; + return 0; + } + #elif IS_WINDOWS + // aladdr queries are needed to get pre-ASLR addresses and targets to run addr2line on + std::vector backtrace_frames(const std::vector& addrs) { + // reference: https://github.com/bminor/glibc/blob/master/debug/backtracesyms.c + std::vector frames; + frames.reserve(addrs.size()); + for(const void* addr : addrs) { + dlframe frame; + frame.raw_address = reinterpret_cast(addr); + HMODULE handle; + if(GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + static_cast(addr), + &handle + )) { + fflush(stderr); + char path[MAX_PATH]; + if(GetModuleFileNameA(handle, path, sizeof(path))) { + ///fprintf(stderr, "path: %s base: %p\n", path, handle); + frame.obj_path = path; + frame.obj_base = reinterpret_cast(handle); + frame.symbol = ""; + } else { + fprintf(stderr, "%s\n", std::system_error(GetLastError(), std::system_category()).what()); + } + } else { + fprintf(stderr, "%s\n", std::system_error(GetLastError(), std::system_category()).what()); + } + frames.push_back(frame); + } + return frames; + } + + bool has_addr2line() { + // TODO: Popen is a hack. Implement properly with CreateProcess and pipes later. + FILE* p = popen("addr2line --version", "r"); + return pclose(p) == 0; + } + + std::string resolve_addresses(const std::string& addresses, const std::string& executable) { + // TODO: Popen is a hack. Implement properly with CreateProcess and pipes later. + ///fprintf(stderr, ("addr2line -e " + executable + " -fCp " + addresses + "\n").c_str()); + FILE* p = popen(("addr2line -e " + executable + " -fCp " + addresses).c_str(), "r"); + std::string output; + constexpr int buffer_size = 4096; + char buffer[buffer_size]; + size_t count = 0; + while((count = fread(buffer, 1, buffer_size, p)) > 0) { + output.insert(output.end(), buffer, buffer + count); + } + pclose(p); + ///fprintf(stderr, "%s\n", output.c_str()); + return output; + } + + // TODO: Refactor into backtrace_frames... + // TODO: Memoize + uintptr_t get_module_image_base(const dlframe &entry) { + // PE header values are little endian + bool do_swap = !is_little_endian(); + FILE* file = fopen(entry.obj_path.c_str(), "rb"); + char magic[2]; + internal_verify(fread(magic, 1, 2, file) == 2); // file + 0x0 + internal_verify(memcmp(magic, "MZ", 2) == 0); + DWORD e_lfanew; + internal_verify(fseek(file, 0x3c, SEEK_SET) == 0); + internal_verify(fread(&e_lfanew, sizeof(DWORD), 1, file) == 1); // file + 0x3c + if(do_swap) e_lfanew = byteswap(e_lfanew); + long nt_header_offset = e_lfanew; + char signature[4]; + internal_verify(fseek(file, nt_header_offset, SEEK_SET) == 0); + internal_verify(fread(signature, 1, 4, file) == 4); // NT header + 0x0 + internal_verify(memcmp(signature, "PE\0\0", 4) == 0); + //WORD machine; + //internal_verify(fseek(file, nt_header_offset + 4, SEEK_SET) == 0); // file header + 0x0 + //internal_verify(fread(&machine, sizeof(WORD), 1, file) == 1); + WORD size_of_optional_header; + internal_verify(fseek(file, nt_header_offset + 4 + 0x10, SEEK_SET) == 0); // file header + 0x10 + internal_verify(fread(&size_of_optional_header, sizeof(DWORD), 1, file) == 1); + if(do_swap) size_of_optional_header = byteswap(size_of_optional_header); + internal_verify(size_of_optional_header != 0); + WORD optional_header_magic; + internal_verify(fseek(file, nt_header_offset + 0x18, SEEK_SET) == 0); // optional header + 0x0 + internal_verify(fread(&optional_header_magic, sizeof(DWORD), 1, file) == 1); + if(do_swap) optional_header_magic = byteswap(optional_header_magic); + internal_verify(optional_header_magic == IMAGE_NT_OPTIONAL_HDR_MAGIC); + uintptr_t image_base; + if(optional_header_magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { + // 32 bit + DWORD base; + internal_verify(fseek(file, nt_header_offset + 0x18 + 0x1c, SEEK_SET) == 0); // optional header + 0x1c + internal_verify(fread(&base, sizeof(DWORD), 1, file) == 1); + if(do_swap) base = byteswap(base); + image_base = base; + } else { + // 64 bit + // I get an "error: 'QWORD' was not declared in this scope" for some reason when using QWORD + unsigned __int64 base; + internal_verify(fseek(file, nt_header_offset + 0x18 + 0x18, SEEK_SET) == 0); // optional header + 0x18 + internal_verify(fread(&base, sizeof(unsigned __int64), 1, file) == 1); + if(do_swap) base = byteswap(base); + image_base = base; + } + fclose(file); + return image_base; + } + #endif + struct symbolizer::impl { using target_vec = std::vector>>; @@ -124,8 +241,9 @@ namespace cpptrace { std::unordered_map entries; for(std::size_t i = 0; i < dlframes.size(); i++) { const auto& entry = dlframes[i]; + ///fprintf(stderr, "%s %s\n", to_hex(entry.raw_address).c_str(), to_hex(entry.raw_address - entry.obj_base + base).c_str()); entries[entry.obj_path].emplace_back( - to_hex(entry.raw_address - entry.obj_base), + to_hex(entry.raw_address - entry.obj_base + get_module_image_base(entry)), trace[i] ); // Set what is known for now, and resolutions from addr2line should overwrite @@ -182,7 +300,11 @@ namespace cpptrace { std::string address_input; for(const auto& pair : entries_vec) { address_input += pair.first; - address_input += '\n'; + #if !IS_WINDOWS + address_input += '\n'; + #else + address_input += ' '; + #endif } auto output = split(trim(resolve_addresses(address_input, object_name)), "\n"); internal_verify(output.size() == entries_vec.size());