diff --git a/CMakeLists.txt b/CMakeLists.txt index ac20fda..ae9a43a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,124 +1,132 @@ if (CMAKE_VERSION VERSION_LESS "2.8.12") cmake_minimum_required(VERSION 2.8.9) set(HEAPTRACK_BUILD_GUI OFF) else() cmake_minimum_required(VERSION 2.8.12) endif() project(heaptrack) enable_testing() if(NOT CMAKE_BUILD_TYPE) message(STATUS "Setting build type to 'RelWithDebInfo' as none was specified.") set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build." FORCE) endif() set(HEAPTRACK_VERSION_MAJOR 1) set(HEAPTRACK_VERSION_MINOR 1) set(HEAPTRACK_VERSION_PATCH 80) set(HEAPTRACK_LIB_VERSION 1.1.80) set(HEAPTRACK_LIB_SOVERSION 2) set(HEAPTRACK_FILE_FORMAT_VERSION 2) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) include(FeatureSummary) find_package(Boost 1.41.0 COMPONENTS system filesystem iostreams) find_package(Threads REQUIRED) find_package(ZLIB REQUIRED) if (${Boost_IOSTREAMS_FOUND}) find_package(Zstd) endif() set_package_properties(Zstd PROPERTIES TYPE RECOMMENDED PURPOSE "Zstandard offers better (de)compression performance compared with gzip/zlib, making heaptrack faster and datafiles smaller.") if (CMAKE_SYSTEM_NAME STREQUAL "Linux") set(HEAPTRACK_BUILD_TRACK_DEFAULT ON) else() set(HEAPTRACK_BUILD_TRACK_DEFAULT OFF) endif() option( HEAPTRACK_BUILD_TRACK "Disable this option to skip building the tracker part for heaptrack, e.g. to only build the GUI." ${HEAPTRACK_BUILD_TRACK_DEFAULT} ) if (CMAKE_CROSSCOMPILING) set(HEAPTRACK_BUILD_ANALYZE_DEFAULT OFF) else() set(HEAPTRACK_BUILD_ANALYZE_DEFAULT ON) endif() option( HEAPTRACK_BUILD_PRINT "Disable this option to skip building heaptrack_print, e.g. when you're cross-compiling." ${HEAPTRACK_BUILD_ANALYZE_DEFAULT} ) option( HEAPTRACK_BUILD_GUI "Disable this option to skip building the Qt5 / KF5 based GUI for heaptrack." ${HEAPTRACK_BUILD_ANALYZE_DEFAULT} ) +option( + HEAPTRACK_USE_LIBUNWIND + "Define preferred unwind functionality - Libunwind as ON and unwind_tables as OFF." + ON +) + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) if (NOT MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -Wpedantic") endif() include (CheckCXXSourceCompiles) check_cxx_source_compiles( "#include #include thread_local int tls; int main() { return 0; }" HAVE_CXX11_SUPPORT) if (NOT HAVE_CXX11_SUPPORT) message(FATAL_ERROR "Your compiler is too old and does not support the required C++11 features.") endif() # cfree() does not exist in glibc 2.26+. # See: https://bugs.kde.org/show_bug.cgi?id=383889 include(CheckSymbolExists) check_symbol_exists(cfree malloc.h HAVE_CFREE) set(BIN_INSTALL_DIR "bin") set(LIB_SUFFIX "" CACHE STRING "Define suffix of directory name (32/64)") set(LIB_INSTALL_DIR "lib${LIB_SUFFIX}") set(LIBEXEC_INSTALL_DIR "${LIB_INSTALL_DIR}/heaptrack/libexec") file(RELATIVE_PATH LIBEXEC_REL_PATH "${CMAKE_INSTALL_PREFIX}/${BIN_INSTALL_DIR}" "${CMAKE_INSTALL_PREFIX}/${LIBEXEC_INSTALL_DIR}") file(RELATIVE_PATH LIB_REL_PATH "${CMAKE_INSTALL_PREFIX}/${BIN_INSTALL_DIR}" "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}/heaptrack") set(ECM_ENABLE_SANITIZERS "" CACHE STRING "semicolon-separated list of sanitizers to enable for code that is not injected into client applications") if (HEAPTRACK_BUILD_TRACK) - find_package(Libunwind REQUIRED) + if (HEAPTRACK_USE_LIBUNWIND) + find_package(Libunwind REQUIRED) + endif() check_cxx_source_compiles( "#include #include #include #include int main() { return 0; }" HAVE_LINUX_HEADERS) if (NOT HAVE_LINUX_HEADERS) message(FATAL_ERROR "You are missing some Linux headers required to compile heaptrack.") endif() endif() add_subdirectory(3rdparty) add_subdirectory(src) add_subdirectory(tests) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/track/CMakeLists.txt b/src/track/CMakeLists.txt index f189bea..515616d 100644 --- a/src/track/CMakeLists.txt +++ b/src/track/CMakeLists.txt @@ -1,64 +1,74 @@ include_directories( ${Boost_INCLUDE_DIRS} - ${LIBUNWIND_INCLUDE_DIR} ) # heaptrack: bash script to inject/preload configure_file(heaptrack.sh.cmake ${PROJECT_BINARY_DIR}/${BIN_INSTALL_DIR}/heaptrack @ONLY ) install(PROGRAMS ${PROJECT_BINARY_DIR}/${BIN_INSTALL_DIR}/heaptrack DESTINATION ${BIN_INSTALL_DIR} ) +if (HEAPTRACK_USE_LIBUNWIND) + include_directories(${LIBUNWIND_INCLUDE_DIR}) + + add_library(heaptrack_unwind STATIC trace_libunwind.cpp) + target_link_libraries(heaptrack_unwind LINK_PRIVATE ${LIBUNWIND_LIBRARY}) +else() + add_library(heaptrack_unwind STATIC trace_unwind_tables.cpp) +endif() + +set_property(TARGET heaptrack_unwind PROPERTY POSITION_INDEPENDENT_CODE ON) + # heaptrack_preload: track a newly started process add_library(heaptrack_preload MODULE heaptrack_preload.cpp libheaptrack.cpp ) target_link_libraries(heaptrack_preload LINK_PRIVATE ${CMAKE_DL_LIBS} ${CMAKE_THREAD_LIBS_INIT} - ${LIBUNWIND_LIBRARY} + heaptrack_unwind rt ) set_target_properties(heaptrack_preload PROPERTIES VERSION ${HEAPTRACK_LIB_VERSION} SOVERSION ${HEAPTRACK_LIB_SOVERSION} LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${LIB_INSTALL_DIR}/heaptrack" ) install(TARGETS heaptrack_preload LIBRARY DESTINATION ${LIB_INSTALL_DIR}/heaptrack/ ) # heaptrack_inject: track an already running process add_library(heaptrack_inject MODULE heaptrack_inject.cpp libheaptrack.cpp ) target_link_libraries(heaptrack_inject LINK_PRIVATE ${CMAKE_DL_LIBS} ${CMAKE_THREAD_LIBS_INIT} - ${LIBUNWIND_LIBRARY} + heaptrack_unwind rt ) set_target_properties(heaptrack_inject PROPERTIES VERSION ${HEAPTRACK_LIB_VERSION} SOVERSION ${HEAPTRACK_LIB_SOVERSION} LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/${LIB_INSTALL_DIR}/heaptrack" ) install(TARGETS heaptrack_inject LIBRARY DESTINATION ${LIB_INSTALL_DIR}/heaptrack/ ) # public API for custom pool allocators or static binaries install(FILES heaptrack_api.h DESTINATION ${CMAKE_INSTALL_PREFIX}/include ) diff --git a/src/track/libheaptrack.cpp b/src/track/libheaptrack.cpp index 8792903..b458882 100644 --- a/src/track/libheaptrack.cpp +++ b/src/track/libheaptrack.cpp @@ -1,762 +1,732 @@ /* * Copyright 2014-2017 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ /** * @file libheaptrack.cpp * * @brief Collect raw heaptrack data by overloading heap allocation functions. */ #include "libheaptrack.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tracetree.h" #include "util/config.h" #include "util/libunwind_config.h" #include "util/linewriter.h" extern "C" { __attribute__((weak)) void __libc_freeres(); } namespace __gnu_cxx { __attribute__((weak)) extern void __freeres(); } /** * uncomment this to get extended debug code for known pointers * there are still some malloc functions I'm missing apparently, * related to TLS and such I guess */ // #define DEBUG_MALLOC_PTRS using namespace std; namespace { using clock = chrono::steady_clock; chrono::time_point startTime() { static const chrono::time_point s_start = clock::now(); return s_start; } chrono::milliseconds elapsedTime() { return chrono::duration_cast(clock::now() - startTime()); } __pid_t gettid() { return syscall(SYS_gettid); } /** * A per-thread handle guard to prevent infinite recursion, which should be * acquired before doing any special symbol handling. */ struct RecursionGuard { RecursionGuard() : wasLocked(isActive) { isActive = true; } ~RecursionGuard() { isActive = wasLocked; } const bool wasLocked; static thread_local bool isActive; }; thread_local bool RecursionGuard::isActive = false; enum DebugVerbosity { NoDebugOutput, MinimalOutput, VerboseOutput, VeryVerboseOutput, }; // change this to add more debug output to stderr constexpr const DebugVerbosity s_debugVerbosity = NoDebugOutput; /** * Call this to optionally show debug information but give the compiler * a hand in removing it all if debug output is disabled. */ template inline void debugLog(const char fmt[], Args... args) { if (debugLevel <= s_debugVerbosity) { RecursionGuard guard; flockfile(stderr); fprintf(stderr, "heaptrack debug(%d) [%d:%d]@%" PRIu64 " ", static_cast(debugLevel), getpid(), gettid(), elapsedTime().count()); fprintf(stderr, fmt, args...); fputc('\n', stderr); funlockfile(stderr); } } void printBacktrace() { if (s_debugVerbosity == NoDebugOutput) return; -#if LIBUNWIND_HAS_UNW_GETCONTEXT && LIBUNWIND_HAS_UNW_INIT_LOCAL RecursionGuard guard; - unw_context_t context; - unw_getcontext(&context); - - unw_cursor_t cursor; - unw_init_local(&cursor, &context); - - int frameNr = 0; - while (unw_step(&cursor)) { - ++frameNr; - unw_word_t ip = 0; - unw_get_reg(&cursor, UNW_REG_IP, &ip); - - unw_word_t sp = 0; - unw_get_reg(&cursor, UNW_REG_SP, &sp); - - char symbol[256] = {""}; - unw_word_t offset = 0; - unw_get_proc_name(&cursor, symbol, sizeof(symbol), &offset); - - fprintf(stderr, "#%-2d 0x%016" PRIxPTR " sp=0x%016" PRIxPTR " %s + 0x%" PRIxPTR "\n", frameNr, - static_cast(ip), static_cast(sp), symbol, static_cast(offset)); - } -#endif + Trace::print(); } /** * Set to true in an atexit handler. In such conditions, the stop callback * will not be called. */ atomic s_atexit{false}; /** * Set to true in heaptrack_stop, when s_atexit was not yet set. In such conditions, * we always fully unload and cleanup behind ourselves */ atomic s_forceCleanup{false}; int createFile(const char* fileName) { string outputFileName; if (fileName) { outputFileName.assign(fileName); } if (outputFileName == "-" || outputFileName == "stdout") { debugLog("%s", "will write to stdout"); return fileno(stdout); } else if (outputFileName == "stderr") { debugLog("%s", "will write to stderr"); return fileno(stderr); } if (outputFileName.empty()) { // env var might not be set when linked directly into an executable outputFileName = "heaptrack.$$"; } boost::replace_all(outputFileName, "$$", to_string(getpid())); auto out = open(outputFileName.c_str(), O_CREAT | O_WRONLY | O_CLOEXEC, 0644); debugLog("will write to %s/%p\n", outputFileName.c_str(), out); // we do our own locking, this speeds up the writing significantly if (out == -1) { fprintf(stderr, "ERROR: failed to open heaptrack output file %s: %s (%d)\n", outputFileName.c_str(), strerror(errno), errno); } else if (flock(out, LOCK_EX | LOCK_NB) != 0) { fprintf(stderr, "ERROR: failed to lock heaptrack output file %s: %s (%d)\n", outputFileName.c_str(), strerror(errno), errno); close(out); return -1; } return out; } /** * Thread-Safe heaptrack API * * The only critical section in libheaptrack is the output of the data, * dl_iterate_phdr * calls, as well as initialization and shutdown. */ class HeapTrack { public: HeapTrack(const RecursionGuard& /*recursionGuard*/) { debugLog("%s", "acquiring lock"); s_lock.lock(); debugLog("%s", "lock acquired"); } ~HeapTrack() { debugLog("%s", "releasing lock"); s_lock.unlock(); } void initialize(const char* fileName, heaptrack_callback_t initBeforeCallback, heaptrack_callback_initialized_t initAfterCallback, heaptrack_callback_t stopCallback) { debugLog("initializing: %s", fileName); if (s_data) { debugLog("%s", "already initialized"); return; } if (initBeforeCallback) { debugLog("%s", "calling initBeforeCallback"); initBeforeCallback(); debugLog("%s", "done calling initBeforeCallback"); } // do some once-only initializations static once_flag once; call_once(once, [] { debugLog("%s", "doing once-only initialization"); - // configure libunwind for better speed - if (unw_set_caching_policy(unw_local_addr_space, UNW_CACHE_PER_THREAD)) { - fprintf(stderr, "WARNING: Failed to enable per-thread libunwind caching.\n"); - } -#if LIBUNWIND_HAS_UNW_SET_CACHE_SIZE - if (unw_set_cache_size(unw_local_addr_space, 1024, 0)) { - fprintf(stderr, "WARNING: Failed to set libunwind cache size.\n"); - } -#endif + + Trace::setup(); // do not trace forked child processes // TODO: make this configurable pthread_atfork(&prepare_fork, &parent_fork, &child_fork); atexit([]() { if (s_forceCleanup) { return; } debugLog("%s", "atexit()"); // free internal libstdc++ resources // see also Valgrind's `--run-cxx-freeres` option if (&__gnu_cxx::__freeres) { __gnu_cxx::__freeres(); } // free internal libc resources, cf: https://bugs.kde.org/show_bug.cgi?id=378765 // see also Valgrind's `--run-libc-freeres` option if (&__libc_freeres) { __libc_freeres(); } s_atexit.store(true); heaptrack_stop(); }); }); const auto out = createFile(fileName); if (out == -1) { if (stopCallback) { stopCallback(); } return; } s_data = new LockedData(out, stopCallback); writeVersion(); writeExe(); writeCommandLine(); writeSystemInfo(); if (initAfterCallback) { debugLog("%s", "calling initAfterCallback"); initAfterCallback(s_data->out); debugLog("%s", "calling initAfterCallback done"); } debugLog("%s", "initialization done"); } void shutdown() { if (!s_data) { return; } debugLog("%s", "shutdown()"); writeTimestamp(); writeRSS(); s_data->out.flush(); s_data->out.close(); // NOTE: we leak heaptrack data on exit, intentionally // This way, we can be sure to get all static deallocations. if (!s_atexit || s_forceCleanup) { delete s_data; s_data = nullptr; } debugLog("%s", "shutdown() done"); } void invalidateModuleCache() { if (!s_data) { return; } s_data->moduleCacheDirty = true; } void writeTimestamp() { if (!s_data || !s_data->out.canWrite()) { return; } auto elapsed = elapsedTime(); debugLog("writeTimestamp(%" PRIx64 ")", elapsed.count()); s_data->out.writeHexLine('c', static_cast(elapsed.count())); } void writeRSS() { if (!s_data || !s_data->out.canWrite() || s_data->procStatm == -1) { return; } // read RSS in pages from statm, then rewind for next read // NOTE: don't use fscanf here, it could potentially deadlock us const int BUF_SIZE = 512; char buf[BUF_SIZE + 1]; if (read(s_data->procStatm, buf, BUF_SIZE) <= 0) { fprintf(stderr, "WARNING: Failed to read RSS value from /proc/self/statm.\n"); close(s_data->procStatm); s_data->procStatm = -1; return; } lseek(s_data->procStatm, 0, SEEK_SET); size_t rss = 0; if (sscanf(buf, "%*u %zu", &rss) != 1) { fprintf(stderr, "WARNING: Failed to read RSS value from /proc/self/statm.\n"); close(s_data->procStatm); s_data->procStatm = -1; return; } // TODO: compare to rusage.ru_maxrss (getrusage) to find "real" peak? // TODO: use custom allocators with known page sizes to prevent tainting // the RSS numbers with heaptrack-internal data s_data->out.writeHexLine('R', rss); } void writeVersion() { s_data->out.writeHexLine('v', static_cast(HEAPTRACK_VERSION), static_cast(HEAPTRACK_FILE_FORMAT_VERSION)); } void writeExe() { const int BUF_SIZE = 1023; char buf[BUF_SIZE + 1]; ssize_t size = readlink("/proc/self/exe", buf, BUF_SIZE); if (size > 0 && size < BUF_SIZE) { buf[size] = 0; s_data->out.write("x %s\n", buf); } } void writeCommandLine() { s_data->out.write("X"); const int BUF_SIZE = 4096; char buf[BUF_SIZE + 1]; auto fd = open("/proc/self/cmdline", O_RDONLY); int bytesRead = read(fd, buf, BUF_SIZE); char* end = buf + bytesRead; for (char* p = buf; p < end;) { s_data->out.write(" %s", p); while (*p++) ; // skip until start of next 0-terminated section } close(fd); s_data->out.write("\n"); } void writeSystemInfo() { s_data->out.writeHexLine('I', static_cast(sysconf(_SC_PAGESIZE)), static_cast(sysconf(_SC_PHYS_PAGES))); } void handleMalloc(void* ptr, size_t size, const Trace& trace) { if (!s_data || !s_data->out.canWrite()) { return; } updateModuleCache(); const auto index = s_data->traceTree.index( trace, [](uintptr_t ip, uint32_t index) { return s_data->out.writeHexLine('t', ip, index); }); #ifdef DEBUG_MALLOC_PTRS auto it = s_data->known.find(ptr); assert(it == s_data->known.end()); s_data->known.insert(ptr); #endif s_data->out.writeHexLine('+', size, index, reinterpret_cast(ptr)); } void handleFree(void* ptr) { if (!s_data || !s_data->out.canWrite()) { return; } #ifdef DEBUG_MALLOC_PTRS auto it = s_data->known.find(ptr); assert(it != s_data->known.end()); s_data->known.erase(it); #endif s_data->out.writeHexLine('-', reinterpret_cast(ptr)); } private: static int dl_iterate_phdr_callback(struct dl_phdr_info* info, size_t /*size*/, void* data) { auto heaptrack = reinterpret_cast(data); const char* fileName = info->dlpi_name; if (!fileName || !fileName[0]) { fileName = "x"; } debugLog("dlopen_notify_callback: %s %zx", fileName, info->dlpi_addr); if (!heaptrack->s_data->out.write("m %s %zx", fileName, info->dlpi_addr)) { return 1; } for (int i = 0; i < info->dlpi_phnum; i++) { const auto& phdr = info->dlpi_phdr[i]; if (phdr.p_type == PT_LOAD) { if (!heaptrack->s_data->out.write(" %zx %zx", phdr.p_vaddr, phdr.p_memsz)) { return 1; } } } if (!heaptrack->s_data->out.write("\n")) { return 1; } return 0; } static void prepare_fork() { debugLog("%s", "prepare_fork()"); // don't do any custom malloc handling while inside fork RecursionGuard::isActive = true; } static void parent_fork() { debugLog("%s", "parent_fork()"); // the parent process can now continue its custom malloc tracking RecursionGuard::isActive = false; } static void child_fork() { debugLog("%s", "child_fork()"); // but the forked child process cleans up itself // this is important to prevent two processes writing to the same file s_data = nullptr; RecursionGuard::isActive = true; } void updateModuleCache() { if (!s_data || !s_data->out.canWrite() || !s_data->moduleCacheDirty) { return; } debugLog("%s", "updateModuleCache()"); if (!s_data->out.write("m -\n")) { return; } dl_iterate_phdr(&dl_iterate_phdr_callback, this); s_data->moduleCacheDirty = false; } void writeError() { debugLog("write error %d/%s", errno, strerror(errno)); printBacktrace(); shutdown(); } struct LockCheckFailed{}; /** * To prevent deadlocks on shutdown, we try to lock from the timer thread * and throw an LockCheckFailed exception otherwise. */ template HeapTrack(AdditionalLockCheck lockCheck) { debugLog("%s", "trying to acquire lock"); while (!s_lock.try_lock()) { if (!lockCheck()) throw LockCheckFailed(); this_thread::sleep_for(chrono::microseconds(1)); } debugLog("%s", "lock acquired"); } struct LockedData { LockedData(int out, heaptrack_callback_t stopCallback) : out(out) , stopCallback(stopCallback) { debugLog("%s", "constructing LockedData"); procStatm = open("/proc/self/statm", O_RDONLY); if (procStatm == -1) { fprintf(stderr, "WARNING: Failed to open /proc/self/statm for reading: %s.\n", strerror(errno)); } // ensure this utility thread is not handling any signals // our host application may assume only one specific thread // will handle the threads, if that's not the case things // seemingly break in non-obvious ways. // see also: https://bugs.kde.org/show_bug.cgi?id=378494 sigset_t previousMask; sigset_t newMask; sigfillset(&newMask); if (pthread_sigmask(SIG_SETMASK, &newMask, &previousMask) != 0) { fprintf(stderr, "WARNING: Failed to block signals, disabling timer thread.\n"); return; } // the mask we set above will be inherited by the thread that we spawn below timerThread = thread([&]() { RecursionGuard::isActive = true; debugLog("%s", "timer thread started"); // now loop and repeatedly print the timestamp and RSS usage to the data stream while (!stopTimerThread) { // TODO: make interval customizable this_thread::sleep_for(chrono::milliseconds(10)); try { HeapTrack heaptrack([&] { return !stopTimerThread.load(); }); heaptrack.writeTimestamp(); heaptrack.writeRSS(); } catch (LockCheckFailed) { break; } } }); // now restore the previous mask as if nothing ever happened if (pthread_sigmask(SIG_SETMASK, &previousMask, nullptr) != 0) { fprintf(stderr, "WARNING: Failed to restore the signal mask.\n"); } } ~LockedData() { debugLog("%s", "destroying LockedData"); stopTimerThread = true; if (timerThread.joinable()) { try { timerThread.join(); } catch (const std::system_error&) { } } out.close(); if (procStatm != -1) { close(procStatm); } if (stopCallback && (!s_atexit || s_forceCleanup)) { stopCallback(); } debugLog("%s", "done destroying LockedData"); } LineWriter out; /// /proc/self/statm file descriptor to read RSS value from int procStatm = -1; /** * Calls to dlopen/dlclose mark the cache as dirty. * When this happened, all modules and their section addresses * must be found again via dl_iterate_phdr before we output the * next instruction pointer. Otherwise, heaptrack_interpret might * encounter IPs of an unknown/invalid module. */ bool moduleCacheDirty = true; TraceTree traceTree; atomic stopTimerThread{false}; thread timerThread; heaptrack_callback_t stopCallback = nullptr; #ifdef DEBUG_MALLOC_PTRS unordered_set known; #endif }; static std::mutex s_lock; static LockedData* s_data; }; std::mutex HeapTrack::s_lock; HeapTrack::LockedData* HeapTrack::s_data{nullptr}; } extern "C" { void heaptrack_init(const char* outputFileName, heaptrack_callback_t initBeforeCallback, heaptrack_callback_initialized_t initAfterCallback, heaptrack_callback_t stopCallback) { RecursionGuard guard; // initialize startTime(); debugLog("heaptrack_init(%s)", outputFileName); HeapTrack heaptrack(guard); heaptrack.initialize(outputFileName, initBeforeCallback, initAfterCallback, stopCallback); } void heaptrack_stop() { RecursionGuard guard; debugLog("%s", "heaptrack_stop()"); HeapTrack heaptrack(guard); if (!s_atexit) { s_forceCleanup.store(true); } heaptrack.shutdown(); } void heaptrack_malloc(void* ptr, size_t size) { if (ptr && !RecursionGuard::isActive) { RecursionGuard guard; debugLog("heaptrack_malloc(%p, %zu)", ptr, size); Trace trace; trace.fill(2 + HEAPTRACK_DEBUG_BUILD); HeapTrack heaptrack(guard); heaptrack.handleMalloc(ptr, size, trace); } } void heaptrack_free(void* ptr) { if (ptr && !RecursionGuard::isActive) { RecursionGuard guard; debugLog("heaptrack_free(%p)", ptr); HeapTrack heaptrack(guard); heaptrack.handleFree(ptr); } } void heaptrack_realloc(void* ptr_in, size_t size, void* ptr_out) { if (ptr_out && !RecursionGuard::isActive) { RecursionGuard guard; debugLog("heaptrack_realloc(%p, %zu, %p)", ptr_in, size, ptr_out); Trace trace; trace.fill(2 + HEAPTRACK_DEBUG_BUILD); HeapTrack heaptrack(guard); if (ptr_in) { heaptrack.handleFree(ptr_in); } heaptrack.handleMalloc(ptr_out, size, trace); } } void heaptrack_invalidate_module_cache() { RecursionGuard guard; debugLog("%s", "heaptrack_invalidate_module_cache()"); HeapTrack heaptrack(guard); heaptrack.invalidateModuleCache(); } } diff --git a/src/track/trace.h b/src/track/trace.h index 3e96ff2..1fa1dcb 100644 --- a/src/track/trace.h +++ b/src/track/trace.h @@ -1,78 +1,80 @@ /* * Copyright 2014-2017 Milian Wolff * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #ifndef TRACE_H #define TRACE_H -#include - -#define UNW_LOCAL_ONLY -#include - /** - * @brief A libunwind based backtrace. + * @brief Backtrace interface. */ struct Trace { using ip_t = void*; enum : int { MAX_SIZE = 64 }; const ip_t* begin() const { return m_data + m_skip; } const ip_t* end() const { return begin() + m_size; } ip_t operator[](int i) const { return m_data[m_skip + i]; } int size() const { return m_size; } bool fill(int skip) { - int size = unw_backtrace(m_data, MAX_SIZE); - // filter bogus frames at the end, which sometimes get returned by libunwind + int size = unwind(m_data); + // filter bogus frames at the end, which sometimes get returned by tracer backend // cf.: https://bugs.kde.org/show_bug.cgi?id=379082 while (size > 0 && !m_data[size - 1]) { --size; } m_size = size > skip ? size - skip : 0; m_skip = skip; return m_size > 0; } + static void setup(); + + static void print(); + +private: + static int unwind(void** data); + private: int m_size = 0; int m_skip = 0; ip_t m_data[MAX_SIZE]; }; #endif // TRACE_H diff --git a/src/track/trace_libunwind.cpp b/src/track/trace_libunwind.cpp new file mode 100644 index 0000000..c76337c --- /dev/null +++ b/src/track/trace_libunwind.cpp @@ -0,0 +1,76 @@ +/* + * Copyright 2014-2019 Milian Wolff + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @brief A libunwind based backtrace. + */ + +#include "trace.h" + +#include "util/libunwind_config.h" + +#define UNW_LOCAL_ONLY +#include + +#include + +void Trace::print() +{ +#if LIBUNWIND_HAS_UNW_GETCONTEXT && LIBUNWIND_HAS_UNW_INIT_LOCAL + unw_context_t context; + unw_getcontext(&context); + + unw_cursor_t cursor; + unw_init_local(&cursor, &context); + + int frameNr = 0; + while (unw_step(&cursor)) { + ++frameNr; + unw_word_t ip = 0; + unw_get_reg(&cursor, UNW_REG_IP, &ip); + + unw_word_t sp = 0; + unw_get_reg(&cursor, UNW_REG_SP, &sp); + + char symbol[256] = {""}; + unw_word_t offset = 0; + unw_get_proc_name(&cursor, symbol, sizeof(symbol), &offset); + + fprintf(stderr, "#%-2d 0x%016" PRIxPTR " sp=0x%016" PRIxPTR " %s + 0x%" PRIxPTR "\n", frameNr, + static_cast(ip), static_cast(sp), symbol, static_cast(offset)); + } +#endif +} + +void Trace::setup() +{ + // configure libunwind for better speed + if (unw_set_caching_policy(unw_local_addr_space, UNW_CACHE_PER_THREAD)) { + fprintf(stderr, "WARNING: Failed to enable per-thread libunwind caching.\n"); + } +#if LIBUNWIND_HAS_UNW_SET_CACHE_SIZE + if (unw_set_cache_size(unw_local_addr_space, 1024, 0)) { + fprintf(stderr, "WARNING: Failed to set libunwind cache size.\n"); + } +#endif +} + +int Trace::unwind(void** data) +{ + return unw_backtrace(data, MAX_SIZE); +} diff --git a/src/track/trace_unwind_tables.cpp b/src/track/trace_unwind_tables.cpp new file mode 100644 index 0000000..0ccdd84 --- /dev/null +++ b/src/track/trace_unwind_tables.cpp @@ -0,0 +1,73 @@ +/* + * Copyright 2019 Volodymyr Nikolaichuk + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @brief A unwind-tables based backtrace. + */ + +#include "trace.h" + +#include +#include +#include + +namespace { + +struct backtrace +{ + void** data = nullptr; + int ctr = 0; + int max_size = 0; +}; + +_Unwind_Reason_Code unwind_backtrace_callback(struct _Unwind_Context* context, void* arg) +{ + backtrace* trace = static_cast(arg); + + uintptr_t pc = _Unwind_GetIP(context); + if (pc && trace->ctr < trace->max_size - 1) { + trace->data[trace->ctr++] = (void*)(pc); + } + + return _URC_NO_REASON; +} + +} + +void Trace::setup() +{ +} + +void Trace::print() +{ + Trace trace; + trace.fill(1); + for (auto ip : trace) { + fprintf(stderr, "%p\n", ip); + } +} + +int Trace::unwind(void** data) +{ + backtrace trace; + trace.data = data; + trace.max_size = MAX_SIZE; + + _Unwind_Backtrace(unwind_backtrace_callback, &trace); + return trace.ctr; +} diff --git a/tests/auto/CMakeLists.txt b/tests/auto/CMakeLists.txt index 12582d9..d8c47dc 100644 --- a/tests/auto/CMakeLists.txt +++ b/tests/auto/CMakeLists.txt @@ -1,68 +1,67 @@ if (ECM_FOUND) include(ECMEnableSanitizers) endif() set(CMAKE_BUILD_TYPE Debug) configure_file(tst_config.h.in ${CMAKE_CURRENT_BINARY_DIR}/tst_config.h @ONLY) include_directories( - ${LIBUNWIND_INCLUDE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../../ ${CMAKE_CURRENT_SOURCE_DIR}/../../src ) add_definitions(-DCATCH_CONFIG_MAIN) add_executable(tst_trace tst_trace.cpp) -target_link_libraries(tst_trace ${LIBUNWIND_LIBRARY}) +target_link_libraries(tst_trace heaptrack_unwind) add_test(NAME tst_trace COMMAND tst_trace) if ("${Boost_FILESYSTEM_FOUND}" AND "${Boost_SYSTEM_FOUND}") include_directories( ${Boost_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR}/../../src/ ${CMAKE_CURRENT_SOURCE_DIR}/../../src/track ) add_executable(tst_libheaptrack tst_libheaptrack.cpp ../../src/track/libheaptrack.cpp) target_link_libraries(tst_libheaptrack LINK_PRIVATE ${CMAKE_DL_LIBS} ${CMAKE_THREAD_LIBS_INIT} - ${LIBUNWIND_LIBRARY} + heaptrack_unwind rt ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} ) add_test(NAME tst_libheaptrack COMMAND tst_libheaptrack) add_executable(tst_io tst_io.cpp) target_link_libraries(tst_io ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} ) add_test(NAME tst_io COMMAND tst_io) find_package(Qt5 5.2.0 CONFIG OPTIONAL_COMPONENTS Test) if (Qt5Test_FOUND) add_executable(tst_parser tst_parser.cpp) target_link_libraries(tst_parser ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} heaptrack_gui_private Qt5::Test ) add_test(NAME tst_parser COMMAND tst_parser) endif() add_executable(tst_inject tst_inject.cpp) target_link_libraries(tst_inject ${CMAKE_DL_LIBS} ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} ) add_test(NAME tst_inject COMMAND tst_inject) endif()