From 7cbf4c25e9d925b9b5da815901468b4bf6adfcce Mon Sep 17 00:00:00 2001 From: Zsolt Parragi <zsolt.parragi@cancellar.hu> Date: Fri, 23 Aug 2019 16:48:59 +0200 Subject: [PATCH] Windows (Clang9, MSVC2017) implementation Remaining issues: * some warinings --- CMakeLists.txt | 2 +- backward.hpp | 458 +++++++++++++++++++++++++++++++++++++++++++- test/_test_main.cpp | 104 +++++++++- test/stacktrace.cpp | 11 +- test/suicide.cpp | 5 + 5 files changed, 563 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 07ae099..97327cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,7 +102,7 @@ if(BACKWARD_TESTS) add_executable(${test_name} ${src} ${ARGN} $<TARGET_OBJECTS:test_main>) - target_link_libraries(${test_name} PRIVATE Backward::Backward test_main) + target_link_libraries(${test_name} PRIVATE Backward::Backward) add_test(NAME ${name} COMMAND ${test_name}) endmacro() diff --git a/backward.hpp b/backward.hpp index 55d03be..3ed2e52 100644 --- a/backward.hpp +++ b/backward.hpp @@ -31,7 +31,7 @@ #if defined(BACKWARD_CXX11) #elif defined(BACKWARD_CXX98) #else -#if __cplusplus >= 201103L +#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800) #define BACKWARD_CXX11 #define BACKWARD_ATLEAST_CXX11 #define BACKWARD_ATLEAST_CXX98 @@ -55,11 +55,14 @@ #if defined(BACKWARD_SYSTEM_LINUX) #elif defined(BACKWARD_SYSTEM_DARWIN) #elif defined(BACKWARD_SYSTEM_UNKNOWN) +#elif defined(BACKWARD_SYSTEM_WINDOWS) #else #if defined(__linux) || defined(__linux__) #define BACKWARD_SYSTEM_LINUX #elif defined(__APPLE__) #define BACKWARD_SYSTEM_DARWIN +#elif defined(_WIN32) +#define BACKWARD_SYSTEM_WINDOWS #else #define BACKWARD_SYSTEM_UNKNOWN #endif @@ -301,6 +304,49 @@ #endif #endif // defined(BACKWARD_SYSTEM_DARWIN) +#if defined(BACKWARD_SYSTEM_WINDOWS) + +#include <condition_variable> +#include <mutex> +#include <thread> + +#include <BaseTsd.h> +typedef SSIZE_T ssize_t; + +#define NOMINMAX +#include <Windows.h> +#include <winnt.h> + +#include <Psapi.h> +#include <signal.h> + +#ifndef __clang__ +#undef NOINLINE +#define NOINLINE __declspec(noinline) +#endif + +#pragma comment(lib, "psapi.lib") +#pragma comment(lib, "dbghelp.lib") + +// Comment / packing is from stackoverflow: +// https://stackoverflow.com/questions/6205981/windows-c-stack-trace-from-a-running-app/28276227#28276227 +// Some versions of imagehlp.dll lack the proper packing directives themselves +// so we need to do it. +#pragma pack(push, before_imagehlp, 8) +#include <imagehlp.h> +#pragma pack(pop, before_imagehlp) + +// TODO maybe these should be undefined somewhere else? +#undef BACKWARD_HAS_UNWIND +#undef BACKWARD_HAS_BACKTRACE +#if BACKWARD_HAS_PDB_SYMBOL == 1 +#else +#undef BACKWARD_HAS_PDB_SYMBOL +#define BACKWARD_HAS_PDB_SYMBOL 1 +#endif + +#endif + #if BACKWARD_HAS_UNWIND == 1 #include <unwind.h> @@ -357,12 +403,15 @@ namespace system_tag { struct linux_tag; // seems that I cannot call that "linux" because the name // is already defined... so I am adding _tag everywhere. struct darwin_tag; +struct windows_tag; struct unknown_tag; #if defined(BACKWARD_SYSTEM_LINUX) typedef linux_tag current_tag; #elif defined(BACKWARD_SYSTEM_DARWIN) typedef darwin_tag current_tag; +#elif defined(BACKWARD_SYSTEM_WINDOWS) +typedef windows_tag current_tag; #elif defined(BACKWARD_SYSTEM_UNKNOWN) typedef unknown_tag current_tag; #else @@ -396,6 +445,13 @@ typedef backtrace_symbol current; #else #error "You shall not pass, until you know what you want." #endif +#elif defined(BACKWARD_SYSTEM_WINDOWS) +struct pdb_symbol; +#if BACKWARD_HAS_PDB_SYMBOL == 1 +typedef pdb_symbol current; +#else +#error "You shall not pass, until you know what you want." +#endif #endif } // namespace trace_resolver_tag @@ -747,7 +803,7 @@ private: }; }; -#else // BACKWARD_HAS_UNWIND == 0 +#elif defined(BACKWARD_HAS_BACKTRACE) template <> class StackTraceImpl<system_tag::current_tag> : public StackTraceImplHolder { @@ -781,7 +837,102 @@ public: } }; -#endif // BACKWARD_HAS_UNWIND +#elif defined(BACKWARD_SYSTEM_WINDOWS) + +template <> +class StackTraceImpl<system_tag::current_tag> : public StackTraceImplHolder { +public: + // We have to load the machine type from the image info + // So we first initialize the resolver, and it tells us this info + void set_machine_type(DWORD machine_type) { machine_type_ = machine_type; } + void set_context(CONTEXT *ctx) { ctx_ = ctx; } + void set_thread_handle(HANDLE handle) { thd_ = handle; } + + NOINLINE + size_t load_here(size_t depth = 32) { + + CONTEXT localCtx; // used when no context is provided + + if (depth == 0) { + return 0; + } + + if (!ctx_) { + ctx_ = &localCtx; + RtlCaptureContext(ctx_); + } + + if (!thd_) { + thd_ = GetCurrentThread(); + } + + HANDLE process = GetCurrentProcess(); + + STACKFRAME64 s; + memset(&s, 0, sizeof(STACKFRAME64)); + + // TODO: 32 bit context capture + s.AddrStack.Mode = AddrModeFlat; + s.AddrFrame.Mode = AddrModeFlat; + s.AddrPC.Mode = AddrModeFlat; +#ifdef _M_X64 + s.AddrPC.Offset = ctx_->Rip; + s.AddrStack.Offset = ctx_->Rsp; + s.AddrFrame.Offset = ctx_->Rbp; +#else + s.AddrPC.Offset = ctx_->Eip; + s.AddrStack.Offset = ctx_->Esp; + s.AddrFrame.Offset = ctx_->Ebp; +#endif + + if (!machine_type_) { +#ifdef _M_X64 + machine_type_ = IMAGE_FILE_MACHINE_AMD64; +#else + machine_type_ = IMAGE_FILE_MACHINE_I386; +#endif + } + + for (;;) { + // NOTE: this only works if PDBs are already loaded! + SetLastError(0); + if (!StackWalk64(machine_type_, process, thd_, &s, ctx_, NULL, + SymFunctionTableAccess64, SymGetModuleBase64, NULL)) + break; + + if (s.AddrReturn.Offset == 0) + break; + + _stacktrace.push_back(reinterpret_cast<void *>(s.AddrPC.Offset)); + + if (size() >= depth) + break; + } + + return size(); + } + + size_t load_from(void *addr, size_t depth = 32) { + load_here(depth + 8); + + for (size_t i = 0; i < _stacktrace.size(); ++i) { + if (_stacktrace[i] == addr) { + skip_n_firsts(i); + break; + } + } + + _stacktrace.resize(std::min(_stacktrace.size(), skip_n_firsts() + depth)); + return size(); + } + +private: + DWORD machine_type_ = 0; + HANDLE thd_ = 0; + CONTEXT *ctx_ = nullptr; +}; + +#endif class StackTrace : public StackTraceImpl<system_tag::current_tag> {}; @@ -3068,6 +3219,132 @@ class TraceResolverImpl<system_tag::darwin_tag> #endif // BACKWARD_SYSTEM_DARWIN +#ifdef BACKWARD_SYSTEM_WINDOWS + +// Load all symbol info +// Based on: +// https://stackoverflow.com/questions/6205981/windows-c-stack-trace-from-a-running-app/28276227#28276227 + +struct module_data { + std::string image_name; + std::string module_name; + void *base_address; + DWORD load_size; +}; + +class get_mod_info { + HANDLE process; + static const int buffer_length = 4096; + +public: + get_mod_info(HANDLE h) : process(h) {} + + module_data operator()(HMODULE module) { + module_data ret; + char temp[buffer_length]; + MODULEINFO mi; + + GetModuleInformation(process, module, &mi, sizeof(mi)); + ret.base_address = mi.lpBaseOfDll; + ret.load_size = mi.SizeOfImage; + + GetModuleFileNameEx(process, module, temp, sizeof(temp)); + ret.image_name = temp; + GetModuleBaseName(process, module, temp, sizeof(temp)); + ret.module_name = temp; + std::vector<char> img(ret.image_name.begin(), ret.image_name.end()); + std::vector<char> mod(ret.module_name.begin(), ret.module_name.end()); + SymLoadModule64(process, 0, &img[0], &mod[0], (DWORD64)ret.base_address, + ret.load_size); + return ret; + } +}; + +template <> class TraceResolverImpl<system_tag::windows_tag> { +public: + TraceResolverImpl() { + + HANDLE process = GetCurrentProcess(); + + std::vector<module_data> modules; + DWORD cbNeeded; + std::vector<HMODULE> module_handles(1); + SymInitialize(process, NULL, false); + DWORD symOptions = SymGetOptions(); + symOptions |= SYMOPT_LOAD_LINES | SYMOPT_UNDNAME; + SymSetOptions(symOptions); + EnumProcessModules(process, &module_handles[0], + module_handles.size() * sizeof(HMODULE), &cbNeeded); + module_handles.resize(cbNeeded / sizeof(HMODULE)); + EnumProcessModules(process, &module_handles[0], + module_handles.size() * sizeof(HMODULE), &cbNeeded); + std::transform(module_handles.begin(), module_handles.end(), + std::back_inserter(modules), get_mod_info(process)); + void *base = modules[0].base_address; + IMAGE_NT_HEADERS *h = ImageNtHeader(base); + image_type = h->FileHeader.Machine; + } + + template <class ST> void load_stacktrace(ST &) {} + + static const int max_sym_len = 255; + struct symbol_t { + SYMBOL_INFO sym; + char buffer[max_sym_len]; + } sym; + + DWORD64 displacement; + + ResolvedTrace resolve(ResolvedTrace t) { + HANDLE process = GetCurrentProcess(); + + char name[256]; + + memset(&sym, sizeof(sym), 0); + sym.sym.SizeOfStruct = sizeof(SYMBOL_INFO); + sym.sym.MaxNameLen = max_sym_len; + + if (!SymFromAddr(process, (ULONG64)t.addr, &displacement, &sym.sym)) { + // TODO: error handling everywhere + LPTSTR lpMsgBuf; + DWORD dw = GetLastError(); + + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR)&lpMsgBuf, 0, NULL); + + printf(lpMsgBuf); + + // abort(); + } + UnDecorateSymbolName(sym.sym.Name, (PSTR)name, 256, UNDNAME_COMPLETE); + + DWORD offset = 0; + IMAGEHLP_LINE line; + if (SymGetLineFromAddr(process, (ULONG64)t.addr, &offset, &line)) { + t.object_filename = line.FileName; + t.source.filename = line.FileName; + t.source.line = line.LineNumber; + t.source.col = offset; + } + + t.source.function = name; + t.object_filename = ""; + t.object_function = name; + + return t; + } + + DWORD machine_type() const { return image_type; } + +private: + DWORD image_type; +}; + +#endif + class TraceResolver : public TraceResolverImpl<system_tag::current_tag> {}; /*************** CODE SNIPPET ***************/ @@ -3379,6 +3656,8 @@ public: return os; } + TraceResolver const &resolver() const { return _resolver; } + private: TraceResolver _resolver; SnippetFactory _snippets; @@ -3623,6 +3902,179 @@ private: #endif // BACKWARD_SYSTEM_LINUX || BACKWARD_SYSTEM_DARWIN +#ifdef BACKWARD_SYSTEM_WINDOWS + +class SignalHandling { +public: + SignalHandling(const std::vector<int> & = std::vector<int>()) + : reporter_thread_([]() { + /* We handle crashes in a utility thread: + backward structures and some Windows functions called here + need stack space, which we do not have when we encounter a + stack overflow. + To support reporting stack traces during a stack overflow, + we create a utility thread at startup, which waits until a + crash happens or the program exits normally. */ + + { + std::unique_lock<std::mutex> lk(mtx()); + cv().wait(lk, [] { return crashed() != crash_status::running; }); + } + if (crashed() == crash_status::crashed) { + handle_stacktrace(skip_recs()); + } + { + std::unique_lock<std::mutex> lk(mtx()); + crashed() = crash_status::ending; + } + cv().notify_one(); + }) { + SetUnhandledExceptionFilter(crash_handler); + + signal(SIGABRT, signal_handler); + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + + set_terminate(&terminator); + set_unexpected(&terminator); + _set_purecall_handler(&terminator); + _set_invalid_parameter_handler(&invalid_parameter_handler); + } + bool loaded() const { return true; } + + ~SignalHandling() { + { + std::unique_lock<std::mutex> lk(mtx()); + crashed() = crash_status::normal_exit; + } + + cv().notify_one(); + + reporter_thread_.join(); + } + +private: + static CONTEXT *ctx() { + static CONTEXT data; + return &data; + } + + enum class crash_status { running, crashed, normal_exit, ending }; + + static crash_status &crashed() { + static crash_status data; + return data; + } + + static std::mutex &mtx() { + static std::mutex data; + return data; + } + + static std::condition_variable &cv() { + static std::condition_variable data; + return data; + } + + static HANDLE &thread_handle() { + static HANDLE handle; + return handle; + } + + std::thread reporter_thread_; + + // TODO: how not to hardcode these? + static const constexpr int signal_skip_recs = +#ifdef __clang__ + // With clang, RtlCaptureContext also captures the stack frame of the + // current function Below that, there ar 3 internal Windows functions + 4 +#else + // With MSVC cl, RtlCaptureContext misses the stack frame of the current + // function The first entries during StackWalk are the 3 internal Windows + // functions + 3 +#endif + ; + + static int &skip_recs() { + static int data; + return data; + } + + static inline void terminator() { + crash_handler(signal_skip_recs); + abort(); + } + + static inline void signal_handler(int) { + crash_handler(signal_skip_recs); + abort(); + } + + static inline void __cdecl invalid_parameter_handler(const wchar_t *, + const wchar_t *, + const wchar_t *, + unsigned int, + uintptr_t) { + crash_handler(signal_skip_recs); + abort(); + } + + NOINLINE static LONG WINAPI crash_handler(EXCEPTION_POINTERS *info) { + // The exception info supplies a trace from exactly where the issue was, + // no need to skip records + crash_handler(0, info->ContextRecord); + return EXCEPTION_CONTINUE_SEARCH; + } + + NOINLINE static void crash_handler(int skip, CONTEXT *ct = nullptr) { + + if (ct == nullptr) { + RtlCaptureContext(ctx()); + } else { + memcpy(ctx(), ct, sizeof(CONTEXT)); + } + DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), + GetCurrentProcess(), &thread_handle(), 0, FALSE, + DUPLICATE_SAME_ACCESS); + + skip_recs() = skip; + + { + std::unique_lock<std::mutex> lk(mtx()); + crashed() = crash_status::crashed; + } + + cv().notify_one(); + + { + std::unique_lock<std::mutex> lk(mtx()); + cv().wait(lk, [] { return crashed() != crash_status::crashed; }); + } + } + + static void handle_stacktrace(int skip_frames = 0) { + // printer creates the TraceResolver, which can supply us a machine type + // for stack walking. Without this, StackTrace can only guess using some + // macros. + // StackTrace also requires that the PDBs are already loaded, which is done + // in the constructor of TraceResolver + Printer printer; + + StackTrace st; + st.set_machine_type(printer.resolver().machine_type()); + st.set_context(ctx()); + st.set_thread_handle(thread_handle()); + st.load_here(32 + skip_frames); + st.skip_n_firsts(skip_frames); + + printer.address = true; + printer.print(st, std::cerr); + } +}; + +#endif // BACKWARD_SYSTEM_WINDOWS + #ifdef BACKWARD_SYSTEM_UNKNOWN class SignalHandling { diff --git a/test/_test_main.cpp b/test/_test_main.cpp index 29f279f..5e1f316 100644 --- a/test/_test_main.cpp +++ b/test/_test_main.cpp @@ -24,15 +24,26 @@ #include "test.hpp" #include <cstdio> #include <cstdlib> + +#ifdef _WIN32 +#include <Windows.h> +#define strcasecmp _stricmp +#else #include <sys/wait.h> #include <unistd.h> +#endif #if defined(__has_include) && __has_include(<error.h>) #include <error.h> #else #include <stdarg.h> -#ifndef __APPLE__ +#ifdef _WIN32 +char argv0[MAX_PATH]; +inline const char *getprogname() { + return GetModuleFileName(NULL, argv0, sizeof(argv0)) ? argv0 : NULL; +} +#elif !defined(__APPLE__) // N.B. getprogname() is an Apple/BSD-ism. // program_invocation_name is a GLIBC-ism, but it's also // supported by libmusl. @@ -59,13 +70,71 @@ void error(int status, int errnum, const char *format, ...) { } #endif -test::test_registry_t test::test_registry; using namespace test; -bool run_test(TestBase &test) { +bool run_test(TestBase &test, bool use_child_process = true) { + if (!use_child_process) { + exit(static_cast<int>(test.run())); + } + printf("-- running test case: %s\n", test.name); fflush(stdout); + + test::TestStatus status = test::SUCCESS; + +#ifdef _WIN32 + char filename[256]; + GetModuleFileName(NULL, filename, 256); // TODO: check for error + std::string cmd_line = filename; + cmd_line += " --nofork "; + cmd_line += test.name; + + STARTUPINFO si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + if (!CreateProcessA(nullptr, const_cast<char *>(cmd_line.c_str()), nullptr, + nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) { + printf("unable to create process\n"); + exit(-1); + } + + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exit_code; + GetExitCodeProcess(pi.hProcess, &exit_code); + switch (exit_code) { + case 3: + status = test::SIGNAL_ABORT; + break; + case 5: + status = test::EXCEPTION_UNCAUGHT; + break; + case EXCEPTION_ACCESS_VIOLATION: + status = test::SIGNAL_SEGFAULT; + break; + case EXCEPTION_STACK_OVERFLOW: + status = test::SIGNAL_SEGFAULT; + break; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + status = test::SIGNAL_DIVZERO; + break; + } + printf("Exit code: %lu\n", exit_code); + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + if (test.expected_status == test::ASSERT_FAIL) { + // assert calls abort on windows + return (status & test::SIGNAL_ABORT); + } + +#else + pid_t child_pid = fork(); if (child_pid == 0) { exit(static_cast<int>(test.run())); @@ -77,8 +146,6 @@ bool run_test(TestBase &test) { int child_status = 0; waitpid(child_pid, &child_status, 0); - test::TestStatus status; - if (WIFEXITED(child_status)) { int exit_status = WEXITSTATUS(child_status); if (exit_status & ~test::STATUS_MASK) { @@ -103,10 +170,10 @@ bool run_test(TestBase &test) { default: status = test::SIGNAL_UNCAUGHT; } - } else { - status = test::SUCCESS; } +#endif + if (test.expected_status == test::FAILED) { return (status & test::FAILED); } @@ -120,6 +187,25 @@ bool run_test(TestBase &test) { int main(int argc, const char *const argv[]) { +#ifdef _WIN32 + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#endif + + if (argc == 3 && strcmp("--nofork", argv[1]) == 0) { + // Windows has no fork, so we simulate it + // we only execute one test, without forking + for (test_registry_t::iterator it = test_registry().begin(); + it != test_registry().end(); ++it) { + TestBase &test = **it; + if (strcasecmp(argv[2], test.name) == 0) { + run_test(test, false); + + return 0; + } + } + return -1; + } + size_t success_cnt = 0; size_t total_cnt = 0; for (test_registry_t::iterator it = test_registry().begin(); @@ -145,9 +231,9 @@ int main(int argc, const char *const argv[]) { printf("** test case FAILED : %s\n", test.name); } } - printf("-- tests passing: %lu/%lu", success_cnt, total_cnt); + printf("-- tests passing: %zu/%zu", success_cnt, total_cnt); if (total_cnt) { - printf(" (%lu%%)\n", success_cnt * 100 / total_cnt); + printf(" (%zu%%)\n", success_cnt * 100 / total_cnt); } else { printf("\n"); } diff --git a/test/stacktrace.cpp b/test/stacktrace.cpp index 812ae0d..47084fe 100644 --- a/test/stacktrace.cpp +++ b/test/stacktrace.cpp @@ -24,17 +24,19 @@ #include "backward.hpp" #include "test/test.hpp" #include <cstdio> +#include <iostream> using namespace backward; void collect_trace(StackTrace &st) { st.load_here(); } TEST(minitrace) { + Printer printer; + StackTrace st; collect_trace(st); - Printer printer; - printer.print(st, stdout); + printer.print(st, std::cout); } void d(StackTrace &st) { st.load_here(); } @@ -46,9 +48,10 @@ void b(StackTrace &st) { return c(st); } NOINLINE void a(StackTrace &st) { return b(st); } TEST(smalltrace) { + Printer printer; + StackTrace st; a(st); - Printer printer; - printer.print(st, stdout); + printer.print(st, std::cout); } diff --git a/test/suicide.cpp b/test/suicide.cpp index 315fc7f..aa8022c 100644 --- a/test/suicide.cpp +++ b/test/suicide.cpp @@ -25,7 +25,10 @@ #include "test/test.hpp" #include <cstdio> + +#ifndef _WIN32 #include <sys/resource.h> +#endif using namespace backward; @@ -75,9 +78,11 @@ TEST_DIVZERO(divide_by_zero) { int bye_bye_stack(int i) { return bye_bye_stack(i + 1) + bye_bye_stack(i * 2); } TEST_SEGFAULT(stackoverflow) { +#ifndef _WIN32 struct rlimit limit; limit.rlim_max = 8096; setrlimit(RLIMIT_STACK, &limit); +#endif int r = bye_bye_stack(42); std::cout << "r=" << r << std::endl; }