File indexing completed on 2024-05-05 05:44:15

0001 /*
0002     SPDX-FileCopyrightText: 2014-2022 Milian Wolff <mail@milianw.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 /**
0008  * @file heaptrack_interpret.cpp
0009  *
0010  * @brief Interpret raw heaptrack data and add Dwarf based debug information.
0011  */
0012 
0013 #include <algorithm>
0014 #include <cinttypes>
0015 #include <iostream>
0016 #include <sstream>
0017 #ifdef __linux__
0018 #include <stdio_ext.h>
0019 #endif
0020 #include <memory>
0021 #include <tuple>
0022 #include <vector>
0023 
0024 #include "dwarfdiecache.h"
0025 #include "symbolcache.h"
0026 
0027 #include "util/linereader.h"
0028 #include "util/linewriter.h"
0029 #include "util/pointermap.h"
0030 
0031 #include <dwarf.h>
0032 #include <elfutils/libdwelf.h>
0033 
0034 #include <csignal>
0035 #include <unistd.h>
0036 
0037 using namespace std;
0038 
0039 namespace {
0040 bool isArmArch()
0041 {
0042 #ifdef __arm__
0043     return true;
0044 #else
0045     return false;
0046 #endif
0047 }
0048 
0049 #define error_out cerr << __FILE__ << ':' << __LINE__ << " ERROR:"
0050 
0051 bool startsWith(const std::string& haystack, const char* needle)
0052 {
0053     return haystack.compare(0, strlen(needle), needle) == 0;
0054 }
0055 
0056 static uint64_t alignedAddress(uint64_t addr, bool isArmArch)
0057 {
0058     // Adjust addr back. The symtab entries are 1 off for all practical purposes.
0059     return (isArmArch && (addr & 1)) ? addr - 1 : addr;
0060 }
0061 
0062 static SymbolCache::Symbols extractSymbols(Dwfl_Module* module, uint64_t elfStart, bool isArmArch)
0063 {
0064     SymbolCache::Symbols symbols;
0065 
0066     const auto numSymbols = dwfl_module_getsymtab(module);
0067     if (numSymbols <= 0)
0068         return symbols;
0069 
0070     symbols.reserve(numSymbols);
0071     for (int i = 0; i < numSymbols; ++i) {
0072         GElf_Sym sym;
0073         GElf_Addr symAddr;
0074         const auto symbol = dwfl_module_getsym_info(module, i, &sym, &symAddr, nullptr, nullptr, nullptr);
0075         if (symbol) {
0076             const uint64_t start = alignedAddress(sym.st_value, isArmArch);
0077             symbols.push_back({symAddr - elfStart, start, sym.st_size, symbol});
0078         }
0079     }
0080     return symbols;
0081 }
0082 
0083 struct Frame
0084 {
0085     Frame(string function = {}, string file = {}, int line = 0)
0086         : function(function)
0087         , file(file)
0088         , line(line)
0089     {
0090     }
0091 
0092     bool isValid() const
0093     {
0094         return !function.empty();
0095     }
0096 
0097     string function;
0098     string file;
0099     int line;
0100 };
0101 
0102 struct AddressInformation
0103 {
0104     Frame frame;
0105     vector<Frame> inlined;
0106 };
0107 
0108 struct ResolvedFrame
0109 {
0110     ResolvedFrame(size_t functionIndex = 0, size_t fileIndex = 0, int line = 0)
0111         : functionIndex(functionIndex)
0112         , fileIndex(fileIndex)
0113         , line(line)
0114     {
0115     }
0116     size_t functionIndex;
0117     size_t fileIndex;
0118     int line;
0119 };
0120 
0121 struct ResolvedIP
0122 {
0123     size_t moduleIndex = 0;
0124     ResolvedFrame frame;
0125     vector<ResolvedFrame> inlined;
0126 };
0127 
0128 struct ModuleFragment
0129 {
0130     ModuleFragment(string fileName, uintptr_t addressStart, uintptr_t fragmentStart, uintptr_t fragmentEnd,
0131                    size_t moduleIndex)
0132         : fileName(fileName)
0133         , addressStart(addressStart)
0134         , fragmentStart(fragmentStart)
0135         , fragmentEnd(fragmentEnd)
0136         , moduleIndex(moduleIndex)
0137     {
0138     }
0139 
0140     bool operator<(const ModuleFragment& module) const
0141     {
0142         return tie(addressStart, fragmentStart, fragmentEnd, moduleIndex)
0143             < tie(module.addressStart, module.fragmentStart, module.fragmentEnd, module.moduleIndex);
0144     }
0145 
0146     bool operator!=(const ModuleFragment& module) const
0147     {
0148         return tie(addressStart, fragmentStart, fragmentEnd, moduleIndex)
0149             != tie(module.addressStart, module.fragmentStart, module.fragmentEnd, module.moduleIndex);
0150     }
0151 
0152     string fileName;
0153     uintptr_t addressStart;
0154     uintptr_t fragmentStart;
0155     uintptr_t fragmentEnd;
0156     size_t moduleIndex;
0157 };
0158 
0159 struct Module
0160 {
0161     Module(string fileName, uintptr_t addressStart, Dwfl_Module* module, SymbolCache* symbolCache)
0162         : fileName(std::move(fileName))
0163         , addressStart(addressStart)
0164         , module(module)
0165         , dieCache(module)
0166         , symbolCache(symbolCache)
0167     {
0168     }
0169 
0170     Module()
0171         : Module({}, 0, nullptr, nullptr)
0172     {
0173     }
0174 
0175     AddressInformation resolveAddress(uintptr_t address) const
0176     {
0177         AddressInformation info;
0178 
0179         if (!module) {
0180             return info;
0181         }
0182 
0183         if (!symbolCache->hasSymbols(fileName)) {
0184             // cache all symbols in a sorted lookup table and demangle them on-demand
0185             // note that the symbols within the symtab aren't necessarily sorted,
0186             // which makes searching repeatedly via dwfl_module_addrinfo potentially very slow
0187             symbolCache->setSymbols(fileName, extractSymbols(module, addressStart, isArmArch()));
0188         }
0189 
0190         auto cachedAddrInfo = symbolCache->findSymbol(fileName, address - addressStart);
0191         if (cachedAddrInfo.isValid()) {
0192             info.frame.function = std::move(cachedAddrInfo.symname);
0193         }
0194 
0195         auto cuDie = dieCache.findCuDie(address);
0196         if (!cuDie) {
0197             return info;
0198         }
0199 
0200         const auto offset = address - cuDie->bias();
0201         auto srcloc = dwarf_getsrc_die(cuDie->cudie(), offset);
0202         if (srcloc) {
0203             const char* srcfile = dwarf_linesrc(srcloc, nullptr, nullptr);
0204             if (srcfile) {
0205                 const auto file = std::string(srcfile);
0206                 info.frame.file = srcfile;
0207                 dwarf_lineno(srcloc, &info.frame.line);
0208             }
0209         }
0210 
0211         auto* subprogram = cuDie->findSubprogramDie(offset);
0212         if (!subprogram) {
0213             return info;
0214         }
0215 
0216         // resolve the inline chain if possible
0217         auto scopes = findInlineScopes(subprogram->die(), offset);
0218 
0219         if (scopes.empty()) {
0220             // no inline frames, use subprogram name directly and return
0221             info.frame.function = cuDie->dieName(subprogram->die());
0222             return info;
0223         }
0224 
0225         // use name of the last inlined function as symbol
0226         info.frame.function = cuDie->dieName(&scopes.back());
0227 
0228         Dwarf_Files* files = nullptr;
0229         dwarf_getsrcfiles(cuDie->cudie(), &files, nullptr);
0230 
0231         auto handleDie = [&](Dwarf_Die *scope, Dwarf_Die *prevScope) {
0232             const auto tag = dwarf_tag(prevScope);
0233             if (tag != DW_TAG_inlined_subroutine) {
0234                 error_out << "unexpected prev scope tag: " << std::hex << tag << '\n';
0235                 return;
0236             }
0237 
0238             auto call = callSourceLocation(prevScope, files, cuDie->cudie());
0239             info.inlined.push_back({cuDie->dieName(scope), std::move(call.file), call.line});
0240         };
0241 
0242         // iterate in reverse, to properly rebuild the inline stack
0243         // note that we need to take the DW_AT_call_{file,line} from the previous scope DIE
0244         const auto numScopes = scopes.size();
0245         for (std::size_t scopeIndex = numScopes - 1; scopeIndex >= 1; --scopeIndex) {
0246             handleDie(&scopes[scopeIndex - 1], &scopes[scopeIndex]);
0247         }
0248 
0249         // the very last frame is the one where all the code got inlined into
0250         handleDie(subprogram->die(), &scopes.front());
0251 
0252         return info;
0253     }
0254 
0255     string fileName;
0256     uintptr_t addressStart;
0257     Dwfl_Module* module;
0258     mutable DwarfDieCache dieCache;
0259     SymbolCache* symbolCache;
0260 };
0261 
0262 struct AccumulatedTraceData
0263 {
0264     AccumulatedTraceData()
0265         : out(fileno(stdout))
0266     {
0267         m_moduleFragments.reserve(256);
0268         m_internedData.reserve(4096);
0269         m_encounteredIps.reserve(32768);
0270 
0271         {
0272             std::string debugPath(":.debug:/usr/lib/debug");
0273             const auto length = debugPath.size() + 1;
0274             m_debugPath = new char[length];
0275             std::memcpy(m_debugPath, debugPath.data(), length);
0276         }
0277 
0278         m_callbacks = {
0279             &dwfl_build_id_find_elf,
0280             &dwfl_standard_find_debuginfo,
0281             &dwfl_offline_section_address,
0282             &m_debugPath,
0283         };
0284 
0285         m_dwfl = dwfl_begin(&m_callbacks);
0286     }
0287 
0288     ~AccumulatedTraceData()
0289     {
0290         out.write("# strings: %zu\n# ips: %zu\n", m_internedData.size(), m_encounteredIps.size());
0291         out.flush();
0292 
0293         delete[] m_debugPath;
0294         dwfl_end(m_dwfl);
0295     }
0296 
0297     ResolvedIP resolve(const uintptr_t ip)
0298     {
0299         if (m_modulesDirty) {
0300             // sort by addresses, required for binary search below
0301             sort(m_moduleFragments.begin(), m_moduleFragments.end());
0302 
0303 #ifndef NDEBUG
0304             for (size_t i = 0; i < m_moduleFragments.size(); ++i) {
0305                 const auto& m1 = m_moduleFragments[i];
0306                 for (size_t j = i + 1; j < m_moduleFragments.size(); ++j) {
0307                     if (i == j) {
0308                         continue;
0309                     }
0310                     const auto& m2 = m_moduleFragments[j];
0311                     if ((m1.fragmentStart <= m2.fragmentStart && m1.fragmentEnd > m2.fragmentStart)
0312                         || (m1.fragmentStart < m2.fragmentEnd && m1.fragmentEnd >= m2.fragmentEnd)) {
0313                         cerr << "OVERLAPPING MODULES: " << hex << m1.moduleIndex << " (" << m1.fragmentStart << " to "
0314                              << m1.fragmentEnd << ") and " << m1.moduleIndex << " (" << m2.fragmentStart << " to "
0315                              << m2.fragmentEnd << ")\n"
0316                              << dec;
0317                     } else if (m2.fragmentStart >= m1.fragmentEnd) {
0318                         break;
0319                     }
0320                 }
0321             }
0322 #endif
0323 
0324             // reset dwfl state
0325             m_modules.clear();
0326 
0327             dwfl_report_begin(m_dwfl);
0328             dwfl_report_end(m_dwfl, nullptr, nullptr);
0329 
0330             m_modulesDirty = false;
0331         }
0332 
0333         auto resolveFrame = [this](const Frame& frame) {
0334             return ResolvedFrame {intern(frame.function), intern(frame.file), frame.line};
0335         };
0336 
0337         ResolvedIP data;
0338         // find module for this instruction pointer
0339         auto fragment = lower_bound(
0340             m_moduleFragments.begin(), m_moduleFragments.end(), ip,
0341             [](const ModuleFragment& fragment, const uintptr_t ip) -> bool { return fragment.fragmentEnd < ip; });
0342         if (fragment != m_moduleFragments.end() && fragment->fragmentStart <= ip && fragment->fragmentEnd >= ip) {
0343             data.moduleIndex = fragment->moduleIndex;
0344 
0345             if (auto module = reportModule(*fragment)) {
0346                 const auto info = module->resolveAddress(ip);
0347                 data.frame = resolveFrame(info.frame);
0348                 std::transform(info.inlined.begin(), info.inlined.end(), std::back_inserter(data.inlined),
0349                                resolveFrame);
0350             }
0351         }
0352         return data;
0353     }
0354 
0355     size_t intern(const string& str, const char** internedString = nullptr)
0356     {
0357         if (str.empty()) {
0358             return 0;
0359         }
0360 
0361         const size_t id = m_internedData.size() + 1;
0362         auto inserted = m_internedData.insert({str, id});
0363         if (internedString) {
0364             *internedString = inserted.first->first.data();
0365         }
0366 
0367         if (!inserted.second) {
0368             return inserted.first->second;
0369         }
0370 
0371         out.write("s ");
0372         out.write(str);
0373         out.write("\n");
0374         return id;
0375     }
0376 
0377     void addModule(string fileName, const size_t moduleIndex, const uintptr_t addressStart,
0378                    const uintptr_t fragmentStart, const uintptr_t fragmentEnd)
0379     {
0380         m_moduleFragments.emplace_back(fileName, addressStart, fragmentStart, fragmentEnd, moduleIndex);
0381         m_modulesDirty = true;
0382     }
0383 
0384     void clearModules()
0385     {
0386         // TODO: optimize this, reuse modules that are still valid
0387         m_moduleFragments.clear();
0388         m_modulesDirty = true;
0389     }
0390 
0391     size_t addIp(const uintptr_t instructionPointer)
0392     {
0393         if (!instructionPointer) {
0394             return 0;
0395         }
0396 
0397         const size_t ipId = m_encounteredIps.size() + 1;
0398         auto inserted = m_encounteredIps.insert({instructionPointer, ipId});
0399         if (!inserted.second) {
0400             return inserted.first->second;
0401         }
0402 
0403         const auto ip = resolve(instructionPointer);
0404         out.write("i %zx %zx", instructionPointer, ip.moduleIndex);
0405         if (ip.frame.functionIndex || ip.frame.fileIndex) {
0406             out.write(" %zx", ip.frame.functionIndex);
0407             if (ip.frame.fileIndex) {
0408                 out.write(" %zx %x", ip.frame.fileIndex, ip.frame.line);
0409                 for (const auto& inlined : ip.inlined) {
0410                     out.write(" %zx %zx %x", inlined.functionIndex, inlined.fileIndex, inlined.line);
0411                 }
0412             }
0413         }
0414         out.write("\n");
0415         return ipId;
0416     }
0417 
0418     LineWriter out;
0419 
0420 private:
0421     Module* reportModule(const ModuleFragment& module)
0422     {
0423         if (startsWith(module.fileName, "linux-vdso.so")) {
0424             return nullptr;
0425         }
0426 
0427         auto& ret = m_modules[module.fileName];
0428         if (ret.module)
0429             return &ret;
0430 
0431         auto dwflModule = dwfl_addrmodule(m_dwfl, module.addressStart);
0432         if (!dwflModule) {
0433             dwfl_report_begin_add(m_dwfl);
0434             dwflModule = dwfl_report_elf(m_dwfl, module.fileName.c_str(), module.fileName.c_str(), -1,
0435                                          module.addressStart, false);
0436             dwfl_report_end(m_dwfl, nullptr, nullptr);
0437 
0438             if (!dwflModule) {
0439                 error_out << "Failed to report module for " << module.fileName << ": " << dwfl_errmsg(dwfl_errno())
0440                           << endl;
0441                 return nullptr;
0442             }
0443         }
0444 
0445         ret = Module(module.fileName, module.addressStart, dwflModule, &m_symbolCache);
0446         return &ret;
0447     }
0448 
0449     vector<ModuleFragment> m_moduleFragments;
0450     Dwfl* m_dwfl = nullptr;
0451     char* m_debugPath = nullptr;
0452     Dwfl_Callbacks m_callbacks;
0453     SymbolCache m_symbolCache;
0454     bool m_modulesDirty = false;
0455 
0456     tsl::robin_map<string, size_t> m_internedData;
0457     tsl::robin_map<uintptr_t, size_t> m_encounteredIps;
0458     tsl::robin_map<string, Module> m_modules;
0459 };
0460 
0461 struct Stats
0462 {
0463     uint64_t allocations = 0;
0464     uint64_t leakedAllocations = 0;
0465     uint64_t temporaryAllocations = 0;
0466 } c_stats;
0467 
0468 void exitHandler()
0469 {
0470     fflush(stdout);
0471     fprintf(stderr,
0472             "heaptrack stats:\n"
0473             "\tallocations:          \t%" PRIu64 "\n"
0474             "\tleaked allocations:   \t%" PRIu64 "\n"
0475             "\ttemporary allocations:\t%" PRIu64 "\n",
0476             c_stats.allocations, c_stats.leakedAllocations, c_stats.temporaryAllocations);
0477 }
0478 }
0479 
0480 int main(int /*argc*/, char** /*argv*/)
0481 {
0482     [] {
0483         // NOTE: we disable debuginfod by default as it can otherwise lead to
0484         //       nasty delays otherwise which are highly unexpected to users
0485         //       if desired, they can opt in to that via
0486         //
0487         //       export HEAPTRACK_ENABLE_DEBUGINFOD=1
0488         if (!getenv("DEBUGINFOD_URLS")) {
0489             return;
0490         }
0491 
0492         auto enable = getenv("HEAPTRACK_ENABLE_DEBUGINFOD");
0493         if (!enable || !atoi(enable)) {
0494             fprintf(stderr,
0495                     "NOTE: heaptrack detected DEBUGINFOD_URLS but will disable it to prevent \n"
0496                     "unintended network delays during recording\n"
0497                     "If you really want to use DEBUGINFOD, export HEAPTRACK_ENABLE_DEBUGINFOD=1\n");
0498             unsetenv("DEBUGINFOD_URLS");
0499         }
0500     }();
0501 
0502     // optimize: we only have a single thread
0503     ios_base::sync_with_stdio(false);
0504 #ifdef __linux__
0505     __fsetlocking(stdout, FSETLOCKING_BYCALLER);
0506     __fsetlocking(stdin, FSETLOCKING_BYCALLER);
0507 #endif
0508 
0509     // output data at end, even when we get terminated
0510     std::atexit(exitHandler);
0511 
0512     AccumulatedTraceData data;
0513 
0514     LineReader reader;
0515 
0516     string exe;
0517 
0518     PointerMap ptrToIndex;
0519     uint64_t lastPtr = 0;
0520     AllocationInfoSet allocationInfos;
0521 
0522     while (reader.getLine(cin)) {
0523         if (reader.mode() == 'v') {
0524             unsigned int heaptrackVersion = 0;
0525             reader >> heaptrackVersion;
0526             unsigned int fileVersion = 0;
0527             reader >> fileVersion;
0528             if (fileVersion >= 3) {
0529                 reader.setExpectedSizedStrings(true);
0530             }
0531             data.out.write("%s\n", reader.line().c_str());
0532         } else if (reader.mode() == 'x') {
0533             if (!exe.empty()) {
0534                 error_out << "received duplicate exe event - child process tracking is not yet supported" << endl;
0535                 return 1;
0536             }
0537             reader >> exe;
0538         } else if (reader.mode() == 'm') {
0539             string fileName;
0540             reader >> fileName;
0541             if (fileName == "-") {
0542                 data.clearModules();
0543             } else {
0544                 if (fileName == "x") {
0545                     fileName = exe;
0546                 }
0547                 const char* internedString = nullptr;
0548                 const auto moduleIndex = data.intern(fileName, &internedString);
0549                 uintptr_t addressStart = 0;
0550                 if (!(reader >> addressStart)) {
0551                     error_out << "failed to parse line: " << reader.line() << endl;
0552                     return 1;
0553                 }
0554                 uintptr_t vAddr = 0;
0555                 uintptr_t memSize = 0;
0556                 while ((reader >> vAddr) && (reader >> memSize)) {
0557                     data.addModule(fileName, moduleIndex, addressStart, addressStart + vAddr,
0558                                    addressStart + vAddr + memSize);
0559                 }
0560             }
0561         } else if (reader.mode() == 't') {
0562             uintptr_t instructionPointer = 0;
0563             size_t parentIndex = 0;
0564             if (!(reader >> instructionPointer) || !(reader >> parentIndex)) {
0565                 error_out << "failed to parse line: " << reader.line() << endl;
0566                 return 1;
0567             }
0568             // ensure ip is encountered
0569             const auto ipId = data.addIp(instructionPointer);
0570             // trace point, map current output index to parent index
0571             data.out.writeHexLine('t', ipId, parentIndex);
0572         } else if (reader.mode() == '+') {
0573             ++c_stats.allocations;
0574             ++c_stats.leakedAllocations;
0575             uint64_t size = 0;
0576             TraceIndex traceId;
0577             uint64_t ptr = 0;
0578             if (!(reader >> size) || !(reader >> traceId.index) || !(reader >> ptr)) {
0579                 error_out << "failed to parse line: " << reader.line() << endl;
0580                 continue;
0581             }
0582 
0583             AllocationInfoIndex index;
0584             if (allocationInfos.add(size, traceId, &index)) {
0585                 data.out.writeHexLine('a', size, traceId.index);
0586             }
0587             ptrToIndex.addPointer(ptr, index);
0588             lastPtr = ptr;
0589             data.out.writeHexLine('+', index.index);
0590         } else if (reader.mode() == '-') {
0591             uint64_t ptr = 0;
0592             if (!(reader >> ptr)) {
0593                 error_out << "failed to parse line: " << reader.line() << endl;
0594                 continue;
0595             }
0596             bool temporary = lastPtr == ptr;
0597             lastPtr = 0;
0598             auto allocation = ptrToIndex.takePointer(ptr);
0599             if (!allocation.second) {
0600                 continue;
0601             }
0602             data.out.writeHexLine('-', allocation.first.index);
0603             if (temporary) {
0604                 ++c_stats.temporaryAllocations;
0605             }
0606             --c_stats.leakedAllocations;
0607         } else {
0608             data.out.write("%s\n", reader.line().c_str());
0609         }
0610     }
0611 
0612     return 0;
0613 }