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 }