From 3e0bb6d8be22efdcd8dcad91859ad6b487aa5623 Mon Sep 17 00:00:00 2001 From: WerWolv Date: Sat, 1 Jun 2024 16:36:36 +0200 Subject: [PATCH] feat: Added initial support for tracing function calls and printing exception stack traces --- CMakeLists.txt | 1 + cmake/build_helpers.cmake | 24 ---- lib/libimhex/CMakeLists.txt | 2 +- lib/trace/CMakeLists.txt | 61 +++++++++ lib/trace/include/hex/trace/exceptions.hpp | 11 ++ .../include/hex/trace/instrumentation.hpp | 6 + .../trace/include/hex/trace}/stacktrace.hpp | 12 +- lib/trace/source/exceptions.cpp | 27 ++++ lib/trace/source/instr_entry.cpp | 28 ++++ lib/trace/source/instrumentation.cpp | 13 ++ {main/gui => lib/trace}/source/stacktrace.cpp | 78 +++++++---- main/gui/CMakeLists.txt | 3 +- main/gui/include/window.hpp | 3 + main/gui/source/crash_handlers.cpp | 6 +- main/gui/source/window/window.cpp | 121 +++++++++--------- plugins/builtin/source/content/events.cpp | 9 ++ 16 files changed, 286 insertions(+), 119 deletions(-) create mode 100644 lib/trace/CMakeLists.txt create mode 100644 lib/trace/include/hex/trace/exceptions.hpp create mode 100644 lib/trace/include/hex/trace/instrumentation.hpp rename {main/gui/include => lib/trace/include/hex/trace}/stacktrace.hpp (52%) create mode 100644 lib/trace/source/exceptions.cpp create mode 100644 lib/trace/source/instr_entry.cpp create mode 100644 lib/trace/source/instrumentation.cpp rename {main/gui => lib/trace}/source/stacktrace.cpp (76%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cfd610f9..64a01823a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ addBundledLibraries() add_subdirectory(lib/libimhex) add_subdirectory(main) addPluginDirectories() +add_subdirectory(lib/trace) # Add unit tests if (IMHEX_ENABLE_UNIT_TESTS) diff --git a/cmake/build_helpers.cmake b/cmake/build_helpers.cmake index 1a989c9f5..c2ce8c12d 100644 --- a/cmake/build_helpers.cmake +++ b/cmake/build_helpers.cmake @@ -734,30 +734,6 @@ macro(addBundledLibraries) find_package(mbedTLS 3.4.0 REQUIRED) find_package(Magic 5.39 REQUIRED) - - if (NOT IMHEX_DISABLE_STACKTRACE) - if (WIN32) - message(STATUS "StackWalk enabled!") - set(LIBBACKTRACE_LIBRARIES DbgHelp.lib) - else () - find_package(Backtrace) - if (${Backtrace_FOUND}) - message(STATUS "Backtrace enabled! Header: ${Backtrace_HEADER}") - - if (Backtrace_HEADER STREQUAL "backtrace.h") - set(LIBBACKTRACE_LIBRARIES ${Backtrace_LIBRARY}) - set(LIBBACKTRACE_INCLUDE_DIRS ${Backtrace_INCLUDE_DIR}) - add_compile_definitions(BACKTRACE_HEADER=<${Backtrace_HEADER}>) - add_compile_definitions(HEX_HAS_BACKTRACE) - elseif (Backtrace_HEADER STREQUAL "execinfo.h") - set(LIBBACKTRACE_LIBRARIES ${Backtrace_LIBRARY}) - set(LIBBACKTRACE_INCLUDE_DIRS ${Backtrace_INCLUDE_DIR}) - add_compile_definitions(BACKTRACE_HEADER=<${Backtrace_HEADER}>) - add_compile_definitions(HEX_HAS_EXECINFO) - endif() - endif() - endif() - endif() endmacro() function(enableUnityBuild TARGET) diff --git a/lib/libimhex/CMakeLists.txt b/lib/libimhex/CMakeLists.txt index 478a67065..351e7d6a3 100644 --- a/lib/libimhex/CMakeLists.txt +++ b/lib/libimhex/CMakeLists.txt @@ -142,7 +142,7 @@ if (NOT IMHEX_EXTERNAL_PLUGIN_BUILD) precompileHeaders(libimhex "${CMAKE_CURRENT_SOURCE_DIR}/include") endif() -target_link_libraries(libimhex ${LIBIMHEX_LIBRARY_TYPE} ${NLOHMANN_JSON_LIBRARIES} imgui_all_includes ${MBEDTLS_LIBRARIES} ${FMT_LIBRARIES} ${LUNASVG_LIBRARIES}) +target_link_libraries(libimhex ${LIBIMHEX_LIBRARY_TYPE} ${NLOHMANN_JSON_LIBRARIES} imgui_all_includes ${MBEDTLS_LIBRARIES} ${FMT_LIBRARIES} ${LUNASVG_LIBRARIES} tracing) set_property(TARGET libimhex PROPERTY INTERPROCEDURAL_OPTIMIZATION FALSE) diff --git a/lib/trace/CMakeLists.txt b/lib/trace/CMakeLists.txt new file mode 100644 index 000000000..7ad260d1a --- /dev/null +++ b/lib/trace/CMakeLists.txt @@ -0,0 +1,61 @@ +project(tracing) + +option(IMHEX_TRACE_EXCEPTIONS "Hook thrown exceptions to display a stack trace when possible" ON) +option(IMHEX_INSTRUMENT_FUNCTIONS "Hook all function entries and exits to profile things in Tracy" OFF) + +add_library(tracing OBJECT + source/stacktrace.cpp + source/exceptions.cpp +) +target_include_directories(tracing PUBLIC include) +target_link_libraries(tracing PRIVATE stdc++exp) + +if (NOT IMHEX_DISABLE_STACKTRACE) + if (WIN32) + message(STATUS "StackWalk enabled!") + target_link_libraries(tracing PRIVATE DbgHelp.lib) + else () + find_package(Backtrace) + if (${Backtrace_FOUND}) + message(STATUS "Backtrace enabled! Header: ${Backtrace_HEADER}") + + if (Backtrace_HEADER STREQUAL "backtrace.h") + target_link_libraries(tracing PRIVATE ${Backtrace_LIBRARY}) + target_include_directories(tracing PRIVATE ${Backtrace_INCLUDE_DIR}) + target_compile_definitions(tracing PRIVATE BACKTRACE_HEADER=<${Backtrace_HEADER}>) + target_compile_definitions(tracing PRIVATE HEX_HAS_BACKTRACE) + elseif (Backtrace_HEADER STREQUAL "execinfo.h") + target_link_libraries(tracing PRIVATE ${Backtrace_LIBRARY}) + target_include_directories(tracing PRIVATE ${Backtrace_INCLUDE_DIR}) + target_compile_definitions(tracing PRIVATE BACKTRACE_HEADER=<${Backtrace_HEADER}>) + target_compile_definitions(tracing PRIVATE HEX_HAS_EXECINFO) + endif() + endif() + endif() + + target_link_libraries(tracing PRIVATE LLVMDemangle) +endif() + +if (IMHEX_TRACE_EXCEPTIONS) + target_link_options(tracing PUBLIC "-Wl,--wrap=__cxa_throw") +endif() + +if (IMHEX_INSTRUMENT_FUNCTIONS) + target_sources(tracing PUBLIC + source/instr_entry.cpp + source/instrumentation.cpp + ) + + FetchContent_Declare( + tracy + GIT_REPOSITORY https://github.com/wolfpld/tracy.git + GIT_TAG v0.10 + GIT_SHALLOW TRUE + GIT_PROGRESS TRUE + ) + FetchContent_MakeAvailable(tracy) + target_compile_options(TracyClient PRIVATE "-Wno-error") + + target_compile_options(tracing PUBLIC "-finstrument-functions") + target_link_libraries(tracing PRIVATE TracyClient) +endif() diff --git a/lib/trace/include/hex/trace/exceptions.hpp b/lib/trace/include/hex/trace/exceptions.hpp new file mode 100644 index 000000000..2490ecc1d --- /dev/null +++ b/lib/trace/include/hex/trace/exceptions.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include + +namespace hex::trace { + + std::optional getLastExceptionStackTrace(); + +} \ No newline at end of file diff --git a/lib/trace/include/hex/trace/instrumentation.hpp b/lib/trace/include/hex/trace/instrumentation.hpp new file mode 100644 index 000000000..dac38ca9b --- /dev/null +++ b/lib/trace/include/hex/trace/instrumentation.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + + diff --git a/main/gui/include/stacktrace.hpp b/lib/trace/include/hex/trace/stacktrace.hpp similarity index 52% rename from main/gui/include/stacktrace.hpp rename to lib/trace/include/hex/trace/stacktrace.hpp index d0b8250e0..2487aaa63 100644 --- a/main/gui/include/stacktrace.hpp +++ b/lib/trace/include/hex/trace/stacktrace.hpp @@ -1,20 +1,20 @@ #pragma once -#include - +#include #include #include -namespace hex::stacktrace { +namespace hex::trace { struct StackFrame { std::string file; std::string function; - u32 line; + std::uint32_t line; }; - void initialize(); + using StackTrace = std::vector; - std::vector getStackTrace(); + void initialize(); + StackTrace getStackTrace(); } \ No newline at end of file diff --git a/lib/trace/source/exceptions.cpp b/lib/trace/source/exceptions.cpp new file mode 100644 index 000000000..f89bb9dde --- /dev/null +++ b/lib/trace/source/exceptions.cpp @@ -0,0 +1,27 @@ +#include + +namespace hex::trace { + + static std::optional s_lastExceptionStackTrace; + std::optional getLastExceptionStackTrace() { + if (!s_lastExceptionStackTrace.has_value()) + return std::nullopt; + + auto result = s_lastExceptionStackTrace.value(); + s_lastExceptionStackTrace.reset(); + + return result; + } + +} + +extern "C" { + + [[noreturn]] void __real___cxa_throw(void* thrownException, void* type, void (*destructor)(void*)); + [[noreturn]] void __wrap___cxa_throw(void* thrownException, void* type, void (*destructor)(void*)) { + hex::trace::s_lastExceptionStackTrace = hex::trace::getStackTrace(); + __real___cxa_throw(thrownException, type, destructor); + } + +} + diff --git a/lib/trace/source/instr_entry.cpp b/lib/trace/source/instr_entry.cpp new file mode 100644 index 000000000..e255f0143 --- /dev/null +++ b/lib/trace/source/instr_entry.cpp @@ -0,0 +1,28 @@ +#include + +namespace hex { + + void functionEntry(void *); + void functionExit(void *); + +} + +extern "C" { + + static std::mutex s_mutex; + + [[gnu::no_instrument_function]] + void __cyg_profile_func_enter(void *functionAddress, void *) { + std::scoped_lock lock(s_mutex); + + hex::functionEntry(functionAddress); + } + + [[gnu::no_instrument_function]] + void __cyg_profile_func_exit(void *functionAddress, void *) { + std::scoped_lock lock(s_mutex); + + hex::functionExit(functionAddress); + } + +} \ No newline at end of file diff --git a/lib/trace/source/instrumentation.cpp b/lib/trace/source/instrumentation.cpp new file mode 100644 index 000000000..c540104d1 --- /dev/null +++ b/lib/trace/source/instrumentation.cpp @@ -0,0 +1,13 @@ +#include + +namespace hex { + + void functionEntry([[maybe_unused]] void *functionAddress) { + + } + + void functionExit([[maybe_unused]] void *functionAddress) { + + } + +} \ No newline at end of file diff --git a/main/gui/source/stacktrace.cpp b/lib/trace/source/stacktrace.cpp similarity index 76% rename from main/gui/source/stacktrace.cpp rename to lib/trace/source/stacktrace.cpp index 45c3e4307..1d68fb1df 100644 --- a/main/gui/source/stacktrace.cpp +++ b/lib/trace/source/stacktrace.cpp @@ -1,7 +1,6 @@ -#include -#include +#include +#include -#include #include namespace { @@ -18,19 +17,48 @@ namespace { } -#if defined(OS_WINDOWS) - #include - #include +#if __has_include() - namespace hex::stacktrace { + #include + + namespace hex::trace { void initialize() { } - std::vector getStackTrace() { - std::vector stackTrace; + StackTrace getStackTrace() { + StackTrace result; + + auto stackTrace = std::stacktrace::current(); + + for (const auto &entry : stackTrace) { + if (entry.source_line() == 0 && entry.source_file().empty()) + result.emplace_back("", "??", 0); + else + result.emplace_back(entry.source_file(), entry.description(), entry.source_line()); + } + + return result; + } + + } + +#elif defined(OS_WINDOWS) + + #include + #include + #include + + namespace hex::trace { + + void initialize() { + + } + + StackTrace getStackTrace() { + StackTrace stackTrace; HANDLE process = GetCurrentProcess(); HANDLE thread = GetCurrentThread(); @@ -84,7 +112,7 @@ namespace { DWORD displacementLine = 0; - u32 lineNumber = 0; + std::uint32_t lineNumber = 0; const char *fileName; if (SymGetLineFromAddr64(process, stackFrame.AddrPC.Offset, &displacementLine, &line) == TRUE) { lineNumber = line.LineNumber; @@ -110,17 +138,18 @@ namespace { #if __has_include(BACKTRACE_HEADER) #include BACKTRACE_HEADER - #include + #include #include + #include - namespace hex::stacktrace { + namespace hex::trace { void initialize() { } - std::vector getStackTrace() { - static std::vector result; + StackTrace getStackTrace() { + StackTrace result; std::array addresses = {}; const size_t count = backtrace(addresses.data(), addresses.size()); @@ -129,7 +158,7 @@ namespace { for (size_t i = 0; i < count; i += 1) { dladdr(addresses[i], &info); - auto fileName = info.dli_fname != nullptr ? std::fs::path(info.dli_fname).filename().string() : "??"; + auto fileName = info.dli_fname != nullptr ? std::filesystem::path(info.dli_fname).filename().string() : "??"; auto demangledName = info.dli_sname != nullptr ? tryDemangle(info.dli_sname) : "??"; result.push_back(StackFrame { std::move(fileName), std::move(demangledName), 0 }); @@ -147,10 +176,10 @@ namespace { #if __has_include(BACKTRACE_HEADER) #include BACKTRACE_HEADER - #include - #include - namespace hex::stacktrace { + #include + + namespace hex::trace { static struct backtrace_state *s_backtraceState; @@ -158,14 +187,13 @@ namespace { void initialize() { if (auto executablePath = wolv::io::fs::getExecutablePath(); executablePath.has_value()) { static std::string path = executablePath->string(); - s_backtraceState = backtrace_create_state(path.c_str(), 1, [](void *, const char *msg, int) { log::error("{}", msg); }, nullptr); + s_backtraceState = backtrace_create_state(path.c_str(), 1, [](void *, const char *, int) { }, nullptr); } } - std::vector getStackTrace() { - static std::vector result; + StackTrace getStackTrace() { + StackTrace result; - result.clear(); if (s_backtraceState != nullptr) { backtrace_full(s_backtraceState, 0, [](void *, uintptr_t, const char *fileName, int lineNumber, const char *function) -> int { if (fileName == nullptr) @@ -173,7 +201,7 @@ namespace { if (function == nullptr) function = "??"; - result.push_back(StackFrame { std::fs::path(fileName).filename().string(), tryDemangle(function), u32(lineNumber) }); + result.push_back(StackFrame { std::filesystem::path(fileName).filename().string(), tryDemangle(function), std::uint32_t(lineNumber) }); return 0; }, nullptr, nullptr); @@ -189,10 +217,10 @@ namespace { #else - namespace hex::stacktrace { + namespace hex::trace { void initialize() { } - std::vector getStackTrace() { return { StackFrame { "??", "Stacktrace collecting not available!", 0 } }; } + StackTrace getStackTrace() { return { StackFrame { "??", "Stacktrace collecting not available!", 0 } }; } } diff --git a/main/gui/CMakeLists.txt b/main/gui/CMakeLists.txt index bae7eaab7..ec10adadc 100644 --- a/main/gui/CMakeLists.txt +++ b/main/gui/CMakeLists.txt @@ -3,7 +3,6 @@ project(main) add_executable(main ${APPLICATION_TYPE} source/main.cpp source/crash_handlers.cpp - source/stacktrace.cpp source/window/window.cpp source/window/win_window.cpp @@ -69,4 +68,4 @@ precompileHeaders(main ${CMAKE_CURRENT_SOURCE_DIR}/include) if (APPLE) add_compile_definitions(GL_SILENCE_DEPRECATION) -endif () +endif () \ No newline at end of file diff --git a/main/gui/include/window.hpp b/main/gui/include/window.hpp index 34aa08d66..fbd592c56 100644 --- a/main/gui/include/window.hpp +++ b/main/gui/include/window.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -34,6 +36,7 @@ namespace hex { void beginNativeWindowFrame(); void endNativeWindowFrame(); void drawTitleBar(); + void drawView(const std::string &name, const std::unique_ptr &view); void frameBegin(); void frame(); diff --git a/main/gui/source/crash_handlers.cpp b/main/gui/source/crash_handlers.cpp index 59987984d..55b71ba87 100644 --- a/main/gui/source/crash_handlers.cpp +++ b/main/gui/source/crash_handlers.cpp @@ -13,7 +13,7 @@ #include -#include +#include #include #include @@ -63,7 +63,7 @@ namespace hex::crash { } static void printStackTrace() { - for (const auto &stackFrame : stacktrace::getStackTrace()) { + for (const auto &stackFrame : trace::getStackTrace()) { if (stackFrame.line == 0) log::fatal(" ({}) | {}", stackFrame.file, stackFrame.function); else @@ -153,7 +153,7 @@ namespace hex::crash { // Setup functions to handle signals, uncaught exception, or similar stuff that will crash ImHex void setupCrashHandlers() { - stacktrace::initialize(); + trace::initialize(); // Register signal handlers { diff --git a/main/gui/source/window/window.cpp b/main/gui/source/window/window.cpp index c3520b5c7..945f25635 100644 --- a/main/gui/source/window/window.cpp +++ b/main/gui/source/window/window.cpp @@ -546,72 +546,76 @@ namespace hex { TaskManager::runDeferredCalls(); } - void Window::frame() { - auto &io = ImGui::GetIO(); + void Window::drawView(const std::string &name, const std::unique_ptr &view) { + // Draw always visible views + view->drawAlwaysVisibleContent(); + // Skip views that shouldn't be processed currently + if (!view->shouldProcess()) + return; + + const auto openViewCount = std::ranges::count_if(ContentRegistry::Views::impl::getEntries(), [](const auto &entry) { + const auto &[unlocalizedName, openView] = entry; + + return openView->hasViewMenuItemEntry() && openView->shouldProcess(); + }); + + ImGuiWindowClass windowClass = {}; + + windowClass.DockNodeFlagsOverrideSet |= ImGuiDockNodeFlags_NoCloseButton; + + if (openViewCount <= 1 || LayoutManager::isLayoutLocked()) + windowClass.DockNodeFlagsOverrideSet |= ImGuiDockNodeFlags_NoTabBar; + + ImGui::SetNextWindowClass(&windowClass); + + auto window = ImGui::FindWindowByName(view->getName().c_str()); + if (window != nullptr && window->DockNode == nullptr) + ImGui::SetNextWindowBgAlpha(1.0F); + + // Draw view + view->draw(); + view->trackViewOpenState(); + + if (view->getWindowOpenState()) { + bool hasWindow = window != nullptr; + bool focused = false; + + // Get the currently focused view + if (hasWindow && (window->Flags & ImGuiWindowFlags_Popup) != ImGuiWindowFlags_Popup) { + auto windowName = View::toWindowName(name); + ImGui::Begin(windowName.c_str()); + + // Detect if the window is focused + focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_NoPopupHierarchy); + + // Dock the window if it's not already docked + if (view->didWindowJustOpen() && !ImGui::IsWindowDocked()) { + ImGui::DockBuilderDockWindow(windowName.c_str(), ImHexApi::System::getMainDockSpaceId()); + EventViewOpened::post(view.get()); + } + + ImGui::End(); + } + + // Pass on currently pressed keys to the shortcut handler + auto &io = ImGui::GetIO(); + for (const auto &key : m_pressedKeys) { + ShortcutManager::process(view.get(), io.KeyCtrl, io.KeyAlt, io.KeyShift, io.KeySuper, focused, key); + } + } + } + + void Window::frame() { // Loop through all views and draw them for (auto &[name, view] : ContentRegistry::Views::impl::getEntries()) { ImGui::GetCurrentContext()->NextWindowData.ClearFlags(); - // Draw always visible views - view->drawAlwaysVisibleContent(); - - // Skip views that shouldn't be processed currently - if (!view->shouldProcess()) - continue; - - const auto openViewCount = std::ranges::count_if(ContentRegistry::Views::impl::getEntries(), [](const auto &entry) { - const auto &[unlocalizedName, openView] = entry; - - return openView->hasViewMenuItemEntry() && openView->shouldProcess(); - }); - - ImGuiWindowClass windowClass = {}; - - windowClass.DockNodeFlagsOverrideSet |= ImGuiDockNodeFlags_NoCloseButton; - - if (openViewCount <= 1 || LayoutManager::isLayoutLocked()) - windowClass.DockNodeFlagsOverrideSet |= ImGuiDockNodeFlags_NoTabBar; - - ImGui::SetNextWindowClass(&windowClass); - - auto window = ImGui::FindWindowByName(view->getName().c_str()); - if (window != nullptr && window->DockNode == nullptr) - ImGui::SetNextWindowBgAlpha(1.0F); - - // Draw view - view->draw(); - view->trackViewOpenState(); - - if (view->getWindowOpenState()) { - bool hasWindow = window != nullptr; - bool focused = false; - - // Get the currently focused view - if (hasWindow && (window->Flags & ImGuiWindowFlags_Popup) != ImGuiWindowFlags_Popup) { - auto windowName = View::toWindowName(name); - ImGui::Begin(windowName.c_str()); - - // Detect if the window is focused - focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_NoPopupHierarchy); - - // Dock the window if it's not already docked - if (view->didWindowJustOpen() && !ImGui::IsWindowDocked()) { - ImGui::DockBuilderDockWindow(windowName.c_str(), ImHexApi::System::getMainDockSpaceId()); - EventViewOpened::post(view.get()); - } - - ImGui::End(); - } - - // Pass on currently pressed keys to the shortcut handler - for (const auto &key : m_pressedKeys) { - ShortcutManager::process(view.get(), io.KeyCtrl, io.KeyAlt, io.KeyShift, io.KeySuper, focused, key); - } - } + drawView(name, view); } // Handle global shortcuts + auto &io = ImGui::GetIO(); for (const auto &key : m_pressedKeys) { ShortcutManager::processGlobals(io.KeyCtrl, io.KeyAlt, io.KeyShift, io.KeySuper, key); } @@ -738,6 +742,7 @@ namespace hex { glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); + glfwWindowHint(GLFW_SRGB_CAPABLE, GLFW_TRUE); if (initialWindowProperties.has_value()) { glfwWindowHint(GLFW_MAXIMIZED, initialWindowProperties->maximized); diff --git a/plugins/builtin/source/content/events.cpp b/plugins/builtin/source/content/events.cpp index 05e942b7e..6f356169b 100644 --- a/plugins/builtin/source/content/events.cpp +++ b/plugins/builtin/source/content/events.cpp @@ -6,6 +6,8 @@ #include #include +#include + #include #include @@ -53,6 +55,13 @@ namespace hex::plugin::builtin { static bool imhexClosing = false; EventCrashRecovered::subscribe([](const std::exception &e) { PopupCrashRecovered::open(e); + + auto stackTrace = hex::trace::getLastExceptionStackTrace(); + if (stackTrace.has_value()) { + for (const auto &entry : *stackTrace) { + hex::log::fatal(" {} at {}:{}", entry.function, entry.file, entry.line); + } + } }); EventWindowClosing::subscribe([](GLFWwindow *window) {