aboutsummaryrefslogtreecommitdiff
path: root/src/zen/trace/symbol_resolver.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zen/trace/symbol_resolver.cpp')
-rw-r--r--src/zen/trace/symbol_resolver.cpp1631
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