// Copyright Epic Games, Inc. All Rights Reserved. #include "symbol_resolver.h" #include #include #include #include #include #include #include #include #include #include #include #if !ZEN_PLATFORM_WINDOWS # include # include #endif #if ZEN_PLATFORM_WINDOWS ZEN_THIRD_PARTY_INCLUDES_START # include # include # include # include # include # include # include # include # include # include # include ZEN_THIRD_PARTY_INCLUDES_END # include ZEN_THIRD_PARTY_INCLUDES_START # include ZEN_THIRD_PARTY_INCLUDES_END #endif // ZEN_PLATFORM_WINDOWS namespace zen::trace_detail { ////////////////////////////////////////////////////////////////////////////// // Null resolver (used when symbolication is off or unsupported) class NullSymbolResolver final : public SymbolResolver { public: void LoadModule(const ModuleInfo&) override {} std::string Resolve(uint64_t) const override { return {}; } }; #if ZEN_PLATFORM_WINDOWS ////////////////////////////////////////////////////////////////////////////// // Helpers shared by Windows backends static std::string FormatSymbol(std::string_view Name, uint64_t Displacement) { if (Displacement == 0) { return std::string(Name); } return fmt::format("{} + 0x{:X}", Name, Displacement); } ////////////////////////////////////////////////////////////////////////////// // Memory-mapped file helper namespace { struct MappedFile { const void* Data = nullptr; size_t Size = 0; HANDLE FileHandle = INVALID_HANDLE_VALUE; HANDLE MappingHandle = nullptr; MappedFile() = default; ~MappedFile() { Close(); } bool Open(const std::filesystem::path& Path) { FileHandle = CreateFileW(Path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (FileHandle == INVALID_HANDLE_VALUE) { return false; } LARGE_INTEGER FileSize; if (!GetFileSizeEx(FileHandle, &FileSize) || FileSize.QuadPart == 0) { Close(); return false; } MappingHandle = CreateFileMappingW(FileHandle, nullptr, PAGE_READONLY, 0, 0, nullptr); if (MappingHandle == nullptr) { Close(); return false; } Data = MapViewOfFile(MappingHandle, FILE_MAP_READ, 0, 0, 0); if (Data == nullptr) { Close(); return false; } Size = size_t(FileSize.QuadPart); return true; } void Close() { if (Data != nullptr) { UnmapViewOfFile(Data); Data = nullptr; } if (MappingHandle != nullptr) { CloseHandle(MappingHandle); MappingHandle = nullptr; } if (FileHandle != INVALID_HANDLE_VALUE) { CloseHandle(FileHandle); FileHandle = INVALID_HANDLE_VALUE; } Size = 0; } MappedFile(const MappedFile&) = delete; MappedFile& operator=(const MappedFile&) = delete; }; // Format an ImageId (16-byte GUID + 4-byte Age) as the hex key used in // symbol server URLs: , e.g. "A1B2C3...1". std::string FormatImageIdKey(const eastl::vector& ImageId) { if (ImageId.size() < 20) { return {}; } // GUID bytes are stored as {Data1 LE, Data2 LE, Data3 LE, Data4[8]}. // The symbol server key encodes Data1/2/3 as big-endian hex. const uint8_t* G = ImageId.data(); uint32_t Data1; uint16_t Data2; uint16_t Data3; memcpy(&Data1, G + 0, 4); memcpy(&Data2, G + 4, 2); memcpy(&Data3, G + 6, 2); uint32_t Age; memcpy(&Age, ImageId.data() + 16, 4); return fmt::format("{:08X}{:04X}{:04X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:x}", Data1, Data2, Data3, G[8], G[9], G[10], G[11], G[12], G[13], G[14], G[15], Age); } // PdbName originates from module metadata in an untrusted trace file and is used // to build both a filesystem path and an HTTP request path. Restrict it to a safe // subset so a malicious trace cannot traverse out of the cache dir, inject URL // syntax, or trip cross-platform path parsing quirks (e.g. `\` is a separator on // Windows but not POSIX, so filename() doesn't always strip it). bool IsSafePdbName(std::string_view Name) { constexpr AsciiSet SafeChars( "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "0123456789" "._-+"); if (Name.empty() || Name.size() > 255 || Name == "." || Name == "..") { return false; } return AsciiSet::HasOnly(Name, SafeChars); } const std::filesystem::path& GetSymbolCacheDir() { // Use %TEMP%/zen-symbols as the default cache location. static const std::filesystem::path s_CacheDir = [] { std::filesystem::path TempDir = std::filesystem::temp_directory_path(); return TempDir / "zen-symbols"; }(); return s_CacheDir; } // Try to download a PDB from a symbol server URL. // Returns the local cache path on success, empty path on failure. std::filesystem::path DownloadPdb(std::string_view ServerUrl, std::string_view PdbName, const std::string& ImageIdKey, const std::filesystem::path& CacheDir) { // Cache path mirrors the server structure std::filesystem::path CachePath = CacheDir / PdbName / ImageIdKey / PdbName; // Already cached? std::error_code Ec; if (std::filesystem::exists(CachePath, Ec)) { return CachePath; } ZEN_INFO("Downloading {} from symbol server...", PdbName); try { std::string RequestPath = fmt::format("/{}/{}/{}", PdbName, ImageIdKey, PdbName); zen::HttpClientSettings Settings; Settings.Timeout = std::chrono::milliseconds(30000); Settings.ConnectTimeout = std::chrono::milliseconds(5000); Settings.FollowRedirects = true; Settings.ExpectedErrorCodes = {zen::HttpResponseCode::NotFound}; zen::HttpClient Http(ServerUrl, Settings); zen::HttpClient::Response Response = Http.Get(RequestPath); if (!Response) { ZEN_DEBUG("Symbol server: {} not found (HTTP {})", PdbName, int(Response.StatusCode)); return {}; } // Write to cache using zencore file I/O std::filesystem::create_directories(CachePath.parent_path(), Ec); zen::WriteFile(CachePath, Response.ResponsePayload); ZEN_INFO("Cached {} ({})", PdbName, zen::NiceBytes(Response.ResponsePayload.GetSize())); return CachePath; } catch (const std::exception& Ex) { ZEN_WARN("Symbol server download failed for {}: {}", PdbName, Ex.what()); return {}; } } // Parse _NT_SYMBOL_PATH to extract symbol server URLs. // Format: "srv**" or "symsrv*symsrv.dll**" or just a URL. // Returns a list of server URLs to try. const std::vector& ParseSymbolPath() { static const std::vector s_Servers = [] { std::vector Servers; const char* EnvPath = std::getenv("_NT_SYMBOL_PATH"); if (EnvPath == nullptr || EnvPath[0] == '\0') { // Default to Microsoft public symbol server Servers.push_back("https://msdl.microsoft.com/download/symbols"); return Servers; } std::string_view Path(EnvPath); // Split on ';' for multiple entries while (!Path.empty()) { size_t Semi = Path.find(';'); std::string_view Entry = (Semi != std::string_view::npos) ? Path.substr(0, Semi) : Path; Path = (Semi != std::string_view::npos) ? Path.substr(Semi + 1) : std::string_view{}; // Look for srv* or symsrv* prefix — the last '*'-delimited token is the server URL. if (Entry.substr(0, 4) == "srv*" || Entry.substr(0, 7) == "symsrv*") { size_t LastStar = Entry.rfind('*'); if (LastStar != std::string_view::npos && LastStar + 1 < Entry.size()) { std::string_view Url = Entry.substr(LastStar + 1); if (Url.substr(0, 4) == "http") { Servers.emplace_back(Url); } } } } if (Servers.empty()) { Servers.push_back("https://msdl.microsoft.com/download/symbols"); } return Servers; }(); return s_Servers; } // Copy a local PDB into the symbol cache so that future analysis of traces // from this build succeeds even after the binary is recompiled. void CacheLocalPdb(const std::filesystem::path& PdbPath, std::string_view PdbName, const std::string& ImageIdKey, const std::filesystem::path& CacheDir) { std::filesystem::path CachePath = CacheDir / PdbName / ImageIdKey / PdbName; std::error_code Ec; if (std::filesystem::exists(CachePath, Ec)) { return; } std::filesystem::create_directories(CachePath.parent_path(), Ec); if (Ec) { return; } std::filesystem::copy_file(PdbPath, CachePath, std::filesystem::copy_options::skip_existing, Ec); if (!Ec) { uint64_t Size = std::filesystem::file_size(PdbPath, Ec); ZEN_INFO("Cached local PDB {} ({})", PdbName, zen::NiceBytes(Size)); } } // Look for a PDB in the local symbol cache or download from symbol servers. // Returns the cache path on success, empty path on failure. std::filesystem::path FindPdbInCacheOrServer(std::string_view PdbName, const std::string& ImageIdKey, const std::filesystem::path& CacheDir) { if (ImageIdKey.empty()) { return {}; } // Check local cache first (includes previously cached local PDBs and // earlier symbol server downloads). std::filesystem::path CachePath = CacheDir / PdbName / ImageIdKey / PdbName; std::error_code Ec; if (std::filesystem::exists(CachePath, Ec)) { return CachePath; } // Try symbol servers const std::vector& Servers = ParseSymbolPath(); for (const std::string& Server : Servers) { std::filesystem::path Downloaded = DownloadPdb(Server, PdbName, ImageIdKey, CacheDir); if (!Downloaded.empty()) { return Downloaded; } } return {}; } } // namespace ////////////////////////////////////////////////////////////////////////////// // RawPdb backend — reads PDB files directly class PdbSymbolResolver final : public SymbolResolver { public: void LoadModule(const ModuleInfo& Module) override; std::string Resolve(uint64_t Address) const override; private: struct FunctionEntry { uint64_t Address; uint32_t Size; std::string Name; }; struct LineEntry { uint64_t Address; uint32_t CodeSize; uint32_t Line; std::string File; // shortened: basename only }; std::vector m_Functions; std::vector m_Lines; }; void PdbSymbolResolver::LoadModule(const ModuleInfo& Module) { if (Module.FullPath.empty() || Module.Base == 0) { return; } std::string ImageIdKey = FormatImageIdKey(Module.ImageId); std::string PdbName = std::filesystem::path(Module.FullPath).filename().replace_extension(".pdb").string(); const std::filesystem::path& CacheDir = GetSymbolCacheDir(); if (!IsSafePdbName(PdbName)) { ZEN_WARN("Rejecting unsafe PDB name from trace: '{}'", PdbName); return; } // Try local PDB first (next to the binary) std::filesystem::path PdbPath(Module.FullPath); PdbPath.replace_extension(".pdb"); std::error_code Ec; bool FromLocal = false; if (std::filesystem::exists(PdbPath, Ec)) { FromLocal = true; } else { // Try symbol cache / symbol server download PdbPath = FindPdbInCacheOrServer(PdbName, ImageIdKey, CacheDir); if (PdbPath.empty()) { ZEN_DEBUG("PDB not found locally or on symbol server: {}", PdbName); return; } } MappedFile File; if (!File.Open(PdbPath)) { ZEN_DEBUG("Failed to open PDB: {}", PdbPath.string()); return; } if (PDB::ValidateFile(File.Data, File.Size) != PDB::ErrorCode::Success) { ZEN_DEBUG("Invalid PDB file: {}", PdbPath.string()); return; } PDB::RawFile RawFile = PDB::CreateRawFile(File.Data); PDB::InfoStream PdbInfoStream(RawFile); // Verify the PDB matches the traced module by comparing GUID + Age. // The trace stores ImageId as 16 bytes GUID followed by 4 bytes Age. if (Module.ImageId.size() >= 20) { const PDB::Header* PdbHeader = PdbInfoStream.GetHeader(); // Only compare the GUID, not the age. The symbol server may return a // PDB with a higher age (from incremental linking) which is compatible. if (memcmp(&PdbHeader->guid, Module.ImageId.data(), 16) != 0) { if (FromLocal) { // The local PDB no longer matches — the binary was recompiled // since the trace was taken. Try the symbol cache / servers for // the original PDB. File.Close(); PdbPath = FindPdbInCacheOrServer(PdbName, ImageIdKey, CacheDir); if (PdbPath.empty()) { ZEN_WARN("PDB mismatch for {} — binary was recompiled and no cached symbols available", Module.Name); return; } FromLocal = false; if (!File.Open(PdbPath)) { ZEN_DEBUG("Failed to open cached PDB: {}", PdbPath.string()); return; } if (PDB::ValidateFile(File.Data, File.Size) != PDB::ErrorCode::Success) { ZEN_DEBUG("Invalid cached PDB: {}", PdbPath.string()); return; } RawFile = PDB::CreateRawFile(File.Data); PdbInfoStream = PDB::InfoStream(RawFile); const PDB::Header* CachedHeader = PdbInfoStream.GetHeader(); if (memcmp(&CachedHeader->guid, Module.ImageId.data(), 16) != 0) { ZEN_WARN("PDB GUID mismatch for {} — skipping", Module.Name); return; } } else { ZEN_WARN("PDB GUID mismatch for {} — skipping", Module.Name); return; } } } // Cache the local PDB so that future analysis of traces from this build // succeeds even after the binary is recompiled. if (FromLocal && !ImageIdKey.empty()) { CacheLocalPdb(PdbPath, PdbName, ImageIdKey, CacheDir); } if (PDB::HasValidDBIStream(RawFile) != PDB::ErrorCode::Success) { return; } const PDB::DBIStream DbiStream = PDB::CreateDBIStream(RawFile); if (DbiStream.HasValidImageSectionStream(RawFile) != PDB::ErrorCode::Success) { return; } const PDB::ImageSectionStream ImageSections = DbiStream.CreateImageSectionStream(RawFile); uint64_t ModuleBase = Module.Base; uint32_t SkippedModules = 0; size_t FunctionCountBeforeModuleSymbols = m_Functions.size(); // Collect functions from module symbol streams (S_GPROC32 / S_LPROC32) { const PDB::ModuleInfoStream ModInfoStream = DbiStream.CreateModuleInfoStream(RawFile); const PDB::ArrayView Modules = ModInfoStream.GetModules(); for (const PDB::ModuleInfoStream::Module& Mod : Modules) { if (!Mod.HasSymbolStream()) { ++SkippedModules; continue; } const PDB::ModuleSymbolStream SymStream = Mod.CreateSymbolStream(RawFile); SymStream.ForEachSymbol([&](const PDB::CodeView::DBI::Record* Record) { using Kind = PDB::CodeView::DBI::SymbolRecordKind; const Kind K = Record->header.kind; const auto& Data = Record->data; if (K == Kind::S_GPROC32 || K == Kind::S_LPROC32 || K == Kind::S_GPROC32_ID || K == Kind::S_LPROC32_ID) { uint32_t Rva = ImageSections.ConvertSectionOffsetToRVA(Data.S_GPROC32.section, Data.S_GPROC32.offset); if (Rva != 0) { m_Functions.push_back({ModuleBase + Rva, Data.S_GPROC32.codeSize, Data.S_GPROC32.name}); } } }); } } // Public symbols as fallback only when module symbol streams did not yield any // functions. Building the coalesced symbol-record stream is expensive and can // allocate tens of megabytes for large PDBs. if (FunctionCountBeforeModuleSymbols == m_Functions.size() && DbiStream.HasValidPublicSymbolStream(RawFile) == PDB::ErrorCode::Success) { const PDB::PublicSymbolStream PubStream = DbiStream.CreatePublicSymbolStream(RawFile); const PDB::CoalescedMSFStream SymRecords = DbiStream.CreateSymbolRecordStream(RawFile); for (const PDB::HashRecord& Hash : PubStream.GetRecords()) { const PDB::CodeView::DBI::Record* Record = SymRecords.GetDataAtOffset(Hash.offset); if (Record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_PUB32) { uint32_t Rva = ImageSections.ConvertSectionOffsetToRVA(Record->data.S_PUB32.section, Record->data.S_PUB32.offset); if (Rva != 0) { m_Functions.push_back({ModuleBase + Rva, 0, Record->data.S_PUB32.name}); } } } } // Collect line information from module line streams if (PdbInfoStream.HasNamesStream()) { const PDB::NamesStream NamesStream = PdbInfoStream.CreateNamesStream(RawFile); const PDB::ModuleInfoStream ModInfoStream2 = DbiStream.CreateModuleInfoStream(RawFile); for (const PDB::ModuleInfoStream::Module& Mod : ModInfoStream2.GetModules()) { if (!Mod.HasLineStream()) { continue; } const PDB::ModuleLineStream LineStream = Mod.CreateLineStream(RawFile); // Two passes: first find the checksums section, then process lines. const PDB::CodeView::DBI::FileChecksumHeader* ModuleChecksumBase = nullptr; LineStream.ForEachSection([&](const PDB::CodeView::DBI::LineSection* Section) { if (Section->header.kind == PDB::CodeView::DBI::DebugSubsectionKind::S_FILECHECKSUMS) { ModuleChecksumBase = &Section->checksumHeader; } }); if (ModuleChecksumBase == nullptr) { continue; } LineStream.ForEachSection([&](const PDB::CodeView::DBI::LineSection* Section) { if (Section->header.kind != PDB::CodeView::DBI::DebugSubsectionKind::S_LINES) { return; } uint16_t SecIdx = Section->linesHeader.sectionIndex; uint32_t SecOff = Section->linesHeader.sectionOffset; LineStream.ForEachLinesBlock(Section, [&](const PDB::CodeView::DBI::LinesFileBlockHeader* Block, const PDB::CodeView::DBI::Line* Lines, const PDB::CodeView::DBI::Column*) { if (Block->numLines == 0) { return; } // Resolve filename for this block const auto* Checksum = PDB::Pointer::Offset( ModuleChecksumBase, Block->fileChecksumOffset); const char* FullFile = NamesStream.GetFilename(Checksum->filenameOffset); // Extract basename std::string_view FileView(FullFile); size_t Cut = FileView.find_last_of("\\/"); std::string Basename(Cut != std::string_view::npos ? FileView.substr(Cut + 1) : FileView); for (uint32_t I = 0; I < Block->numLines; ++I) { uint32_t Rva = ImageSections.ConvertSectionOffsetToRVA(SecIdx, SecOff + Lines[I].offset); if (Rva == 0) { continue; } uint32_t CodeSize = 0; if (I + 1 < Block->numLines) { CodeSize = Lines[I + 1].offset - Lines[I].offset; } else { CodeSize = Section->linesHeader.codeSize - Lines[I].offset; } m_Lines.push_back({ModuleBase + Rva, CodeSize, Lines[I].linenumStart, Basename}); } }); }); } } std::sort(m_Functions.begin(), m_Functions.end(), [](const FunctionEntry& A, const FunctionEntry& B) { return A.Address < B.Address; }); std::sort(m_Lines.begin(), m_Lines.end(), [](const LineEntry& A, const LineEntry& B) { return A.Address < B.Address; }); if (SkippedModules > 0) { ZEN_INFO("Loaded {} symbols, {} line records from {} ({} modules without embedded debug info)", m_Functions.size(), m_Lines.size(), Module.Name, SkippedModules); } else { ZEN_INFO("Loaded {} symbols, {} line records from {}", m_Functions.size(), m_Lines.size(), Module.Name); } } std::string PdbSymbolResolver::Resolve(uint64_t Address) const { if (m_Functions.empty()) { return {}; } // Resolve function name auto FnIt = std::upper_bound(m_Functions.begin(), m_Functions.end(), Address, [](uint64_t Addr, const FunctionEntry& E) { return Addr < E.Address; }); if (FnIt == m_Functions.begin()) { return {}; } --FnIt; if (FnIt->Size > 0 && Address >= FnIt->Address + FnIt->Size) { return {}; } std::string Result = FormatSymbol(FnIt->Name, Address - FnIt->Address); // Resolve file:line if (!m_Lines.empty()) { auto LineIt = std::upper_bound(m_Lines.begin(), m_Lines.end(), Address, [](uint64_t Addr, const LineEntry& E) { return Addr < E.Address; }); if (LineIt != m_Lines.begin()) { --LineIt; if (LineIt->CodeSize == 0 || Address < LineIt->Address + LineIt->CodeSize) { Result += fmt::format(" [{}:{}]", LineIt->File, LineIt->Line); } } } return Result; } ////////////////////////////////////////////////////////////////////////////// // DbgHelp backend — uses Windows symbol API, supports _NT_SYMBOL_PATH class DbgHelpSymbolResolver final : public SymbolResolver { public: DbgHelpSymbolResolver(); ~DbgHelpSymbolResolver() override; void LoadModule(const ModuleInfo& Module) override; std::string Resolve(uint64_t Address) const override; private: // Map trace addresses to DbgHelp addresses when the loaded base differs. struct ModuleMapping { uint64_t TraceBase; uint64_t TraceEnd; int64_t Delta; // DbgHelpBase - TraceBase }; HANDLE m_Process = nullptr; std::vector m_Mappings; // DbgHelp is not thread-safe; its API functions require serialized access. This // mutex covers every DbgHelp call (SymInitialize/SymLoadModuleExW/SymFromAddr/ // SymGetLineFromAddr64) and therefore serializes all parallel symbol lookups in // trace_analyze. For workloads where lookup throughput matters, prefer // PdbSymbolResolver, which parses PDBs directly and is lock-free per-module. mutable std::mutex m_Mutex; }; DbgHelpSymbolResolver::DbgHelpSymbolResolver() { std::lock_guard Lock(m_Mutex); // Use a unique pseudo-handle so we don't conflict with the runtime // symbol handler used by callstack.cpp / crashhandler.cpp. m_Process = reinterpret_cast(static_cast(0xDEAD0042)); // NULL search path lets DbgHelp use _NT_SYMBOL_PATH and its defaults. if (!SymInitialize(m_Process, nullptr, FALSE)) { ZEN_WARN("DbgHelp: SymInitialize failed (error {})", GetLastError()); m_Process = nullptr; } } DbgHelpSymbolResolver::~DbgHelpSymbolResolver() { std::lock_guard Lock(m_Mutex); if (m_Process != nullptr) { SymCleanup(m_Process); } } void DbgHelpSymbolResolver::LoadModule(const ModuleInfo& Module) { std::lock_guard Lock(m_Mutex); if (m_Process == nullptr || Module.FullPath.empty() || Module.Base == 0) { return; } std::filesystem::path ModulePath(Module.FullPath); std::wstring WidePath = ModulePath.wstring(); DWORD64 LoadedBase = SymLoadModuleExW(m_Process, nullptr, WidePath.c_str(), nullptr, Module.Base, Module.Size, nullptr, 0); if (LoadedBase == 0) { DWORD Err = GetLastError(); if (Err != ERROR_SUCCESS) { ZEN_DEBUG("DbgHelp: failed to load {}: error {}", Module.Name, Err); } return; } int64_t Delta = int64_t(LoadedBase) - int64_t(Module.Base); if (Delta != 0) { ZEN_DEBUG("DbgHelp: {} loaded at 0x{:X} (trace base 0x{:X}, delta {:+})", Module.Name, LoadedBase, Module.Base, Delta); } uint64_t TraceEnd = Module.Base + (Module.Size > 0 ? Module.Size : 0x1000000); m_Mappings.push_back({Module.Base, TraceEnd, Delta}); ZEN_INFO("DbgHelp: loaded symbols for {}", Module.Name); } std::string DbgHelpSymbolResolver::Resolve(uint64_t Address) const { std::lock_guard Lock(m_Mutex); if (m_Process == nullptr) { return {}; } // Translate the trace address to the DbgHelp address space uint64_t DbgAddr = Address; for (const ModuleMapping& M : m_Mappings) { if (Address >= M.TraceBase && Address < M.TraceEnd) { DbgAddr = uint64_t(int64_t(Address) + M.Delta); break; } } alignas(SYMBOL_INFO) char Buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME]; SYMBOL_INFO* SymInfo = reinterpret_cast(Buffer); SymInfo->SizeOfStruct = sizeof(SYMBOL_INFO); SymInfo->MaxNameLen = MAX_SYM_NAME; DWORD64 Displacement = 0; if (!SymFromAddr(m_Process, DbgAddr, &Displacement, SymInfo)) { return {}; } std::string Result = FormatSymbol(std::string_view(SymInfo->Name, SymInfo->NameLen), Displacement); IMAGEHLP_LINE64 LineInfo = {}; LineInfo.SizeOfStruct = sizeof(IMAGEHLP_LINE64); DWORD LineDisplacement = 0; if (SymGetLineFromAddr64(m_Process, DbgAddr, &LineDisplacement, &LineInfo)) { std::string_view FileView(LineInfo.FileName); size_t Cut = FileView.find_last_of("\\/"); std::string_view Basename = (Cut != std::string_view::npos) ? FileView.substr(Cut + 1) : FileView; Result += fmt::format(" [{}:{}]", Basename, LineInfo.LineNumber); } return Result; } #endif // ZEN_PLATFORM_WINDOWS ////////////////////////////////////////////////////////////////////////////// // Shared helpers for subprocess-based backends namespace { // CreateProc parses the command line as space-separated tokens, so argv[0] // must be quoted if the resolved executable path contains spaces (e.g. an // Xcode toolchain location on macOS). std::string QuoteIfNeeded(std::string_view Path) { if (Path.find(' ') == std::string_view::npos) { return std::string(Path); } return fmt::format("\"{}\"", Path); } } // namespace ////////////////////////////////////////////////////////////////////////////// // llvm-symbolizer backend — cross-platform, shells out to `llvm-symbolizer` // and speaks its interactive protocol over pipes. // // Protocol (one request / response): // We write: " 0x\n" // It replies: "FunctionName\n" // "file:line:col\n" // "\n" <-- blank line terminates the record // // Launch flags: // --demangle demangle C++ names (default, but explicit) // --output-style=LLVM stable two-line format described above // --functions=linkage keep template arguments visible // --relative-address treat the address as an offset from module base // --inlining=false emit one frame per address (no inline expansion) class LlvmSymbolizerResolver final : public SymbolResolver { public: LlvmSymbolizerResolver() = default; ~LlvmSymbolizerResolver() override; void LoadModule(const ModuleInfo& Module) override; std::string Resolve(uint64_t Address) const override; private: struct Module { std::string FullPath; uint64_t Base = 0; uint64_t End = 0; }; const Module* FindModule(uint64_t Address) const; bool EnsureProcess() const; bool ReadLine(std::string& Out) const; std::string DoQuery(const Module& M, uint64_t RelAddress) const; std::vector m_Modules; // Subprocess + IO state. All accesses serialized under m_Mutex. mutable std::mutex m_Mutex; mutable bool m_Attempted = false; mutable bool m_Alive = false; mutable zen::ProcessHandle m_Process; mutable zen::StdinPipeHandles m_StdinPipe; mutable zen::StdoutPipeHandles m_StdoutPipe; mutable std::string m_ReadBuffer; // Cache resolved addresses (same mutex). mutable std::unordered_map m_Cache; }; LlvmSymbolizerResolver::~LlvmSymbolizerResolver() { std::lock_guard Lock(m_Mutex); if (m_Alive) { // Closing stdin lets llvm-symbolizer exit cleanly on EOF. m_StdinPipe.CloseWriteEnd(); m_Process.Wait(2000); if (m_Process.IsRunning()) { m_Process.Terminate(0); } } } void LlvmSymbolizerResolver::LoadModule(const ModuleInfo& Mod) { if (Mod.FullPath.empty() || Mod.Base == 0) { return; } // llvm-symbolizer auto-discovers adjacent debug info (Foo.dSYM on Mac, // .gnu_debuglink / build-id sources on Linux, Foo.pdb on Windows). If the // binary itself isn't present locally, there's nothing we can do. std::error_code Ec; if (!std::filesystem::exists(Mod.FullPath, Ec)) { ZEN_DEBUG("llvm-symbolizer: binary not found for {} at {}", Mod.Name, Mod.FullPath); return; } uint64_t End = Mod.Base + (Mod.Size > 0 ? Mod.Size : 0x1000000); std::lock_guard Lock(m_Mutex); m_Modules.push_back({Mod.FullPath, Mod.Base, End}); ZEN_INFO("llvm-symbolizer: registered {} [0x{:X}..0x{:X})", Mod.Name, Mod.Base, End); } std::string LlvmSymbolizerResolver::Resolve(uint64_t Address) const { std::lock_guard Lock(m_Mutex); auto CacheIt = m_Cache.find(Address); if (CacheIt != m_Cache.end()) { return CacheIt->second; } const Module* M = FindModule(Address); if (M == nullptr) { m_Cache.emplace(Address, std::string{}); return {}; } std::string Result = DoQuery(*M, Address - M->Base); m_Cache.emplace(Address, Result); return Result; } const LlvmSymbolizerResolver::Module* LlvmSymbolizerResolver::FindModule(uint64_t Address) const { for (const Module& M : m_Modules) { if (Address >= M.Base && Address < M.End) { return &M; } } return nullptr; } bool LlvmSymbolizerResolver::EnsureProcess() const { if (m_Attempted) { return m_Alive; } m_Attempted = true; std::filesystem::path Executable = SearchPathForExecutable("llvm-symbolizer"); if (!CreateStdinPipe(m_StdinPipe) || !CreateStdoutPipe(m_StdoutPipe)) { ZEN_WARN("llvm-symbolizer: failed to create pipes"); return false; } // Build the command line. CommandLine begins with the executable name (arg[0]). std::string CommandLine = fmt::format("{} --demangle --output-style=LLVM --functions=linkage --relative-address --inlining=false", QuoteIfNeeded(Executable.string())); CreateProcOptions Options; Options.StdinPipe = &m_StdinPipe; Options.StdoutPipe = &m_StdoutPipe; CreateProcResult Handle = CreateProc(Executable, CommandLine, Options); #if ZEN_PLATFORM_WINDOWS if (Handle == nullptr) #else if (Handle <= 0) #endif { ZEN_WARN("llvm-symbolizer: failed to launch '{}' - install LLVM or add to PATH", Executable.string()); m_StdinPipe.Close(); m_StdoutPipe.Close(); return false; } #if ZEN_PLATFORM_WINDOWS m_Process.Initialize(Handle); #else std::error_code Ec; m_Process.Initialize(int(Handle), Ec); if (Ec) { ZEN_WARN("llvm-symbolizer: ProcessHandle init failed: {}", Ec.message()); m_StdinPipe.Close(); m_StdoutPipe.Close(); return false; } #endif // Close the child-side handles in the parent. m_StdinPipe.CloseReadEnd(); m_StdoutPipe.CloseWriteEnd(); m_Alive = true; return true; } bool LlvmSymbolizerResolver::ReadLine(std::string& Out) const { // Search for a newline already in the buffer; if not, read more. for (;;) { size_t NewlinePos = m_ReadBuffer.find('\n'); if (NewlinePos != std::string::npos) { Out.assign(m_ReadBuffer, 0, NewlinePos); m_ReadBuffer.erase(0, NewlinePos + 1); // Trim a trailing \r (in case of CRLF line endings). if (!Out.empty() && Out.back() == '\r') { Out.pop_back(); } return true; } char Buffer[1024]; #if ZEN_PLATFORM_WINDOWS DWORD BytesRead = 0; if (!::ReadFile(m_StdoutPipe.ReadHandle, Buffer, sizeof(Buffer), &BytesRead, nullptr) || BytesRead == 0) { return false; } m_ReadBuffer.append(Buffer, BytesRead); #else ssize_t BytesRead = ::read(m_StdoutPipe.ReadFd, Buffer, sizeof(Buffer)); if (BytesRead <= 0) { if (BytesRead < 0 && errno == EINTR) { continue; } return false; } m_ReadBuffer.append(Buffer, static_cast(BytesRead)); #endif } } std::string LlvmSymbolizerResolver::DoQuery(const Module& M, uint64_t RelAddress) const { if (!EnsureProcess()) { return {}; } // Write " 0x\n". Paths with spaces must be quoted for llvm-symbolizer // interactive input; it accepts double quotes. std::string Line; if (M.FullPath.find(' ') != std::string::npos) { Line = fmt::format("\"{}\" 0x{:X}\n", M.FullPath, RelAddress); } else { Line = fmt::format("{} 0x{:X}\n", M.FullPath, RelAddress); } #if ZEN_PLATFORM_WINDOWS DWORD BytesWritten = 0; if (!::WriteFile(m_StdinPipe.WriteHandle, Line.data(), static_cast(Line.size()), &BytesWritten, nullptr) || BytesWritten != Line.size()) { ZEN_WARN("llvm-symbolizer: write failed, disabling backend"); m_Alive = false; return {}; } #else const char* Ptr = Line.data(); size_t Remaining = Line.size(); while (Remaining > 0) { ssize_t N = ::write(m_StdinPipe.WriteFd, Ptr, Remaining); if (N <= 0) { if (N < 0 && errno == EINTR) { continue; } ZEN_WARN("llvm-symbolizer: write failed, disabling backend"); m_Alive = false; return {}; } Ptr += N; Remaining -= static_cast(N); } #endif // Read lines until a blank line terminates the record. std::string Function; std::string Location; std::string Buf; int LineIdx = 0; while (ReadLine(Buf)) { if (Buf.empty()) { break; } if (LineIdx == 0) { Function = Buf; } else if (LineIdx == 1) { Location = Buf; } // Additional lines would be inline frames (--inlining=false suppresses them); ignore. ++LineIdx; } if (Function.empty() || Function == "??") { return {}; } std::string Result = std::move(Function); if (!Location.empty() && Location != "??:0:0") { // Location is "path:line:col" — trim to "basename:line" to match Windows output. std::string_view LocView(Location); size_t LastColon = LocView.find_last_of(':'); if (LastColon != std::string_view::npos) { LocView = LocView.substr(0, LastColon); } size_t Slash = LocView.find_last_of("/\\"); std::string_view FileLine = (Slash == std::string_view::npos) ? LocView : LocView.substr(Slash + 1); Result += fmt::format(" [{}]", FileLine); } return Result; } ////////////////////////////////////////////////////////////////////////////// // atos backend — macOS only. Apple's symbolizer; ships with Xcode + the CLT. // // Unlike llvm-symbolizer, atos accepts only one binary per process. We keep // one subprocess per loaded module and demultiplex queries by module path. // // Protocol (one request / response): // We write: "0x\n" // It replies: "Function (in Binary) (file.cpp:NN)\n" // or "Function (in Binary) + 0x\n" (no debug info) // or "0x
\n" (nothing known) // // Launched with: atos -o -l 0x // atos subtracts -l from each input address to get the file offset. #if ZEN_PLATFORM_MAC class AtosSymbolizerResolver final : public SymbolResolver { public: AtosSymbolizerResolver() = default; ~AtosSymbolizerResolver() override; void LoadModule(const ModuleInfo& Module) override; std::string Resolve(uint64_t Address) const override; private: struct Module { std::string FullPath; uint64_t Base = 0; uint64_t End = 0; }; // One atos subprocess per loaded module (atos is single-binary). struct AtosProcess { zen::ProcessHandle Process; zen::StdinPipeHandles StdinPipe; zen::StdoutPipeHandles StdoutPipe; std::string ReadBuffer; bool Alive = false; }; const Module* FindModule(uint64_t Address) const; AtosProcess* EnsureProcessFor(const Module& M) const; bool ReadLine(AtosProcess& P, std::string& Out) const; std::string DoQuery(const Module& M, uint64_t Address) const; std::vector m_Modules; mutable std::mutex m_Mutex; mutable std::unordered_map> m_Processes; mutable std::unordered_map m_Cache; }; AtosSymbolizerResolver::~AtosSymbolizerResolver() { std::lock_guard Lock(m_Mutex); for (auto& [Path, P] : m_Processes) { if (P && P->Alive) { P->StdinPipe.CloseWriteEnd(); P->Process.Wait(2000); if (P->Process.IsRunning()) { P->Process.Terminate(0); } } } } void AtosSymbolizerResolver::LoadModule(const ModuleInfo& Mod) { if (Mod.FullPath.empty() || Mod.Base == 0) { return; } std::error_code Ec; if (!std::filesystem::exists(Mod.FullPath, Ec)) { ZEN_DEBUG("atos: binary not found for {} at {}", Mod.Name, Mod.FullPath); return; } uint64_t End = Mod.Base + (Mod.Size > 0 ? Mod.Size : 0x1000000); std::lock_guard Lock(m_Mutex); m_Modules.push_back({Mod.FullPath, Mod.Base, End}); ZEN_INFO("atos: registered {} [0x{:X}..0x{:X})", Mod.Name, Mod.Base, End); } std::string AtosSymbolizerResolver::Resolve(uint64_t Address) const { std::lock_guard Lock(m_Mutex); auto CacheIt = m_Cache.find(Address); if (CacheIt != m_Cache.end()) { return CacheIt->second; } const Module* M = FindModule(Address); if (M == nullptr) { m_Cache.emplace(Address, std::string{}); return {}; } std::string Result = DoQuery(*M, Address); m_Cache.emplace(Address, Result); return Result; } const AtosSymbolizerResolver::Module* AtosSymbolizerResolver::FindModule(uint64_t Address) const { for (const Module& M : m_Modules) { if (Address >= M.Base && Address < M.End) { return &M; } } return nullptr; } AtosSymbolizerResolver::AtosProcess* AtosSymbolizerResolver::EnsureProcessFor(const Module& M) const { auto It = m_Processes.find(M.FullPath); if (It != m_Processes.end()) { return It->second.get(); } auto P = std::make_unique(); if (!CreateStdinPipe(P->StdinPipe) || !CreateStdoutPipe(P->StdoutPipe)) { ZEN_WARN("atos: failed to create pipes for {}", M.FullPath); auto [Ins, _] = m_Processes.emplace(M.FullPath, std::move(P)); return Ins->second.get(); // Alive = false } std::filesystem::path Executable = SearchPathForExecutable("atos"); std::string CommandLine = fmt::format("{} -o \"{}\" -l 0x{:X}", QuoteIfNeeded(Executable.string()), M.FullPath, M.Base); CreateProcOptions Options; Options.StdinPipe = &P->StdinPipe; Options.StdoutPipe = &P->StdoutPipe; CreateProcResult Handle = CreateProc(Executable, CommandLine, Options); if (Handle <= 0) { ZEN_WARN("atos: failed to launch for {} - `atos` should be on PATH on macOS", M.FullPath); P->StdinPipe.Close(); P->StdoutPipe.Close(); auto [Ins, _] = m_Processes.emplace(M.FullPath, std::move(P)); return Ins->second.get(); // Alive = false } std::error_code Ec; P->Process.Initialize(int(Handle), Ec); if (Ec) { ZEN_WARN("atos: ProcessHandle init failed for {}: {}", M.FullPath, Ec.message()); P->StdinPipe.Close(); P->StdoutPipe.Close(); auto [Ins, _] = m_Processes.emplace(M.FullPath, std::move(P)); return Ins->second.get(); // Alive = false } P->StdinPipe.CloseReadEnd(); P->StdoutPipe.CloseWriteEnd(); P->Alive = true; auto [Ins, _] = m_Processes.emplace(M.FullPath, std::move(P)); return Ins->second.get(); } bool AtosSymbolizerResolver::ReadLine(AtosProcess& P, std::string& Out) const { for (;;) { size_t NewlinePos = P.ReadBuffer.find('\n'); if (NewlinePos != std::string::npos) { Out.assign(P.ReadBuffer, 0, NewlinePos); P.ReadBuffer.erase(0, NewlinePos + 1); return true; } char Buffer[1024]; ssize_t BytesRead = ::read(P.StdoutPipe.ReadFd, Buffer, sizeof(Buffer)); if (BytesRead <= 0) { if (BytesRead < 0 && errno == EINTR) { continue; } return false; } P.ReadBuffer.append(Buffer, static_cast(BytesRead)); } } std::string AtosSymbolizerResolver::DoQuery(const Module& M, uint64_t Address) const { AtosProcess* P = EnsureProcessFor(M); if (P == nullptr || !P->Alive) { return {}; } std::string Line = fmt::format("0x{:X}\n", Address); const char* Ptr = Line.data(); size_t Remaining = Line.size(); while (Remaining > 0) { ssize_t N = ::write(P->StdinPipe.WriteFd, Ptr, Remaining); if (N <= 0) { if (N < 0 && errno == EINTR) { continue; } ZEN_WARN("atos: write failed for {}, disabling", M.FullPath); P->Alive = false; return {}; } Ptr += N; Remaining -= static_cast(N); } std::string Reply; if (!ReadLine(*P, Reply) || Reply.empty()) { return {}; } // Parse "Function (in Binary) (file.cpp:NN)" or "... + 0xNN" or just "0xADDR". // Extract everything before " (in " as the function name. size_t InPos = Reply.find(" (in "); if (InPos == std::string::npos) { // No match — either raw "0xADDR" (no info) or an error message. Skip. return {}; } std::string_view Function(Reply.data(), InPos); // Look for a trailing "(file:line)" after the "(in ...)" block. std::string_view LocationView; size_t AfterIn = Reply.find(')', InPos); if (AfterIn != std::string::npos) { size_t OpenParen = Reply.find('(', AfterIn); if (OpenParen != std::string::npos) { size_t CloseParen = Reply.find(')', OpenParen); if (CloseParen != std::string::npos && CloseParen > OpenParen + 1) { LocationView = std::string_view(Reply).substr(OpenParen + 1, CloseParen - OpenParen - 1); } } } std::string Result(Function); if (!LocationView.empty()) { // atos gives us "file.cpp:NN" directly — no need to strip a directory. Result += fmt::format(" [{}]", LocationView); } return Result; } #endif // ZEN_PLATFORM_MAC ////////////////////////////////////////////////////////////////////////////// // Factory namespace { #if ZEN_PLATFORM_MAC // Probe PATH for a tool and return true if something usable was found. // SearchPathForExecutable returns the input unchanged if the tool can't be // found, so we compare against the filesystem to detect a hit. bool ToolIsOnPath(std::string_view Name) { std::filesystem::path Resolved = SearchPathForExecutable(Name); std::error_code Ec; return std::filesystem::exists(Resolved, Ec) && std::filesystem::is_regular_file(Resolved, Ec); } #endif SymbolBackend ResolveAutoBackend() { #if ZEN_PLATFORM_WINDOWS return SymbolBackend::Pdb; #elif ZEN_PLATFORM_MAC if (ToolIsOnPath("llvm-symbolizer")) { return SymbolBackend::LlvmSymbolizer; } return SymbolBackend::Atos; #else // Linux: llvm-symbolizer is the only backend we ship. return SymbolBackend::LlvmSymbolizer; #endif } } // namespace std::unique_ptr CreateSymbolResolver(SymbolBackend Backend) { if (Backend == SymbolBackend::Auto) { Backend = ResolveAutoBackend(); } if (Backend == SymbolBackend::Off) { return std::make_unique(); } if (Backend == SymbolBackend::LlvmSymbolizer) { return std::make_unique(); } #if ZEN_PLATFORM_MAC if (Backend == SymbolBackend::Atos) { return std::make_unique(); } #else if (Backend == SymbolBackend::Atos) { ZEN_WARN("atos backend is macOS-only; falling back to llvm-symbolizer"); return std::make_unique(); } #endif #if ZEN_PLATFORM_WINDOWS if (Backend == SymbolBackend::DbgHelp) { return std::make_unique(); } return std::make_unique(); #else // Pdb / DbgHelp aren't available on non-Windows; any other request falls back to llvm-symbolizer. return std::make_unique(); #endif } SymbolBackend ParseSymbolBackend(std::string_view Name) { if (Name == "auto") { return SymbolBackend::Auto; } if (Name == "pdb") { return SymbolBackend::Pdb; } if (Name == "dbghelp") { return SymbolBackend::DbgHelp; } if (Name == "llvm" || Name == "llvm-symbolizer") { return SymbolBackend::LlvmSymbolizer; } if (Name == "atos") { return SymbolBackend::Atos; } if (Name == "off") { return SymbolBackend::Off; } return SymbolBackend::Off; } } // namespace zen::trace_detail