diff --git a/LegacyForgeRuntime/src/CrashHandler.cpp b/LegacyForgeRuntime/src/CrashHandler.cpp index 4a959f4..17c05db 100644 --- a/LegacyForgeRuntime/src/CrashHandler.cpp +++ b/LegacyForgeRuntime/src/CrashHandler.cpp @@ -1,5 +1,6 @@ #include "CrashHandler.h" #include "LogUtil.h" +#include "PdbParser.h" #include #include #include @@ -7,6 +8,7 @@ #include static HMODULE s_runtimeModule = nullptr; +static uintptr_t s_gameBase = 0; static volatile LONG s_handling = 0; static const char* ExceptionCodeToString(DWORD code) @@ -102,10 +104,30 @@ static void WalkStack(CONTEXT* ctx) modName = slash ? slash + 1 : modPath; } - LogUtil::LogCrash(" [%2d] 0x%016llX %s+0x%llX", frame, rip, modName, rip - modBase); + char symName[512] = {0}; + uint32_t symOff = 0; + if (s_gameBase != 0 && modBase == static_cast(s_gameBase)) + { + uint32_t rva = static_cast(rip - modBase); + if (PdbParser::FindNameByRVA(rva, symName, sizeof(symName), &symOff)) + { + LogUtil::LogCrash(" [%2d] 0x%016llX %s!%s+0x%X", + frame, rip, modName, symName, symOff); + } + else + { + LogUtil::LogCrash(" [%2d] 0x%016llX %s+0x%llX", + frame, rip, modName, rip - modBase); + } + } + else + { + LogUtil::LogCrash(" [%2d] 0x%016llX %s+0x%llX", + frame, rip, modName, rip - modBase); + } + frame++; - // Use RtlLookupFunctionEntry + RtlVirtualUnwind for x64 stack walking DWORD64 imageBase = 0; PRUNTIME_FUNCTION pFunc = RtlLookupFunctionEntry(rip, &imageBase, nullptr); if (!pFunc) @@ -162,14 +184,24 @@ static LONG WINAPI VectoredHandler(EXCEPTION_POINTERS* ep) LogUtil::LogCrash("Fault: %s of address 0x%016llX", op, er->ExceptionInformation[1]); } - // Module containing the faulting address + // Module containing the faulting address + PDB symbol resolution { + DWORD64 faultAddr = reinterpret_cast(er->ExceptionAddress); char modPath[MAX_PATH] = {0}; DWORD64 modBase = 0; - GetModuleForAddr(reinterpret_cast(er->ExceptionAddress), modPath, sizeof(modPath), &modBase); + GetModuleForAddr(faultAddr, modPath, sizeof(modPath), &modBase); if (modPath[0]) LogUtil::LogCrash("Module: %s (base: 0x%016llX, offset: +0x%llX)", - modPath, modBase, reinterpret_cast(er->ExceptionAddress) - modBase); + modPath, modBase, faultAddr - modBase); + + char symName[512] = {0}; + uint32_t symOff = 0; + if (s_gameBase != 0 && modBase == static_cast(s_gameBase)) + { + uint32_t rva = static_cast(faultAddr - modBase); + if (PdbParser::FindNameByRVA(rva, symName, sizeof(symName), &symOff)) + LogUtil::LogCrash("Symbol: %s+0x%X", symName, symOff); + } } LogUtil::LogCrash(""); @@ -228,4 +260,10 @@ void Install(HMODULE runtimeModule) AddVectoredExceptionHandler(1, VectoredHandler); } +void SetGameBase(uintptr_t base) +{ + s_gameBase = base; + LogUtil::Log("[LegacyForge] Crash handler: game base set to 0x%016llX", (DWORD64)base); +} + } // namespace CrashHandler diff --git a/LegacyForgeRuntime/src/CrashHandler.h b/LegacyForgeRuntime/src/CrashHandler.h index e51068a..80bef50 100644 --- a/LegacyForgeRuntime/src/CrashHandler.h +++ b/LegacyForgeRuntime/src/CrashHandler.h @@ -1,8 +1,12 @@ #pragma once #include +#include namespace CrashHandler { // Installs the vectored exception handler. Safe to call from DllMain. void Install(HMODULE runtimeModule); + + // Store the game exe's base address so we can compute RVAs for PDB lookups. + void SetGameBase(uintptr_t base); } diff --git a/LegacyForgeRuntime/src/PdbParser.cpp b/LegacyForgeRuntime/src/PdbParser.cpp index b1e7b03..c0aefe5 100644 --- a/LegacyForgeRuntime/src/PdbParser.cpp +++ b/LegacyForgeRuntime/src/PdbParser.cpp @@ -2,6 +2,9 @@ #include "LogUtil.h" #include #include +#include +#include +#include #include "PDB.h" #include "PDB_RawFile.h" @@ -14,6 +17,14 @@ #include "PDB_ModuleInfoStream.h" #include "PDB_ModuleSymbolStream.h" +struct SymEntry +{ + uint32_t rva; + std::string name; +}; + +static std::vector s_addrIndex; + struct MappedFile { HANDLE hFile = INVALID_HANDLE_VALUE; @@ -361,6 +372,121 @@ void DumpMatching(const char* substring) LogUtil::Log("[LegacyForge] PdbParser: found %d matching symbols", count); } +void BuildAddressIndex() +{ + if (!s_open) return; + + s_addrIndex.clear(); + + // Collect all public symbols (S_PUB32) -- these cover exported and + // non-static functions/data with their decorated names. + { + const PDB::ArrayView records = s_publicStream->GetRecords(); + s_addrIndex.reserve(records.GetLength()); + 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; + + uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA( + record->data.S_PUB32.section, record->data.S_PUB32.offset); + if (rva != 0) + s_addrIndex.push_back({ rva, record->data.S_PUB32.name }); + } + } + + // Also pull in per-module procedure symbols (S_GPROC32/S_LPROC32) which + // include internal/static functions not in the public stream. + { + 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; + default: + return; + } + + if (!name) return; + uint32_t rva = s_sectionStream->ConvertSectionOffsetToRVA(section, offset); + if (rva != 0) + s_addrIndex.push_back({ rva, name }); + }); + } + } + + // Sort by RVA and deduplicate + std::sort(s_addrIndex.begin(), s_addrIndex.end(), + [](const SymEntry& a, const SymEntry& b) { return a.rva < b.rva; }); + + // Remove duplicates (same RVA), keeping the first entry + auto last = std::unique(s_addrIndex.begin(), s_addrIndex.end(), + [](const SymEntry& a, const SymEntry& b) { return a.rva == b.rva; }); + s_addrIndex.erase(last, s_addrIndex.end()); + + LogUtil::Log("[LegacyForge] PdbParser: built address index with %zu symbols", s_addrIndex.size()); +} + +bool FindNameByRVA(uint32_t rva, char* outName, size_t nameSize, uint32_t* outOffset) +{ + if (s_addrIndex.empty() || rva == 0) + return false; + + // Binary search for the largest RVA <= target + SymEntry key = { rva, {} }; + auto it = std::upper_bound(s_addrIndex.begin(), s_addrIndex.end(), key, + [](const SymEntry& a, const SymEntry& b) { return a.rva < b.rva; }); + + if (it == s_addrIndex.begin()) + return false; + + --it; + + // Sanity: don't report symbols more than 1MB away + if (rva - it->rva > 0x100000) + return false; + + if (outName && nameSize > 0) + { + strncpy(outName, it->name.c_str(), nameSize - 1); + outName[nameSize - 1] = '\0'; + } + if (outOffset) + *outOffset = rva - it->rva; + + return true; +} + void Close() { delete s_moduleStream; s_moduleStream = nullptr; @@ -372,6 +498,8 @@ void Close() delete s_rawFile; s_rawFile = nullptr; CloseMappedFile(s_mapped); s_open = false; + // Note: s_addrIndex intentionally NOT cleared -- it survives Close() + // so the crash handler can resolve addresses after PDB is released. } } // namespace PdbParser diff --git a/LegacyForgeRuntime/src/PdbParser.h b/LegacyForgeRuntime/src/PdbParser.h index 074a0b1..a3901ef 100644 --- a/LegacyForgeRuntime/src/PdbParser.h +++ b/LegacyForgeRuntime/src/PdbParser.h @@ -11,5 +11,14 @@ namespace PdbParser // Logs all symbols whose name contains the given substring (for debugging). void DumpMatching(const char* substring); + // 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(); + + // Reverse lookup: given an RVA, find the nearest symbol at or before it. + // Returns true if found. outName receives the symbol name, outOffset + // the byte distance from the symbol's start address. + bool FindNameByRVA(uint32_t rva, char* outName, size_t nameSize, uint32_t* outOffset); + void Close(); } diff --git a/LegacyForgeRuntime/src/dllmain.cpp b/LegacyForgeRuntime/src/dllmain.cpp index 5181dd8..1a1bb71 100644 --- a/LegacyForgeRuntime/src/dllmain.cpp +++ b/LegacyForgeRuntime/src/dllmain.cpp @@ -3,6 +3,7 @@ #include #include "LogUtil.h" #include "CrashHandler.h" +#include "PdbParser.h" #include "SymbolResolver.h" #include "HookManager.h" #include "DotNetHost.h" @@ -59,6 +60,11 @@ DWORD WINAPI InitThread(LPVOID lpParam) } LogUtil::Log("[LegacyForge] Hooks installed"); + // Build the RVA->name index before releasing the PDB. + // This index survives PdbParser::Close() and is used by the crash handler. + PdbParser::BuildAddressIndex(); + CrashHandler::SetGameBase(reinterpret_cast(GetModuleHandleA(nullptr))); + symbols.Cleanup(); if (!DotNetHost::Initialize())