diff options
Diffstat (limited to 'src/zen/trace/symbol_resolver.cpp')
| -rw-r--r-- | src/zen/trace/symbol_resolver.cpp | 1631 |
1 files changed, 1631 insertions, 0 deletions
diff --git a/src/zen/trace/symbol_resolver.cpp b/src/zen/trace/symbol_resolver.cpp new file mode 100644 index 000000000..53374cd64 --- /dev/null +++ b/src/zen/trace/symbol_resolver.cpp @@ -0,0 +1,1631 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "symbol_resolver.h" + +#include <zencore/filesystem.h> +#include <zencore/fmtutils.h> +#include <zencore/logging.h> +#include <zencore/process.h> +#include <zencore/string.h> +#include <zenhttp/httpclient.h> + +#include <algorithm> +#include <filesystem> +#include <mutex> +#include <unordered_map> +#include <vector> + +#if !ZEN_PLATFORM_WINDOWS +# include <cerrno> +# include <unistd.h> +#endif + +#if ZEN_PLATFORM_WINDOWS + +ZEN_THIRD_PARTY_INCLUDES_START +# include <Foundation/PDB_PointerUtil.h> +# include <PDB.h> +# include <PDB_CoalescedMSFStream.h> +# include <PDB_DBIStream.h> +# include <PDB_ImageSectionStream.h> +# include <PDB_InfoStream.h> +# include <PDB_ModuleInfoStream.h> +# include <PDB_ModuleLineStream.h> +# include <PDB_ModuleSymbolStream.h> +# include <PDB_PublicSymbolStream.h> +# include <PDB_RawFile.h> +ZEN_THIRD_PARTY_INCLUDES_END + +# include <zencore/windows.h> + +ZEN_THIRD_PARTY_INCLUDES_START +# include <DbgHelp.h> +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: <GUID_no_dashes><Age_hex>, e.g. "A1B2C3...1". + std::string FormatImageIdKey(const eastl::vector<uint8_t>& 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*<cache>*<url>" or "symsrv*symsrv.dll*<cache>*<url>" or just a URL. + // Returns a list of server URLs to try. + const std::vector<std::string>& ParseSymbolPath() + { + static const std::vector<std::string> s_Servers = [] { + std::vector<std::string> 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<std::string>& 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<FunctionEntry> m_Functions; + std::vector<LineEntry> 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<PDB::ModuleInfoStream::Module> 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<PDB::CodeView::DBI::Record>(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<const PDB::CodeView::DBI::FileChecksumHeader*>( + 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<ModuleMapping> 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<HANDLE>(static_cast<uintptr_t>(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<SYMBOL_INFO*>(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: "<path-to-binary> 0x<relative-address>\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<Module> 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<uint64_t, std::string> 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<size_t>(BytesRead)); +#endif + } +} + +std::string +LlvmSymbolizerResolver::DoQuery(const Module& M, uint64_t RelAddress) const +{ + if (!EnsureProcess()) + { + return {}; + } + + // Write "<path> 0x<addr>\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<DWORD>(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<size_t>(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<absolute-address>\n" +// It replies: "Function (in Binary) (file.cpp:NN)\n" +// or "Function (in Binary) + 0x<disp>\n" (no debug info) +// or "0x<address>\n" (nothing known) +// +// Launched with: atos -o <binary> -l 0x<module-base> +// 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<Module> m_Modules; + + mutable std::mutex m_Mutex; + mutable std::unordered_map<std::string, std::unique_ptr<AtosProcess>> m_Processes; + mutable std::unordered_map<uint64_t, std::string> 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<AtosProcess>(); + + 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<size_t>(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<size_t>(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<SymbolResolver> +CreateSymbolResolver(SymbolBackend Backend) +{ + if (Backend == SymbolBackend::Auto) + { + Backend = ResolveAutoBackend(); + } + + if (Backend == SymbolBackend::Off) + { + return std::make_unique<NullSymbolResolver>(); + } + + if (Backend == SymbolBackend::LlvmSymbolizer) + { + return std::make_unique<LlvmSymbolizerResolver>(); + } + +#if ZEN_PLATFORM_MAC + if (Backend == SymbolBackend::Atos) + { + return std::make_unique<AtosSymbolizerResolver>(); + } +#else + if (Backend == SymbolBackend::Atos) + { + ZEN_WARN("atos backend is macOS-only; falling back to llvm-symbolizer"); + return std::make_unique<LlvmSymbolizerResolver>(); + } +#endif + +#if ZEN_PLATFORM_WINDOWS + if (Backend == SymbolBackend::DbgHelp) + { + return std::make_unique<DbgHelpSymbolResolver>(); + } + return std::make_unique<PdbSymbolResolver>(); +#else + // Pdb / DbgHelp aren't available on non-Windows; any other request falls back to llvm-symbolizer. + return std::make_unique<LlvmSymbolizerResolver>(); +#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 |