diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cc25bd..ed8578b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,93 +1,93 @@ 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 0) +set(HEAPTRACK_VERSION_MINOR 1) set(HEAPTRACK_VERSION_PATCH 0) set(HEAPTRACK_LIB_VERSION 1.0.0) set(HEAPTRACK_LIB_SOVERSION 1) -set(HEAPTRACK_FILE_FORMAT_VERSION 1) +set(HEAPTRACK_FILE_FORMAT_VERSION 2) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) find_package(Libunwind REQUIRED) find_package(Boost 1.41.0 REQUIRED COMPONENTS iostreams program_options) find_package(Threads REQUIRED) find_package(ZLIB REQUIRED) include(FeatureSummary) option( HEAPTRACK_BUILD_GUI "Disable this option to skip building the Qt5 / KF5 based GUI for heaptrack." On ) if(HEAPTRACK_BUILD_GUI) find_package(Qt5 5.2.0 NO_MODULE OPTIONAL_COMPONENTS Widgets) find_package(ECM 1.0.0 NO_MODULE) if(Qt5_FOUND AND ECM_FOUND) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) find_package(KF5 OPTIONAL_COMPONENTS CoreAddons I18n ItemModels ThreadWeaver ConfigWidgets KIO) find_package(KChart "2.6.0") set_package_properties(KChart PROPERTIES TYPE RECOMMENDED PURPOSE "Required for the heaptrack_gui executable. Get it from the kdiagram module.") endif() endif() set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -Wpedantic") 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() 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() 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") add_subdirectory(3rdparty) add_subdirectory(src) add_subdirectory(tests) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/interpret/heaptrack_interpret.cpp b/src/interpret/heaptrack_interpret.cpp index 8bb840f..8e9af5f 100644 --- a/src/interpret/heaptrack_interpret.cpp +++ b/src/interpret/heaptrack_interpret.cpp @@ -1,446 +1,465 @@ /* * Copyright 2014-2017 Milian Wolff * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** * @file heaptrack_interpret.cpp * * @brief Interpret raw heaptrack data and add Dwarf based debug information. */ #include #include #include #include #include #include #include #include #include #include #include "libbacktrace/backtrace.h" #include "libbacktrace/internal.h" #include "util/linereader.h" #include "util/pointermap.h" #include using namespace std; namespace { string demangle(const char* function) { if (!function) { return {}; } else if (function[0] != '_' || function[1] != 'Z') { return {function}; } string ret; int status = 0; char* demangled = abi::__cxa_demangle(function, 0, 0, &status); if (demangled) { ret = demangled; free(demangled); } return ret; } +bool fileIsReadable(const string& path) +{ + return access(path.c_str(), R_OK) == 0; +} + struct AddressInformation { string function; string file; int line = 0; }; struct Module { Module(uintptr_t addressStart, uintptr_t addressEnd, backtrace_state* backtraceState, size_t moduleIndex) : addressStart(addressStart) , addressEnd(addressEnd) , moduleIndex(moduleIndex) , backtraceState(backtraceState) { } AddressInformation resolveAddress(uintptr_t address) const { AddressInformation info; if (!backtraceState) { return info; } backtrace_pcinfo(backtraceState, address, [](void* data, uintptr_t /*addr*/, const char* file, int line, const char* function) -> int { auto info = reinterpret_cast(data); info->function = demangle(function); info->file = file ? file : ""; info->line = line; return 0; }, [](void* /*data*/, const char* /*msg*/, int /*errnum*/) {}, &info); if (info.function.empty()) { backtrace_syminfo( backtraceState, address, [](void* data, uintptr_t /*pc*/, const char* symname, uintptr_t /*symval*/, uintptr_t /*symsize*/) { if (symname) { reinterpret_cast(data)->function = demangle(symname); } }, [](void* /*data*/, const char* msg, int errnum) { cerr << "Module backtrace error (code " << errnum << "): " << msg << endl; }, &info); } return info; } bool operator<(const Module& module) const { return tie(addressStart, addressEnd, moduleIndex) < tie(module.addressStart, module.addressEnd, module.moduleIndex); } bool operator!=(const Module& module) const { return tie(addressStart, addressEnd, moduleIndex) != tie(module.addressStart, module.addressEnd, module.moduleIndex); } uintptr_t addressStart; uintptr_t addressEnd; size_t moduleIndex; backtrace_state* backtraceState; }; struct ResolvedIP { size_t moduleIndex = 0; size_t fileIndex = 0; size_t functionIndex = 0; int line = 0; }; struct AccumulatedTraceData { AccumulatedTraceData() { m_modules.reserve(256); m_backtraceStates.reserve(64); m_internedData.reserve(4096); m_encounteredIps.reserve(32768); } ~AccumulatedTraceData() { fprintf(stdout, "# strings: %zu\n# ips: %zu\n", m_internedData.size(), m_encounteredIps.size()); } ResolvedIP resolve(const uintptr_t ip) { if (m_modulesDirty) { // sort by addresses, required for binary search below sort(m_modules.begin(), m_modules.end()); #ifndef NDEBUG for (size_t i = 0; i < m_modules.size(); ++i) { const auto& m1 = m_modules[i]; for (size_t j = i + 1; j < m_modules.size(); ++j) { if (i == j) { continue; } const auto& m2 = m_modules[j]; if ((m1.addressStart <= m2.addressStart && m1.addressEnd > m2.addressStart) || (m1.addressStart < m2.addressEnd && m1.addressEnd >= m2.addressEnd)) { cerr << "OVERLAPPING MODULES: " << hex << m1.moduleIndex << " (" << m1.addressStart << " to " << m1.addressEnd << ") and " << m1.moduleIndex << " (" << m2.addressStart << " to " << m2.addressEnd << ")\n" << dec; } else if (m2.addressStart >= m1.addressEnd) { break; } } } #endif m_modulesDirty = false; } ResolvedIP data; // find module for this instruction pointer auto module = lower_bound(m_modules.begin(), m_modules.end(), ip, [](const Module& module, const uintptr_t ip) -> bool { return module.addressEnd < ip; }); if (module != m_modules.end() && module->addressStart <= ip && module->addressEnd >= ip) { data.moduleIndex = module->moduleIndex; const auto info = module->resolveAddress(ip); data.fileIndex = intern(info.file); data.functionIndex = intern(info.function); data.line = info.line; } return data; } size_t intern(const string& str, std::string* internedString = nullptr) { if (str.empty()) { return 0; } auto it = m_internedData.find(str); if (it != m_internedData.end()) { if (internedString) { *internedString = it->first; } return it->second; } const size_t id = m_internedData.size() + 1; it = m_internedData.insert(it, make_pair(str, id)); if (internedString) { *internedString = it->first; } fprintf(stdout, "s %s\n", str.c_str()); return id; } void addModule(backtrace_state* backtraceState, const size_t moduleIndex, const uintptr_t addressStart, const uintptr_t addressEnd) { m_modules.emplace_back(addressStart, addressEnd, backtraceState, moduleIndex); m_modulesDirty = true; } void clearModules() { // TODO: optimize this, reuse modules that are still valid m_modules.clear(); m_modulesDirty = true; } size_t addIp(const uintptr_t instructionPointer) { if (!instructionPointer) { return 0; } auto it = m_encounteredIps.find(instructionPointer); if (it != m_encounteredIps.end()) { return it->second; } const size_t ipId = m_encounteredIps.size() + 1; m_encounteredIps.insert(it, make_pair(instructionPointer, ipId)); const auto ip = resolve(instructionPointer); fprintf(stdout, "i %zx %zx", instructionPointer, ip.moduleIndex); if (ip.functionIndex || ip.fileIndex) { fprintf(stdout, " %zx", ip.functionIndex); if (ip.fileIndex) { fprintf(stdout, " %zx %x", ip.fileIndex, ip.line); } } fputc('\n', stdout); return ipId; } std::string findDebugFile(const std::string& input) const { // TODO: also try to find a debug file by build-id // TODO: also lookup in (user-configurable) debug path std::string file = input + ".debug"; - if (access(file.c_str(), R_OK) == 0) { - return file; - } else { - return input; + return fileIsReadable(file) ? file : input; + } + + std::string findBuildIdFile(const string& buildId) const + { + if (buildId.empty()) { + return {}; } + // TODO: also lookup in (user-configurable) debug path + const auto path = "/usr/lib/debug/.build-id/" + buildId.substr(0, 2) + '/' + buildId.substr(2) + ".debug"; + cerr << path << endl; + return fileIsReadable(path) ? path : string(); } /** * Prevent the same file from being initialized multiple times. * This drastically cuts the memory consumption down */ - backtrace_state* findBacktraceState(const std::string& originalFileName, uintptr_t addressStart) + backtrace_state* findBacktraceState(const std::string& originalFileName, const string& buildId, uintptr_t addressStart) { if (boost::algorithm::starts_with(originalFileName, "linux-vdso.so")) { // prevent warning, since this will always fail return nullptr; } auto it = m_backtraceStates.find(originalFileName); if (it != m_backtraceStates.end()) { return it->second; } - const auto fileName = findDebugFile(originalFileName); + // TODO: also lookup in (user-configurable) sysroot path + const auto buildIdFile = findBuildIdFile(buildId); + const auto fileName = buildIdFile.empty() ? findDebugFile(originalFileName) : buildIdFile; struct CallbackData { const char* fileName; }; CallbackData data = {fileName.c_str()}; auto errorHandler = [](void* rawData, const char* msg, int errnum) { auto data = reinterpret_cast(rawData); cerr << "Failed to create backtrace state for module " << data->fileName << ": " << msg << " / " << strerror(errnum) << " (error code " << errnum << ")" << endl; }; auto state = backtrace_create_state(data.fileName, /* we are single threaded, so: not thread safe */ false, errorHandler, &data); if (state) { const int descriptor = backtrace_open(data.fileName, errorHandler, &data, nullptr); if (descriptor >= 1) { int foundSym = 0; int foundDwarf = 0; auto ret = elf_add(state, descriptor, addressStart, errorHandler, &data, &state->fileline_fn, &foundSym, &foundDwarf, false); if (ret && foundSym) { state->syminfo_fn = &elf_syminfo; } } } m_backtraceStates.insert(it, make_pair(originalFileName, state)); return state; } private: vector m_modules; unordered_map m_backtraceStates; bool m_modulesDirty = false; unordered_map m_internedData; unordered_map m_encounteredIps; }; } int main(int /*argc*/, char** /*argv*/) { // optimize: we only have a single thread ios_base::sync_with_stdio(false); __fsetlocking(stdout, FSETLOCKING_BYCALLER); __fsetlocking(stdin, FSETLOCKING_BYCALLER); AccumulatedTraceData data; LineReader reader; string exe; PointerMap ptrToIndex; uint64_t lastPtr = 0; AllocationInfoSet allocationInfos; uint64_t allocations = 0; uint64_t leakedAllocations = 0; uint64_t temporaryAllocations = 0; while (reader.getLine(cin)) { if (reader.mode() == 'x') { reader >> exe; } else if (reader.mode() == 'm') { string fileName; reader >> fileName; if (fileName == "-") { data.clearModules(); } else { - if (fileName == "x") { - fileName = exe; - } - std::string internedString; - const auto moduleIndex = data.intern(fileName, &internedString); + string buildId; + reader >> buildId; + uintptr_t addressStart = 0; if (!(reader >> addressStart)) { cerr << "failed to parse line: " << reader.line() << endl; return 1; } - auto state = data.findBacktraceState(internedString, addressStart); + + if (fileName == "x") { + fileName = exe; + } + std::string internedString; + const auto moduleIndex = data.intern(fileName, &internedString); + + auto state = data.findBacktraceState(internedString, buildId, addressStart); uintptr_t vAddr = 0; uintptr_t memSize = 0; while ((reader >> vAddr) && (reader >> memSize)) { data.addModule(state, moduleIndex, addressStart + vAddr, addressStart + vAddr + memSize); } } } else if (reader.mode() == 't') { uintptr_t instructionPointer = 0; size_t parentIndex = 0; if (!(reader >> instructionPointer) || !(reader >> parentIndex)) { cerr << "failed to parse line: " << reader.line() << endl; return 1; } // ensure ip is encountered const auto ipId = data.addIp(instructionPointer); // trace point, map current output index to parent index fprintf(stdout, "t %zx %zx\n", ipId, parentIndex); } else if (reader.mode() == '+') { ++allocations; ++leakedAllocations; uint64_t size = 0; TraceIndex traceId; uint64_t ptr = 0; if (!(reader >> size) || !(reader >> traceId.index) || !(reader >> ptr)) { cerr << "failed to parse line: " << reader.line() << endl; continue; } AllocationIndex index; if (allocationInfos.add(size, traceId, &index)) { fprintf(stdout, "a %" PRIx64 " %x\n", size, traceId.index); } ptrToIndex.addPointer(ptr, index); lastPtr = ptr; fprintf(stdout, "+ %x\n", index.index); } else if (reader.mode() == '-') { uint64_t ptr = 0; if (!(reader >> ptr)) { cerr << "failed to parse line: " << reader.line() << endl; continue; } bool temporary = lastPtr == ptr; lastPtr = 0; auto allocation = ptrToIndex.takePointer(ptr); if (!allocation.second) { continue; } fprintf(stdout, "- %x\n", allocation.first.index); if (temporary) { ++temporaryAllocations; } --leakedAllocations; } else { fputs(reader.line().c_str(), stdout); fputc('\n', stdout); } } fprintf(stderr, "heaptrack stats:\n" "\tallocations: \t%" PRIu64 "\n" "\tleaked allocations: \t%" PRIu64 "\n" "\ttemporary allocations:\t%" PRIu64 "\n", allocations, leakedAllocations, temporaryAllocations); return 0; } diff --git a/src/track/libheaptrack.cpp b/src/track/libheaptrack.cpp index b21b00b..351ce6b 100644 --- a/src/track/libheaptrack.cpp +++ b/src/track/libheaptrack.cpp @@ -1,651 +1,698 @@ /* * Copyright 2014-2017 Milian Wolff * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; 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 "tracetree.h" #include "util/config.h" #include "util/libunwind_config.h" /** * 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 { 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) { flockfile(stderr); fprintf(stderr, "heaptrack debug [%d]: ", static_cast(debugLevel)); fprintf(stderr, fmt, args...); fputc('\n', stderr); funlockfile(stderr); } } /** * 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}; /** * 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; void writeVersion(FILE* out) { fprintf(out, "v %x %x\n", HEAPTRACK_VERSION, HEAPTRACK_FILE_FORMAT_VERSION); } void writeExe(FILE* out) { 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; fprintf(out, "x %s\n", buf); } } void writeCommandLine(FILE* out) { fputc('X', out); 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;) { fputc(' ', out); fputs(p, out); while (*p++) ; // skip until start of next 0-terminated section } close(fd); fputc('\n', out); } void writeSystemInfo(FILE* out) { fprintf(out, "I %lx %lx\n", sysconf(_SC_PAGESIZE), sysconf(_SC_PHYS_PAGES)); } FILE* createFile(const char* fileName) { string outputFileName; if (fileName) { outputFileName.assign(fileName); } if (outputFileName == "-" || outputFileName == "stdout") { debugLog("%s", "will write to stdout"); return stdout; } else if (outputFileName == "stderr") { debugLog("%s", "will write to stderr"); return 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 = fopen(outputFileName.c_str(), "w"); debugLog("will write to %s/%p\n", outputFileName.c_str(), out); // we do our own locking, this speeds up the writing significantly __fsetlocking(out, FSETLOCKING_BYCALLER); 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. * * This uses a spinlock, instead of a std::mutex, as the latter can lead to * deadlocks * on destruction. The spinlock is "simple", and OK to only guard the small * sections. */ class HeapTrack { public: HeapTrack(const RecursionGuard& /*recursionGuard*/) : HeapTrack([] { return true; }) { } ~HeapTrack() { debugLog("%s", "releasing lock"); s_locked.store(false, memory_order_release); } 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"); } #ifdef 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 // 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()"); s_atexit.store(true); heaptrack_stop(); }); }); FILE* out = createFile(fileName); if (!out) { fprintf(stderr, "ERROR: Failed to open heaptrack output file: %s\n", fileName); if (stopCallback) { stopCallback(); } return; } writeVersion(out); writeExe(out); writeCommandLine(out); writeSystemInfo(out); s_data = new LockedData(out, stopCallback); if (initAfterCallback) { debugLog("%s", "calling initAfterCallback"); initAfterCallback(out); debugLog("%s", "calling initAfterCallback done"); } debugLog("%s", "initialization done"); } void shutdown() { if (!s_data) { return; } debugLog("%s", "shutdown()"); writeTimestamp(); writeRSS(); // 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) { return; } auto elapsed = chrono::duration_cast(clock::now() - s_data->start); debugLog("writeTimestamp(%" PRIx64 ")", elapsed.count()); if (fprintf(s_data->out, "c %" PRIx64 "\n", elapsed.count()) < 0) { writeError(); return; } } void writeRSS() { if (!s_data || !s_data->out || !s_data->procStatm) { return; } // read RSS in pages from statm, then rewind for next read size_t rss = 0; fscanf(s_data->procStatm, "%*x %zx", &rss); rewind(s_data->procStatm); // 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 if (fprintf(s_data->out, "R %zx\n", rss) < 0) { writeError(); return; } } void handleMalloc(void* ptr, size_t size, const Trace& trace) { if (!s_data || !s_data->out) { return; } updateModuleCache(); const auto index = s_data->traceTree.index(trace, s_data->out); #ifdef DEBUG_MALLOC_PTRS auto it = s_data->known.find(ptr); assert(it == s_data->known.end()); s_data->known.insert(ptr); #endif if (fprintf(s_data->out, "+ %zx %x %" PRIxPTR "\n", size, index, reinterpret_cast(ptr)) < 0) { writeError(); return; } } void handleFree(void* ptr) { if (!s_data || !s_data->out) { return; } #ifdef DEBUG_MALLOC_PTRS auto it = s_data->known.find(ptr); assert(it != s_data->known.end()); s_data->known.erase(it); #endif if (fprintf(s_data->out, "- %" PRIxPTR "\n", reinterpret_cast(ptr)) < 0) { writeError(); return; } } 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 (fprintf(heaptrack->s_data->out, "m %s %zx", fileName, info->dlpi_addr) < 0) { + const auto MAX_BUILD_ID_SIZE = 20u; + unsigned raw_build_id_size = 0; + unsigned char raw_build_id[MAX_BUILD_ID_SIZE] = {}; + + for (int i = 0; i < info->dlpi_phnum; i++) { + const auto& phdr = info->dlpi_phdr[i]; + if (raw_build_id_size == 0 && phdr.p_type == PT_NOTE) { + auto segmentAddr = phdr.p_vaddr + info->dlpi_addr; + const auto segmentEnd = segmentAddr + phdr.p_memsz; + const ElfW(Nhdr)* nhdr = nullptr; + while (segmentAddr < segmentEnd) { + nhdr = reinterpret_cast(segmentAddr); + if (nhdr->n_type == NT_GNU_BUILD_ID) { + break; + } + segmentAddr += sizeof(ElfW(Nhdr)) + nhdr->n_namesz + nhdr->n_descsz; + } + if (nhdr->n_type == NT_GNU_BUILD_ID) { + const auto buildIdAddr = segmentAddr + sizeof(ElfW(Nhdr)) + nhdr->n_namesz; + if (buildIdAddr + nhdr->n_descsz <= segmentEnd && nhdr->n_descsz <= MAX_BUILD_ID_SIZE) { + const auto* buildId = reinterpret_cast(buildIdAddr); + raw_build_id_size = nhdr->n_descsz; + std::memcpy(raw_build_id, buildId, raw_build_id_size); + break; + } + } + } + } + + if (fprintf(heaptrack->s_data->out, "m %s ", fileName) < 0) { + heaptrack->writeError(); + return 1; + } + if (raw_build_id_size == 0) { + if (fprintf(heaptrack->s_data->out, "- ") < 0) { + heaptrack->writeError(); + return 1; + } + } else { + for (unsigned i = 0; i < raw_build_id_size; ++i) { + if (fprintf(heaptrack->s_data->out, "%02x", raw_build_id[i]) < 0) { + heaptrack->writeError(); + return 1; + } + } + } + if (fprintf(heaptrack->s_data->out, " %zx", info->dlpi_addr) < 0) { heaptrack->writeError(); 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 (fprintf(heaptrack->s_data->out, " %zx %zx", phdr.p_vaddr, phdr.p_memsz) < 0) { heaptrack->writeError(); return 1; } } } if (fputc('\n', heaptrack->s_data->out) == EOF) { heaptrack->writeError(); 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 || !s_data->moduleCacheDirty) { return; } debugLog("%s", "updateModuleCache()"); if (fputs("m -\n", s_data->out) == EOF) { writeError(); return; } dl_iterate_phdr(&dl_iterate_phdr_callback, this); s_data->moduleCacheDirty = false; } void writeError() { debugLog("write error %d/%s", errno, strerror(errno)); s_data->out = nullptr; shutdown(); } template HeapTrack(AdditionalLockCheck lockCheck) { debugLog("%s", "acquiring lock"); while (s_locked.exchange(true, memory_order_acquire) && lockCheck()) { this_thread::sleep_for(chrono::microseconds(1)); } debugLog("%s", "lock acquired"); } using clock = chrono::steady_clock; struct LockedData { LockedData(FILE* out, heaptrack_callback_t stopCallback) : out(out) , stopCallback(stopCallback) { debugLog("%s", "constructing LockedData"); procStatm = fopen("/proc/self/statm", "r"); if (!procStatm) { fprintf(stderr, "WARNING: Failed to open /proc/self/statm for reading.\n"); } timerThread = thread([&]() { RecursionGuard::isActive = true; debugLog("%s", "timer thread started"); while (!stopTimerThread) { // TODO: make interval customizable this_thread::sleep_for(chrono::milliseconds(10)); HeapTrack heaptrack([&] { return !stopTimerThread.load(); }); if (!stopTimerThread) { heaptrack.writeTimestamp(); heaptrack.writeRSS(); } } }); } ~LockedData() { debugLog("%s", "destroying LockedData"); stopTimerThread = true; if (timerThread.joinable()) { try { timerThread.join(); } catch (std::system_error) { } } if (out) { fclose(out); } if (procStatm) { fclose(procStatm); } if (stopCallback && (!s_atexit || s_forceCleanup)) { stopCallback(); } debugLog("%s", "done destroying LockedData"); } /** * Note: We use the C stdio API here for performance reasons. * Esp. in multi-threaded environments this is much faster * to produce non-per-line-interleaved output. */ FILE* out = nullptr; /// /proc/self/statm file stream to read RSS value from FILE* procStatm = nullptr; /** * 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; const chrono::time_point start = clock::now(); atomic stopTimerThread{false}; thread timerThread; heaptrack_callback_t stopCallback = nullptr; #ifdef DEBUG_MALLOC_PTRS unordered_set known; #endif }; static atomic s_locked; static LockedData* s_data; }; atomic HeapTrack::s_locked{false}; 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; 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(); } }