Resolve crash stack traces to PDB symbol names

- Add PdbParser::BuildAddressIndex() to build a sorted RVA->name table
  from public and module symbols before closing the PDB file
- Add PdbParser::FindNameByRVA() for O(log n) reverse lookup at crash time
- Update crash handler to resolve Minecraft.Client.exe addresses to
  decorated function names (e.g. Minecraft.Client.exe!?tick@Minecraft+0x30)
- Resolve the faulting address itself in the crash report header
This commit is contained in:
Jacobwasbeast
2026-03-06 19:26:32 -06:00
parent 17f3a03aa0
commit 7af64bf83c
5 changed files with 190 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
#include "CrashHandler.h"
#include "LogUtil.h"
#include "PdbParser.h"
#include <Windows.h>
#include <TlHelp32.h>
#include <cstdio>
@@ -7,6 +8,7 @@
#include <ctime>
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<DWORD64>(s_gameBase))
{
uint32_t rva = static_cast<uint32_t>(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<DWORD64>(er->ExceptionAddress);
char modPath[MAX_PATH] = {0};
DWORD64 modBase = 0;
GetModuleForAddr(reinterpret_cast<DWORD64>(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<DWORD64>(er->ExceptionAddress) - modBase);
modPath, modBase, faultAddr - modBase);
char symName[512] = {0};
uint32_t symOff = 0;
if (s_gameBase != 0 && modBase == static_cast<DWORD64>(s_gameBase))
{
uint32_t rva = static_cast<uint32_t>(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

View File

@@ -1,8 +1,12 @@
#pragma once
#include <Windows.h>
#include <cstdint>
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);
}

View File

@@ -2,6 +2,9 @@
#include "LogUtil.h"
#include <Windows.h>
#include <cstring>
#include <vector>
#include <string>
#include <algorithm>
#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<SymEntry> 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<PDB::HashRecord> 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<PDB::ModuleInfoStream::Module> 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

View File

@@ -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();
}

View File

@@ -3,6 +3,7 @@
#include <string>
#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<uintptr_t>(GetModuleHandleA(nullptr)));
symbols.Cleanup();
if (!DotNetHost::Initialize())