From 9118ce668283abb25666fec43bf40b92dccb47fc Mon Sep 17 00:00:00 2001 From: Jacobwasbeast Date: Mon, 9 Mar 2026 20:58:15 -0500 Subject: [PATCH] runtime: add launcher-driven extensive symbol scan mode --- WeaveLoader.Launcher/Injector.cs | 7 +- WeaveLoader.Launcher/Program.cs | 22 +- WeaveLoaderRuntime/src/PdbParser.cpp | 309 ++++++++++++++++++++++ WeaveLoaderRuntime/src/PdbParser.h | 6 + WeaveLoaderRuntime/src/SymbolResolver.cpp | 55 ++++ 5 files changed, 393 insertions(+), 6 deletions(-) diff --git a/WeaveLoader.Launcher/Injector.cs b/WeaveLoader.Launcher/Injector.cs index 24993f7..5239a11 100644 --- a/WeaveLoader.Launcher/Injector.cs +++ b/WeaveLoader.Launcher/Injector.cs @@ -16,15 +16,18 @@ public static class Injector IntPtr ThreadHandle, int ProcessId); - public static InjectedProcess LaunchSuspended(string exePath, string? workingDir = null) + public static InjectedProcess LaunchSuspended(string exePath, string? workingDir = null, string? extraArgs = null) { workingDir ??= Path.GetDirectoryName(exePath); + string commandLine = $"\"{exePath}\""; + if (!string.IsNullOrWhiteSpace(extraArgs)) + commandLine += " " + extraArgs; var si = new STARTUPINFO { cb = Marshal.SizeOf() }; bool success = CreateProcess( exePath, - null, + commandLine, IntPtr.Zero, IntPtr.Zero, false, diff --git a/WeaveLoader.Launcher/Program.cs b/WeaveLoader.Launcher/Program.cs index 538b2e9..5445852 100644 --- a/WeaveLoader.Launcher/Program.cs +++ b/WeaveLoader.Launcher/Program.cs @@ -24,11 +24,21 @@ class Program try { var config = Config.Load(configFile); + bool extensiveSymbolScan = false; - if (args.Length > 0 && File.Exists(args[0])) + foreach (string arg in args) { - config.GameExePath = args[0]; - config.Save(configFile); + if (arg == "--extensive-symbol-scan") + { + extensiveSymbolScan = true; + continue; + } + + if (File.Exists(arg)) + { + config.GameExePath = arg; + config.Save(configFile); + } } if (string.IsNullOrEmpty(config.GameExePath) || !File.Exists(config.GameExePath)) @@ -72,8 +82,12 @@ class Program int modCount = Directory.GetFiles(modsDir, "*.dll").Length; Console.WriteLine($"Found {modCount} mod(s) in mods/"); Console.WriteLine($"Launching {Path.GetFileName(config.GameExePath)}..."); + if (extensiveSymbolScan) + Console.WriteLine("[..] Extensive symbol scan mode enabled"); - var process = Injector.LaunchSuspended(config.GameExePath); + var process = Injector.LaunchSuspended( + config.GameExePath, + extraArgs: extensiveSymbolScan ? "--extensive-symbol-scan" : null); Console.WriteLine($"[OK] Game process created (PID: {process.ProcessId})"); Console.WriteLine($"[..] Injecting {RuntimeDllName}..."); diff --git a/WeaveLoaderRuntime/src/PdbParser.cpp b/WeaveLoaderRuntime/src/PdbParser.cpp index d6b0aaa..2bebdc4 100644 --- a/WeaveLoaderRuntime/src/PdbParser.cpp +++ b/WeaveLoaderRuntime/src/PdbParser.cpp @@ -1,7 +1,9 @@ #include "PdbParser.h" #include "LogUtil.h" #include +#include #include +#include #include #include #include @@ -19,6 +21,13 @@ #include "PDB_Util.h" #include "Foundation/PDB_BitUtil.h" +#ifdef min +#undef min +#endif +#ifdef max +#undef max +#endif + struct SymEntry { uint32_t rva; @@ -46,6 +55,13 @@ static PDB::GlobalSymbolStream* s_globalStream = nullptr; static PDB::ModuleInfoStream* s_moduleStream = nullptr; static PDB::CoalescedMSFStream* s_symbolRecords = nullptr; +struct SimilarMatch +{ + std::string name; + uint32_t rva; + int score; +}; + static void CloseMappedFile(MappedFile& mf) { if (mf.baseAddress) { UnmapViewOfFile(mf.baseAddress); mf.baseAddress = nullptr; } @@ -208,6 +224,167 @@ static uint32_t ResolveProcRefRVA(uint16_t moduleIndex, uint32_t symbolOffset) return 0; } +static void AddUniquePattern(std::vector& patterns, const std::string& pattern) +{ + if (pattern.empty()) + return; + + for (const std::string& existing : patterns) + { + if (existing == pattern) + return; + } + + patterns.push_back(pattern); +} + +static std::string ToLowerCopy(const char* text) +{ + std::string out = text ? text : ""; + std::transform(out.begin(), out.end(), out.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return out; +} + +static size_t CommonPrefixLength(const std::string& lhs, const std::string& rhs) +{ + const size_t limit = std::min(lhs.size(), rhs.size()); + size_t n = 0; + while (n < limit && lhs[n] == rhs[n]) + ++n; + return n; +} + +static size_t GreedySubsequenceMatches(const std::string& needle, const std::string& haystack) +{ + size_t matched = 0; + size_t j = 0; + for (char c : needle) + { + while (j < haystack.size() && haystack[j] != c) + ++j; + if (j >= haystack.size()) + break; + ++matched; + ++j; + } + return matched; +} + +static size_t LevenshteinDistance(const std::string& lhs, const std::string& rhs) +{ + if (lhs.empty()) return rhs.size(); + if (rhs.empty()) return lhs.size(); + + std::vector prev(rhs.size() + 1); + std::vector curr(rhs.size() + 1); + for (size_t j = 0; j <= rhs.size(); ++j) + prev[j] = j; + + for (size_t i = 0; i < lhs.size(); ++i) + { + curr[0] = i + 1; + for (size_t j = 0; j < rhs.size(); ++j) + { + const size_t cost = (lhs[i] == rhs[j]) ? 0u : 1u; + curr[j + 1] = std::min({ prev[j + 1] + 1u, curr[j] + 1u, prev[j] + cost }); + } + prev.swap(curr); + } + + return prev[rhs.size()]; +} + +static int ScoreSimilarName(const std::string& missing, const std::string& candidate) +{ + if (candidate.empty()) + return -1000000; + + const size_t prefix = CommonPrefixLength(missing, candidate); + const size_t subseq = GreedySubsequenceMatches(missing, candidate); + const size_t distance = LevenshteinDistance(missing, candidate); + const bool contains = candidate.find(missing) != std::string::npos + || missing.find(candidate) != std::string::npos; + + int score = 0; + if (contains) score += 200; + score += static_cast(prefix * 8); + score += static_cast(subseq * 3); + score -= static_cast(distance * 4); + score -= static_cast((missing.size() > candidate.size()) + ? (missing.size() - candidate.size()) + : (candidate.size() - missing.size())); + return score; +} + +static void AddOrUpdateSimilar(std::vector& out, const char* name, uint32_t rva, int score) +{ + if (!name || !name[0]) + return; + + for (SimilarMatch& entry : out) + { + if (entry.name == name) + { + if (score > entry.score) + { + entry.score = score; + entry.rva = rva; + } + return; + } + } + + out.push_back({ name, rva, score }); +} + +static void ExtractExactNamePatterns(const char* missingName, std::vector& patterns) +{ + const std::string name(missingName ? missingName : ""); + if (name.empty()) + return; + + AddUniquePattern(patterns, name); + + const size_t scopePos = name.rfind("::"); + if (scopePos != std::string::npos) + { + AddUniquePattern(patterns, name.substr(scopePos + 2)); + AddUniquePattern(patterns, name.substr(0, scopePos)); + } +} + +static void ExtractDecoratedNamePatterns(const char* missingName, std::vector& patterns) +{ + const std::string name(missingName ? missingName : ""); + if (name.empty()) + return; + + size_t start = 0; + while (start < name.size() && name[start] == '?') + ++start; + + size_t end = name.find('@', start); + if (end != std::string::npos && end > start) + AddUniquePattern(patterns, name.substr(start, end - start)); + + size_t typeStart = end; + while (typeStart != std::string::npos && typeStart + 1 < name.size()) + { + ++typeStart; + size_t typeEnd = name.find('@', typeStart); + if (typeEnd == std::string::npos || typeEnd == typeStart) + break; + + const std::string token = name.substr(typeStart, typeEnd - typeStart); + if (token != "std" && token != "_W" && token != "_N" && token != "PEAV" && token != "QEAA") + AddUniquePattern(patterns, token); + + typeStart = typeEnd; + } +} + namespace PdbParser { @@ -613,6 +790,138 @@ void DumpMatching(const char* substring) LogUtil::Log("[WeaveLoader] PdbParser: found %d matching symbols", count); } +void DumpSimilar(const char* missingName) +{ +#ifndef WEAVELOADER_DEBUG_BUILD + (void)missingName; +#else + if (!missingName || !missingName[0]) + return; + + LogUtil::Log("[WeaveLoader] PdbParser: symbol '%s' is missing. Run WeaveLoader.exe --extensive-symbol-scan for a full similarity dump.", missingName); +#endif +} + +void DumpSimilarFull(const char* missingName, const char* logPath, size_t maxResults) +{ + if (!s_open || !missingName || !missingName[0] || !logPath || !logPath[0]) + return; + + std::ofstream out(logPath, std::ios::out | std::ios::app); + if (!out.is_open()) + { + LogUtil::Log("[WeaveLoader] PdbParser: failed to open full similarity log '%s'", logPath); + return; + } + + out << "[WeaveLoader] Full similar symbol dump\n"; + out << "[WeaveLoader] Missing symbol: " << missingName << "\n"; + + std::vector matches; + matches.reserve(4096); + const std::string missingLower = ToLowerCopy(missingName); + + { + const PDB::ArrayView records = s_publicStream->GetRecords(); + for (const PDB::HashRecord& hashRecord : records) + { + const PDB::CodeView::DBI::Record* record = s_publicStream->GetRecord(*s_symbolRecords, hashRecord); + if (record->header.kind != PDB::CodeView::DBI::SymbolRecordKind::S_PUB32) + continue; + + const char* name = record->data.S_PUB32.name; + const int score = ScoreSimilarName(missingLower, ToLowerCopy(name)); + if (score <= 0) + continue; + + const uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA( + record->data.S_PUB32.section, record->data.S_PUB32.offset); + AddOrUpdateSimilar(matches, name, rva, score); + } + } + + { + const PDB::ArrayView records = s_globalStream->GetRecords(); + for (const PDB::HashRecord& hashRecord : records) + { + const PDB::CodeView::DBI::Record* record = s_globalStream->GetRecord(*s_symbolRecords, hashRecord); + uint16_t section = 0; + uint32_t offset = 0; + const char* name = GetGlobalSymName(record, section, offset); + if (!name) + continue; + + const int score = ScoreSimilarName(missingLower, ToLowerCopy(name)); + if (score <= 0) + continue; + + const uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset); + AddOrUpdateSimilar(matches, name, rva, score); + } + } + + { + const PDB::ArrayView modules = s_moduleStream->GetModules(); + for (const PDB::ModuleInfoStream::Module& mod : modules) + { + if (!mod.HasSymbolStream()) + continue; + + const PDB::ModuleSymbolStream modSymStream = mod.CreateSymbolStream(*s_rawFile); + modSymStream.ForEachSymbol([&](const PDB::CodeView::DBI::Record* record) + { + const char* name = nullptr; + uint16_t section = 0; + uint32_t offset = 0; + + switch (record->header.kind) + { + case PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32: + name = record->data.S_LPROC32.name; section = record->data.S_LPROC32.section; offset = record->data.S_LPROC32.offset; break; + case PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32: + name = record->data.S_GPROC32.name; section = record->data.S_GPROC32.section; offset = record->data.S_GPROC32.offset; break; + case PDB::CodeView::DBI::SymbolRecordKind::S_LPROC32_ID: + name = record->data.S_LPROC32_ID.name; section = record->data.S_LPROC32_ID.section; offset = record->data.S_LPROC32_ID.offset; break; + case PDB::CodeView::DBI::SymbolRecordKind::S_GPROC32_ID: + name = record->data.S_GPROC32_ID.name; section = record->data.S_GPROC32_ID.section; offset = record->data.S_GPROC32_ID.offset; break; + case PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32: + name = record->data.S_LDATA32.name; section = record->data.S_LDATA32.section; offset = record->data.S_LDATA32.offset; break; + case PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32: + name = record->data.S_GDATA32.name; section = record->data.S_GDATA32.section; offset = record->data.S_GDATA32.offset; break; + default: + return; + } + + const int score = ScoreSimilarName(missingLower, ToLowerCopy(name)); + if (score <= 0) + return; + + const uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset); + AddOrUpdateSimilar(matches, name, rva, score); + }); + } + } + + std::sort(matches.begin(), matches.end(), [](const SimilarMatch& a, const SimilarMatch& b) { + if (a.score != b.score) return a.score > b.score; + if (a.rva != b.rva) return a.rva < b.rva; + return a.name < b.name; + }); + + out << "[WeaveLoader] Similar matches: " << matches.size() << "\n"; + const size_t count = std::min(maxResults, matches.size()); + for (size_t i = 0; i < count; ++i) + { + char hexBuf[16]; + std::snprintf(hexBuf, sizeof(hexBuf), "%08X", matches[i].rva); + out << "[SIM score=" << matches[i].score << "] rva=0x" << hexBuf << " " << matches[i].name << "\n"; + } + out << "\n"; + out.flush(); + + LogUtil::Log("[WeaveLoader] PdbParser: wrote full similarity dump for '%s' to %s", missingName, logPath); +} + void BuildAddressIndex() { if (!s_open) return; diff --git a/WeaveLoaderRuntime/src/PdbParser.h b/WeaveLoaderRuntime/src/PdbParser.h index 1c9e560..460e4d5 100644 --- a/WeaveLoaderRuntime/src/PdbParser.h +++ b/WeaveLoaderRuntime/src/PdbParser.h @@ -15,6 +15,12 @@ namespace PdbParser // Logs all symbols whose name contains the given substring (for debugging). void DumpMatching(const char* substring); + // Logs a short list of likely DumpMatching() patterns for a missing symbol. + void DumpSimilar(const char* missingName); + + // Writes a ranked full similarity dump to a file for a missing symbol. + void DumpSimilarFull(const char* missingName, const char* logPath, size_t maxResults = 200); + // Builds a sorted index of all symbols for reverse RVA->name lookup. // Must be called while PDB is open. The index survives Close(). void BuildAddressIndex(); diff --git a/WeaveLoaderRuntime/src/SymbolResolver.cpp b/WeaveLoaderRuntime/src/SymbolResolver.cpp index 021aa68..0a419cc 100644 --- a/WeaveLoaderRuntime/src/SymbolResolver.cpp +++ b/WeaveLoaderRuntime/src/SymbolResolver.cpp @@ -1,9 +1,55 @@ #include "SymbolResolver.h" #include "PdbParser.h" #include "LogUtil.h" +#include #include #include #include +#include + +static bool HasExtensiveSymbolScanArg() +{ + const char* cmdLine = GetCommandLineA(); + return cmdLine && std::strstr(cmdLine, "--extensive-symbol-scan") != nullptr; +} + +static std::vector g_pendingExtensiveSymbolScans; + +static void QueueExtensiveSymbolScan(const char* missingName) +{ + if (!HasExtensiveSymbolScanArg() || !missingName || !missingName[0]) + return; + + for (const std::string& existing : g_pendingExtensiveSymbolScans) + { + if (existing == missingName) + return; + } + + g_pendingExtensiveSymbolScans.push_back(missingName); +} + +static void RunQueuedExtensiveSymbolScanAndExit() +{ + if (!HasExtensiveSymbolScanArg()) + return; + + if (g_pendingExtensiveSymbolScans.empty()) + { + LogUtil::Log("[WeaveLoader] Extensive symbol scan requested, but no unresolved symbols were queued"); + return; + } + + const char* logsDir = LogUtil::GetLogsDir(); + std::string logPath = (logsDir && logsDir[0]) + ? std::string(logsDir) + "similar_dumped_addresses.log" + : "similar_dumped_addresses.log"; + LogUtil::Log("[WeaveLoader] Extensive symbol scan requested for %zu missing symbol(s)", g_pendingExtensiveSymbolScans.size()); + for (const std::string& missingName : g_pendingExtensiveSymbolScans) + PdbParser::DumpSimilarFull(missingName.c_str(), logPath.c_str()); + LogUtil::Log("[WeaveLoader] Extensive symbol scan complete, terminating process"); + ExitProcess(0); +} static const char* SYM_RUN_STATIC_CTORS = "?MinecraftWorld_RunStaticCtors@@YAXXZ"; static const char* SYM_MINECRAFT_TICK = "?tick@Minecraft@@QEAAX_N0@Z"; @@ -98,7 +144,12 @@ static void* ResolveExactProcName(uintptr_t moduleBase, const char* exactName) { uint32_t rva = PdbParser::FindSymbolRVAByName(exactName); if (rva == 0) + { + LogUtil::Log("[WeaveLoader] Exact symbol not found in PDB: '%s'", exactName); + QueueExtensiveSymbolScan(exactName); + PdbParser::DumpSimilar(exactName); return nullptr; + } return reinterpret_cast(moduleBase + rva); } @@ -147,6 +198,8 @@ void* SymbolResolver::Resolve(const char* decoratedName) if (rva == 0) { LogUtil::Log("[WeaveLoader] Symbol not found in PDB: '%s'", decoratedName); + QueueExtensiveSymbolScan(decoratedName); + PdbParser::DumpSimilar(decoratedName); return nullptr; } @@ -389,6 +442,8 @@ bool SymbolResolver::ResolveGameFunctions() else LogUtil::Log("[WeaveLoader] CRITICAL symbols missing - hooks will not be installed"); + RunQueuedExtensiveSymbolScanAndExit(); + return ok; }