Per-module symbol save/load, module identity via crc, GetModuleIndex fix

SymbolMap:
- Fix GetModuleIndex(): it only checked the end of an active module's range
  (via activeModuleEnds.upper_bound), never the start, so an address sitting
  in the gap before a module was silently misattributed to it. Added
  GetModuleIndexByName() as a companion lookup.
- AddModule() gains an optional crc param, stored per ModuleEntry. Reactivating
  a module by name now also requires the crc to agree when both sides know it,
  so two unrelated binaries that happen to share a name no longer get merged
  into one symbol table (addresses the old TODO at the top of SymbolMap.h).
- AddLabel()/AddFunction() gain an updateName param (default false, preserving
  existing "first writer wins" behavior) so a trusted source - like a loaded
  symbol file - can be allowed to overwrite a name that a lower-confidence
  automatic pass already assigned.
- New SaveModuleSymbols()/LoadModuleSymbols()/GetModuleSymbolsPath(): save or
  restore one module's functions/data/labels to/from a small human-editable
  text file, addressed relative to the module (so the file stays valid however
  the module ends up positioned on a later run). Keyed by
  PSP/SYSTEM/SYMBOLS/<moduleName>_<crc>.ppsym - deliberately by module+crc
  rather than by game, so it's shared by every game/homebrew that loads the
  exact same module. A "# game <id> <title>" comment records who last saved
  it, informational only.

WebSocket debugger: hle.module.saveSymbols/loadSymbols expose the above.

sceKernelModule.cpp: auto-load a module's saved symbols right after it's
registered with the symbol map (both the real ELF-load path and the
savestate-load path), and auto-save on unload (before UnloadModule(), while
its symbols are still active) - gated behind the new bAutoSaveLoadSymbols
config setting (default off), with a matching Developer Tools checkbox and
a --auto-save-load-symbols command-line override for headless use.

Includes some in-progress cleanup already staged: DescribeAddress now calls
g_symbolMap->GetDescription() directly instead of through the now-removed
MIPSDebugInterface::getDescription() wrapper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This commit is contained in:
Henrik Rydgård
2026-08-17 16:01:23 +02:00
co-authored by Claude Sonnet 5
parent 2a1df9f0b8
commit 29a38af37e
14 changed files with 458 additions and 40 deletions
+6
View File
@@ -193,6 +193,7 @@ static const CommandLineParam g_autoParams[] = {
{POFF(generateInterpreterDispatch), CmdParamType::Bool, "generate-interpreter-dispatch", '\0', "Generate C++ interpreter dispatch code (ExecInstruction) to stdout and exit", CmdLineMode::Headless},
{POFF(resolutionScale), CmdParamType::Int, "resolution-scale", '\0', "Set the resolution scale factor"},
{POFF(debuggerPort), CmdParamType::Int, "debugger", '\0', "Enable the WebSocket debugger on this port (0 = pick automatically); see docs/WebSocketDebugger.md"},
{POFF(autoSaveLoadSymbols), CmdParamType::Bool, "auto-save-load-symbols", '\0', "Auto save/load per-module symbol files (see bAutoSaveLoadSymbols)", CmdLineMode::Both},
{POFF(bootVSH), CmdParamType::Bool, "vsh", '\0', "Boot the VSH (requires files dumped from a PSP in the flash0 directory)"},
{POFF(memReadAction), CmdParamType::Enum, "memread", '\0', "Set the action for memory read exceptions", CmdLineMode::Both, g_ExceptionActionValues, ARRAY_SIZE(g_ExceptionActionValues)},
{POFF(memWriteAction), CmdParamType::Enum, "memwrite", '\0', "Set the action for memory write exceptions", CmdLineMode::Both, g_ExceptionActionValues, ARRAY_SIZE(g_ExceptionActionValues)},
@@ -517,6 +518,11 @@ void CommandLineOptions::ApplyToConfig() const {
WebServerSetRequireExactPort(debuggerPort.value() != 0);
}
if (autoSaveLoadSymbols.has_value()) {
g_Config.bAutoSaveLoadSymbols = autoSaveLoadSymbols.value();
g_Config.DoNotSaveSetting(&g_Config.bAutoSaveLoadSymbols);
}
if (logLevel.has_value()) {
g_logManager.SetAllLogLevels(logLevel.value());
}
+5
View File
@@ -47,6 +47,11 @@ struct CommandLineOptions {
// Also breaks the CPU at start in the headless build. See docs/WebSocketDebugger.md.
std::optional<int> debuggerPort;
// Overrides g_Config.bAutoSaveLoadSymbols for this run only (see SymbolMap::SaveModuleSymbols/
// LoadModuleSymbols and Core/HLE/sceKernelModule.cpp) - handy for headless runs that want
// symbol names without persisting the setting via Settings > Developer Tools.
std::optional<bool> autoSaveLoadSymbols;
// Attempts to boot the vsh, which will only work if the correct files are present in the flash
// and once we've fixed all the bugs. This is just here to allow testing.
std::optional<bool> bootVSH;
+1
View File
@@ -1105,6 +1105,7 @@ static const ConfigSetting debuggerSettings[] = {
ConfigSetting("FuncHashMap", SETTING(g_Config, bFuncHashMap), false, CfgFlag::DEFAULT),
ConfigSetting("SkipFuncHashMap", SETTING(g_Config, sSkipFuncHashMap), "", CfgFlag::DEFAULT),
ConfigSetting("MemInfoDetailed", SETTING(g_Config, bDebugMemInfoDetailed), false, CfgFlag::DEFAULT),
ConfigSetting("AutoSaveLoadSymbols", SETTING(g_Config, bAutoSaveLoadSymbols), false, CfgFlag::DEFAULT),
};
static const ConfigSetting jitSettings[] = {
+4
View File
@@ -658,6 +658,10 @@ public:
bool bFuncHashMap;
std::string sSkipFuncHashMap;
bool bDebugMemInfoDetailed;
// Auto-save a loaded module's symbols (name/CRC keyed, shared across games that load the
// same module) to PSP/SYSTEM/SYMBOLS on unload, and auto-load them back on module load.
// See SymbolMap::SaveModuleSymbols/LoadModuleSymbols and Core/HLE/sceKernelModule.cpp.
bool bAutoSaveLoadSymbols;
// Volatile development settings
// Overlays
+2 -1
View File
@@ -34,6 +34,7 @@
#include "Core/HLE/sceKernelModule.h" // for DescribeAddress
#include "Core/MIPS/MIPS.h"
#include "Common/StringUtils.h"
#include "Core/Debugger/SymbolMap.h"
class MemSlabMap {
public:
@@ -797,7 +798,7 @@ void DescribeAddress(const MIPSDebugInterface *mips, u32 address, char *buffer,
w.C("(invalid)");
}
} else if (DescribeModuleAddress(address, desc, sizeof(desc))) {
std::string temp = mips->getDescription(address);
std::string temp = g_symbolMap->GetDescription(address);
w.F("[%s]%s%s: %s", desc, kernel, uncached, temp.c_str());
} else if (Memory::IsVRAMAddress(address)) {
w.F("[VRAM]%s", uncached); // can't be kernel
+262 -26
View File
@@ -46,6 +46,7 @@
#include "Core/MemMap.h"
#include "Core/Config.h"
#include "Core/Debugger/SymbolMap.h"
#include "Core/Util/PathUtil.h"
SymbolMap *g_symbolMap;
@@ -372,6 +373,189 @@ bool SymbolMap::SaveNocashSym(const Path &filename) const {
return true;
}
static const char *DataTypeName(DataType type) {
switch (type) {
case DATATYPE_BYTE: return "byte";
case DATATYPE_HALFWORD: return "halfword";
case DATATYPE_WORD: return "word";
case DATATYPE_ASCII: return "ascii";
default: return "byte";
}
}
static bool DataTypeFromName(const char *s, DataType *out) {
if (!strcmp(s, "byte")) *out = DATATYPE_BYTE;
else if (!strcmp(s, "halfword")) *out = DATATYPE_HALFWORD;
else if (!strcmp(s, "word")) *out = DATATYPE_WORD;
else if (!strcmp(s, "ascii")) *out = DATATYPE_ASCII;
else return false;
return true;
}
// Returns a pointer to the start of the (count+1)th whitespace-separated token in line, or to
// the trailing '\0' if there aren't that many - used instead of sscanf's %s/%[^\n] for the
// trailing name field below, since scanf's "match one or more characters" conversions fail
// (rather than matching an empty string) when a data/label entry legitimately has no name,
// which would otherwise silently drop the whole line instead of just leaving the name blank.
static const char *SkipTokens(const char *line, int count) {
const char *p = line;
for (int i = 0; i < count; i++) {
while (*p == ' ' || *p == '\t') p++;
while (*p && *p != ' ' && *p != '\t') p++;
}
while (*p == ' ' || *p == '\t') p++;
return p;
}
Path SymbolMap::GetModuleSymbolsPath(const std::string &moduleName, u32 crc) {
// Module names come from untrusted game/homebrew data (the ELF's module-info string) and
// may contain characters that aren't safe as a filename - replace anything but the basics.
std::string safeName;
safeName.reserve(moduleName.size());
for (char c : moduleName) {
bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '.';
safeName += ok ? c : '_';
}
if (safeName.empty())
safeName = "module";
// Deliberately keyed by module name + crc, NOT by game - the exact same module (e.g. a
// kernel/driver module, or a homebrew's own statically-linked library) commonly gets loaded
// by many different games/homebrew, and symbols for it are equally valid for all of them.
return GetSysDirectory(DIRECTORY_SYSTEM) / "SYMBOLS" / StringFromFormat("%s_%08x.ppsym", safeName.c_str(), crc);
}
u32 SymbolMap::GetModuleCrc(int moduleIndex) const {
for (const auto &module : modules) {
if (module.index == moduleIndex)
return module.crc;
}
return 0;
}
// File format is a simple, human-editable text format - deliberately not the denser gzipped
// .map format LoadSymbolMap/SaveSymbolMap use, since these files are meant to be hand-tweaked
// (e.g. after manually naming a function) and diffed/version-controlled if the user wants to.
//
// .ppsym 1
// crc <hex, 0 if unknown>
// # game <gameID> <gameTitle> -- informational only, see SaveModuleSymbols
// F <relAddr hex> <size hex> <name> -- function
// D <relAddr hex> <size hex> <type> <name> -- data (type: byte/halfword/word/ascii)
// L <relAddr hex> <name> -- bare label (not a function or data start)
bool SymbolMap::SaveModuleSymbols(int moduleIndex, const Path &filename, const std::string &gameID, const std::string &gameTitle) const {
u32 crc = 0;
bool found = false;
for (const auto &module : modules) {
if (module.index == moduleIndex) {
crc = module.crc;
found = true;
break;
}
}
if (!found)
return false;
File::CreateFullPath(filename.NavigateUp());
FILE *f = File::OpenCFile(filename, "w");
if (!f)
return false;
fprintf(f, ".ppsym 1\n");
fprintf(f, "crc %08x\n", crc);
// This file may be shared between multiple games that all load this module - this comment
// just records who saved it most recently, purely for a human's benefit (e.g. to recognize
// where a set of names came from); it's never read back by LoadModuleSymbols.
std::string safeTitle = gameTitle;
std::replace(safeTitle.begin(), safeTitle.end(), '\n', ' ');
std::replace(safeTitle.begin(), safeTitle.end(), '\r', ' ');
fprintf(f, "# game %s %s\n", gameID.empty() ? "?" : gameID.c_str(), safeTitle.c_str());
for (const auto &[key, e] : functions) {
if (key.first != moduleIndex)
continue;
const char *name = GetLabelNameRel(e.start, moduleIndex);
fprintf(f, "F %08x %08x %s\n", e.start, e.size, name ? name : "");
}
for (const auto &[key, e] : data) {
if (key.first != moduleIndex)
continue;
const char *name = GetLabelNameRel(e.start, moduleIndex);
fprintf(f, "D %08x %08x %s %s\n", e.start, e.size, DataTypeName(e.type), name ? name : "");
}
for (const auto &[key, e] : labels) {
if (key.first != moduleIndex)
continue;
// Functions/data already saved their own (function/data-start) label above - only save
// the remainder here, labels that aren't at a function or data start.
if (functions.find(key) != functions.end() || data.find(key) != data.end())
continue;
fprintf(f, "L %08x %s\n", e.addr, e.name);
}
fclose(f);
return true;
}
bool SymbolMap::LoadModuleSymbols(int moduleIndex, const Path &filename) {
if (!IsModuleActive(moduleIndex))
return false;
FILE *f = File::OpenCFile(filename, "r");
if (!f)
return false;
u32 currentCrc = 0;
for (const auto &module : modules) {
if (module.index == moduleIndex) {
currentCrc = module.crc;
break;
}
}
char line[512];
while (fgets(line, sizeof(line), f)) {
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
line[--len] = '\0';
if (line[0] == '\0' || line[0] == '#' || !strncmp(line, ".ppsym", 6))
continue;
u32 addr, size;
char field[192];
if (!strncmp(line, "crc ", 4)) {
u32 fileCrc = 0;
if (sscanf(line + 4, "%x", &fileCrc) == 1 && currentCrc != 0 && fileCrc != 0 && fileCrc != currentCrc) {
WARN_LOG(Log::Loader, "LoadModuleSymbols: crc mismatch for '%s' (file %08x, loaded %08x) - symbols may not match this build of the module", filename.c_str(), fileCrc, currentCrc);
}
} else if (line[0] == 'F' && sscanf(line, "F %x %x", &addr, &size) == 2) {
const char *name = SkipTokens(line, 3);
std::string autoName;
if (!name[0]) {
autoName = StringFromFormat("z_un_%08x", addr);
name = autoName.c_str();
}
AddFunction(name, GetModuleAbsoluteAddr(addr, moduleIndex), size, moduleIndex, true);
} else if (line[0] == 'D' && sscanf(line, "D %x %x %191s", &addr, &size, field) == 3) {
DataType type;
if (!DataTypeFromName(field, &type))
type = DATATYPE_BYTE;
u32 absAddr = GetModuleAbsoluteAddr(addr, moduleIndex);
AddData(absAddr, size, type, moduleIndex);
const char *name = SkipTokens(line, 4);
if (name[0])
AddLabel(name, absAddr, moduleIndex, true);
} else if (line[0] == 'L' && sscanf(line, "L %x", &addr) == 1) {
const char *name = SkipTokens(line, 2);
if (name[0])
AddLabel(name, GetModuleAbsoluteAddr(addr, moduleIndex), moduleIndex, true);
}
}
fclose(f);
SortSymbols();
return true;
}
SymbolType SymbolMap::GetSymbolType(u32 address) {
if (activeNeedUpdate_)
UpdateActiveSymbols();
@@ -383,11 +567,11 @@ SymbolType SymbolMap::GetSymbolType(u32 address) {
return ST_NONE;
}
bool SymbolMap::GetSymbolInfo(SymbolInfo *info, u32 address, SymbolType symmask) {
bool SymbolMap::GetSymbolInfo(SymbolInfo *info, u32 address, SymbolType symbolMask) {
u32 functionAddress = INVALID_ADDRESS;
u32 dataAddress = INVALID_ADDRESS;
if (symmask & ST_FUNCTION) {
if (symbolMask & ST_FUNCTION) {
functionAddress = GetFunctionStart(address);
// If both are found, we always return the function, so just do that early.
@@ -403,7 +587,7 @@ bool SymbolMap::GetSymbolInfo(SymbolInfo *info, u32 address, SymbolType symmask)
}
}
if (symmask & ST_DATA) {
if (symbolMask & ST_DATA) {
dataAddress = GetDataStart(address);
if (dataAddress != INVALID_ADDRESS) {
@@ -421,12 +605,12 @@ bool SymbolMap::GetSymbolInfo(SymbolInfo *info, u32 address, SymbolType symmask)
return false;
}
u32 SymbolMap::GetNextSymbolAddress(u32 address, SymbolType symmask) {
u32 SymbolMap::GetNextSymbolAddress(u32 address, SymbolType symbolMask) {
if (activeNeedUpdate_)
UpdateActiveSymbols();
const auto functionEntry = symmask & ST_FUNCTION ? activeFunctions.upper_bound(address) : activeFunctions.end();
const auto dataEntry = symmask & ST_DATA ? activeData.upper_bound(address) : activeData.end();
const auto functionEntry = (symbolMask & ST_FUNCTION) ? activeFunctions.upper_bound(address) : activeFunctions.end();
const auto dataEntry = (symbolMask & ST_DATA) ? activeData.upper_bound(address) : activeData.end();
if (functionEntry == activeFunctions.end() && dataEntry == activeData.end())
return INVALID_ADDRESS;
@@ -440,36 +624,37 @@ u32 SymbolMap::GetNextSymbolAddress(u32 address, SymbolType symmask) {
return dataAddress;
}
std::string SymbolMap::GetDescription(unsigned int address) {
std::string labelName;
std::string SymbolMap::GetDescription(u32 address) {
u32 funcStart = GetFunctionStart(address);
const char *labelName = nullptr;
if (funcStart != INVALID_ADDRESS) {
labelName = GetLabelName(funcStart);
} else {
u32 dataStart = GetDataStart(address);
if (dataStart != INVALID_ADDRESS)
if (dataStart != INVALID_ADDRESS) {
labelName = GetLabelName(dataStart);
}
}
if (!labelName.empty())
return labelName;
if (labelName) {
return std::string(labelName);
}
char descriptionTemp[32];
snprintf(descriptionTemp, sizeof(descriptionTemp), "(%08x)", address);
return descriptionTemp;
}
std::vector<SymbolEntry> SymbolMap::GetAllActiveSymbols(SymbolType symmask) {
std::vector<SymbolEntry> SymbolMap::GetAllActiveSymbols(SymbolType symbolMask) {
if (activeNeedUpdate_)
UpdateActiveSymbols();
std::vector<SymbolEntry> result;
if (symmask & ST_FUNCTION) {
for (auto it = activeFunctions.begin(); it != activeFunctions.end(); it++) {
if (symbolMask & ST_FUNCTION) {
for (auto &[key, func] : activeFunctions) {
SymbolEntry entry;
entry.address = it->first;
entry.address = key;
entry.size = GetFunctionSize(entry.address);
const char* name = GetLabelName(entry.address);
if (name)
@@ -478,10 +663,10 @@ std::vector<SymbolEntry> SymbolMap::GetAllActiveSymbols(SymbolType symmask) {
}
}
if (symmask & ST_DATA) {
for (auto it = activeData.begin(); it != activeData.end(); it++) {
if (symbolMask & ST_DATA) {
for (auto &[key, data] : activeData) {
SymbolEntry entry;
entry.address = it->first;
entry.address = key;
entry.size = GetDataSize(entry.address);
const char* name = GetLabelName(entry.address);
if (name)
@@ -493,13 +678,24 @@ std::vector<SymbolEntry> SymbolMap::GetAllActiveSymbols(SymbolType symmask) {
return result;
}
void SymbolMap::AddModule(const char *name, u32 address, u32 size) {
void SymbolMap::AddModule(const char *name, u32 address, u32 size, u32 crc) {
for (auto &module : modules) {
if (equals(module.name, name)) {
// A name match alone isn't proof it's really the same module reloading - some
// module names are generic enough to collide between unrelated binaries. If both
// sides know their crc and they disagree, treat this as a different module instead
// of falling through to reactivate (and thus reusing/polluting) the old one's
// symbol table; crc == 0 on either side means "unknown" and we fall back to
// matching by name alone, same as before crc existed.
if (module.crc != 0 && crc != 0 && module.crc != crc)
continue;
// Just reactivate that one.
module.start = address;
module.size = size;
activeModuleEnds.emplace(module.start + module.size, module );
if (crc != 0)
module.crc = crc;
activeModuleEnds.emplace(module.start + module.size, module);
activeNeedUpdate_ = true;
return;
}
@@ -509,6 +705,7 @@ void SymbolMap::AddModule(const char *name, u32 address, u32 size) {
truncate_cpy(mod.name, name);
mod.start = address;
mod.size = size;
mod.crc = crc;
mod.index = (int)modules.size() + 1;
modules.push_back(mod);
@@ -544,12 +741,35 @@ u32 SymbolMap::GetModuleAbsoluteAddr(u32 relative, int moduleIndex) const {
}
int SymbolMap::GetModuleIndex(u32 address) const {
// activeModuleEnds is keyed by each active module's END address, so upper_bound() finds
// the first module whose end is > address. That alone doesn't prove address falls inside
// it though - address could just as well be sitting in the gap before that module's start
// (e.g. between two active modules, or before the very first one) - so start must be
// checked too, or addresses in such a gap get silently misattributed to the wrong module.
auto iter = activeModuleEnds.upper_bound(address);
if (iter == activeModuleEnds.end())
return -1;
if (address < iter->second.start)
return -1;
return iter->second.index;
}
int SymbolMap::GetModuleIndexByName(const std::string &name) const {
// Prefer a currently active module if the name is ambiguous (e.g. two distinct modules
// that happen to share a name - see AddModule's crc handling).
for (const auto &[key, module] : activeModuleEnds) {
if (name == module.name)
return module.index;
}
// Not active (or never was) - fall back to the most recently added entry with that name.
int found = -1;
for (const auto &module : modules) {
if (name == module.name)
found = module.index;
}
return found;
}
bool SymbolMap::IsModuleActive(int moduleIndex) {
if (moduleIndex == 0) {
return true;
@@ -580,7 +800,7 @@ std::vector<LoadedModuleInfo> SymbolMap::getAllModules() const {
return result;
}
void SymbolMap::AddFunction(const char* name, u32 address, u32 size, int moduleIndex) {
void SymbolMap::AddFunction(const char* name, u32 address, u32 size, int moduleIndex, bool updateName) {
if (moduleIndex == -1) {
moduleIndex = GetModuleIndex(address);
} else if (moduleIndex == 0) {
@@ -625,7 +845,7 @@ void SymbolMap::AddFunction(const char* name, u32 address, u32 size, int moduleI
}
}
AddLabel(name, address, moduleIndex);
AddLabel(name, address, moduleIndex, updateName);
}
u32 SymbolMap::GetFunctionStart(u32 address) {
@@ -837,7 +1057,7 @@ bool SymbolMap::RemoveFunction(u32 startAddress, bool removeName) {
return true;
}
void SymbolMap::AddLabel(const char* name, u32 address, int moduleIndex) {
void SymbolMap::AddLabel(const char* name, u32 address, int moduleIndex, bool updateName) {
if (moduleIndex == -1) {
moduleIndex = GetModuleIndex(address);
} else if (moduleIndex == 0) {
@@ -854,8 +1074,15 @@ void SymbolMap::AddLabel(const char* name, u32 address, int moduleIndex) {
}
if (existing != labels.end()) {
// We leave an existing label alone, rather than overwriting.
// But we'll still upgrade it to the correct module / relative address.
// By default we leave an existing label's name alone, rather than overwriting it (see
// updateName's doc comment in the header).
bool nameChanged = false;
if (updateName && !equals(existing->second.name, name)) {
truncate_cpy(existing->second.name, name);
nameChanged = true;
}
// We'll still upgrade it to the correct module / relative address.
if (existing->second.module != moduleIndex) {
LabelEntry label = existing->second;
label.addr = relAddress;
@@ -869,6 +1096,15 @@ void SymbolMap::AddLabel(const char* name, u32 address, int moduleIndex) {
activeLabels.erase(active);
activeLabels.emplace(address, label);
}
} else if (nameChanged) {
// Module/address didn't change, but the name did - activeLabels still needs a
// refresh, since it holds a separate flattened (and const-valued) copy, not a
// reference into labels.
auto active = activeLabels.find(address);
if (active != activeLabels.end()) {
activeLabels.erase(active);
activeLabels.emplace(address, existing->second);
}
}
} else {
LabelEntry label;
+59 -4
View File
@@ -69,6 +69,24 @@ struct HWND__;
typedef struct HWND__ *HWND;
#endif
// SymbolMap keeps two parallel sets of tables for functions/labels/data:
// - The "master" tables (functions/labels/data below) are permanent: keyed by
// (moduleIndex, addressRelativeToModuleStart), they retain every symbol ever seen for
// every module ever loaded this session, even after a module unloads. This is what makes
// it safe for two unrelated modules to occupy the same address range at different points
// in a session (common - modules load/unload constantly) without losing or corrupting
// each other's symbols.
// - The "active" tables (activeFunctions/activeLabels/activeData) are a derived, read-only
// cache: just the master entries belonging to currently-loaded modules, flattened to plain
// absolute addresses. Nearly every query (GetFunctionStart, GetSymbolType, ...) reads only
// from these, so lookups are a single cheap map access with no per-call module resolution,
// and a stale/unloaded module's symbols can never shadow whatever's actually live right now.
// The active tables are rebuilt lazily by UpdateActiveSymbols() - AddModule/UnloadModule just
// set activeNeedUpdate_, and the rebuild happens on the next query.
//
// Module index 0 is reserved to mean "unknown module" and is always treated as active - it
// exists for backward compatibility with old flat (non-per-module) symbol files, and for
// symbols added without an explicit module.
class SymbolMap {
public:
SymbolMap() {}
@@ -81,10 +99,30 @@ public:
bool LoadNocashSym(const Path &filename);
bool SaveNocashSym(const Path &filename) const;
// Save/load just one module's symbols, e.g. for the WebSocket debugger's
// hle.module.saveSymbols/loadSymbols, or the auto-save/load hooks around module load/unload
// in Core/HLE/sceKernelModule.cpp (gated on g_Config.bAutoSaveLoadSymbols). Addresses inside
// the file are relative to the module's load address (like the master tables above), so a
// saved file stays valid however the module ends up positioned on a later run.
// The file is keyed by module name + crc, not by game - see GetModuleSymbolsPath - so it's
// deliberately shared by every game that happens to load the exact same module (common for
// kernel/driver modules, or a homebrew's own libraries). gameID/gameTitle are only recorded
// as an informational "last saved by" comment for a human reading the file; they don't
// affect the file path or matching.
// LoadModuleSymbols requires moduleIndex to currently be an active module (so relative
// addresses can be resolved), and overwrites any existing names for symbols it touches -
// the file is assumed to be the authoritative, possibly hand-edited version.
bool SaveModuleSymbols(int moduleIndex, const Path &filename, const std::string &gameID, const std::string &gameTitle) const;
bool LoadModuleSymbols(int moduleIndex, const Path &filename);
// Standard per-module symbol file path: <memstick>/PSP/SYSTEM/SYMBOLS/<moduleName>_<crc>.ppsym
static Path GetModuleSymbolsPath(const std::string &moduleName, u32 crc);
// 0 if moduleIndex isn't known (never seen, not just inactive).
u32 GetModuleCrc(int moduleIndex) const;
SymbolType GetSymbolType(u32 address);
bool GetSymbolInfo(SymbolInfo *info, u32 address, SymbolType symmask = ST_FUNCTION);
u32 GetNextSymbolAddress(u32 address, SymbolType symmask);
std::string GetDescription(unsigned int address);
std::string GetDescription(u32 address);
std::vector<SymbolEntry> GetAllActiveSymbols(SymbolType symmask);
#ifdef _WIN32
@@ -92,15 +130,26 @@ public:
#endif
void GetLabels(std::vector<LabelDefinition> &dest);
void AddModule(const char *name, u32 address, u32 size);
// crc is optional (0 = unknown) and lets a reload of the same-named module be told apart
// from a different binary that just happens to share a name (common with generic module
// names) - see AddModule's implementation comment. Passing 0 falls back to matching by
// name alone, same as before crc existed.
void AddModule(const char *name, u32 address, u32 size, u32 crc = 0);
void UnloadModule(u32 address, u32 size);
u32 GetModuleRelativeAddr(u32 address, int moduleIndex = -1) const;
u32 GetModuleAbsoluteAddr(u32 relative, int moduleIndex) const;
// Index of the currently *active* module containing address, or -1 if none does (including
// if address falls in a gap between active modules' ranges).
int GetModuleIndex(u32 address) const;
// Prefers a currently active module if the name is ambiguous; otherwise the most recently
// added module entry with that name (which may be inactive). -1 if never seen.
int GetModuleIndexByName(const std::string &name) const;
bool IsModuleActive(int moduleIndex);
std::vector<LoadedModuleInfo> getAllModules() const;
void AddFunction(const char* name, u32 address, u32 size, int moduleIndex = -1);
// updateName controls whether an existing label at this address gets its name overwritten -
// see AddLabel.
void AddFunction(const char* name, u32 address, u32 size, int moduleIndex = -1, bool updateName = false);
u32 GetFunctionStart(u32 address);
int GetFunctionNum(u32 address);
u32 GetFunctionSize(u32 startAddress);
@@ -111,7 +160,11 @@ public:
// Only valid for currently loaded modules. Not guaranteed there will be a function.
u32 FindPossibleFunctionAtAfter(u32 address);
void AddLabel(const char* name, u32 address, int moduleIndex = -1);
// By default, an existing label's name is left alone (first writer wins) - this protects a
// deliberately-assigned name from being clobbered by a later, lower-confidence automatic
// detection pass. Pass updateName=true to override this for a source that should take
// priority, e.g. restoring symbols explicitly saved for this module.
void AddLabel(const char* name, u32 address, int moduleIndex = -1, bool updateName = false);
std::string GetLabelString(u32 address);
void SetLabelName(const char* name, u32 address);
bool GetLabelValue(const char* name, u32& dest);
@@ -158,6 +211,8 @@ private:
u32 start;
u32 size;
char name[128];
// 0 = unknown. See AddModule.
u32 crc = 0;
};
// These are flattened, read-only copies of the actual data in active modules only.
+87
View File
@@ -20,6 +20,7 @@
#include "Core/Config.h"
#include "Core/Core.h"
#include "Core/System.h"
#include "Core/ELF/ParamSFO.h"
#include "Core/Debugger/DisassemblyManager.h"
#include "Core/Debugger/SymbolMap.h"
#include "Core/Debugger/WebSocket/HLESubscriber.h"
@@ -46,6 +47,8 @@ DebuggerSubscriber *WebSocketHLEInit(DebuggerEventHandlerMap &map) {
map["hle.func.rename"] = &WebSocketHLEFuncRename;
map["hle.func.scan"] = &WebSocketHLEFuncScan;
map["hle.module.list"] = &WebSocketHLEModuleList;
map["hle.module.saveSymbols"] = &WebSocketHLEModuleSaveSymbols;
map["hle.module.loadSymbols"] = &WebSocketHLEModuleLoadSymbols;
map["hle.backtrace"] = &WebSocketHLEBacktrace;
map["hle.data.list"] = &WebSocketHLEDataList;
map["hle.data.add"] = &WebSocketHLEDataAdd;
@@ -649,6 +652,90 @@ void WebSocketHLEModuleList(DebuggerRequest &req) {
});
}
// Save one module's symbols to its standard per-module file (hle.module.saveSymbols)
//
// Saves to <memstick>/PSP/SYSTEM/SYMBOLS/<moduleName>_<crc>.ppsym - see
// SymbolMap::GetModuleSymbolsPath. Keyed by module name+crc rather than the current game, so
// the file is shared by every game/homebrew that happens to load the exact same module. This is
// the same file LoadModuleSymbols/hle.module.loadSymbols reads, and (if
// g_Config.bAutoSaveLoadSymbols is on - see Core/HLE/sceKernelModule.cpp) the same file
// auto-save-on-unload writes and auto-load-on-module-load reads.
//
// Parameters:
// - name: string, name of the module to save (as returned by hle.module.list.)
//
// Response (same event name):
// - path: string, the file path that was written.
void WebSocketHLEModuleSaveSymbols(DebuggerRequest &req) {
if (!g_symbolMap)
return req.Fail("CPU not active");
std::string name;
if (!req.ParamString("name", &name))
return;
// Route the actual symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
int moduleIndex = g_symbolMap->GetModuleIndexByName(name);
if (moduleIndex <= 0) {
req.Fail("No module found with that name");
return;
}
Path path = SymbolMap::GetModuleSymbolsPath(name, g_symbolMap->GetModuleCrc(moduleIndex));
if (!g_symbolMap->SaveModuleSymbols(moduleIndex, path, g_paramSFO.GetDiscID(), g_paramSFO.GetValueString("TITLE"))) {
req.Fail("Failed to save symbols file");
return;
}
JsonWriter &json = req.Respond();
json.writeString("path", path.ToString());
});
}
// Load a module's previously saved symbols (hle.module.loadSymbols)
//
// Reads from the same standard path hle.module.saveSymbols writes - see its docs above.
// Existing symbol names for this module are overwritten by what's in the file.
//
// Parameters:
// - name: string, name of the module to load into (must currently be loaded - see
// hle.module.list.)
//
// Response (same event name):
// - path: string, the file path that was read.
void WebSocketHLEModuleLoadSymbols(DebuggerRequest &req) {
if (!g_symbolMap)
return req.Fail("CPU not active");
std::string name;
if (!req.ParamString("name", &name))
return;
// Route the actual symbol manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
int moduleIndex = g_symbolMap->GetModuleIndexByName(name);
if (moduleIndex <= 0 || !g_symbolMap->IsModuleActive(moduleIndex)) {
req.Fail("No active module found with that name");
return;
}
Path path = SymbolMap::GetModuleSymbolsPath(name, g_symbolMap->GetModuleCrc(moduleIndex));
if (!g_symbolMap->LoadModuleSymbols(moduleIndex, path)) {
req.Fail("Failed to load symbols file (does it exist?)");
return;
}
// Clear cache so the disassembly view picks up the newly loaded names.
g_disassemblyManager.clear();
JsonWriter &json = req.Respond();
json.writeString("path", path.ToString());
});
}
// Walk the stack and list stack frames (hle.backtrace)
//
// Parameters:
+2
View File
@@ -31,6 +31,8 @@ void WebSocketHLEFuncRemoveRange(DebuggerRequest &req);
void WebSocketHLEFuncRename(DebuggerRequest &req);
void WebSocketHLEFuncScan(DebuggerRequest &req);
void WebSocketHLEModuleList(DebuggerRequest &req);
void WebSocketHLEModuleSaveSymbols(DebuggerRequest &req);
void WebSocketHLEModuleLoadSymbols(DebuggerRequest &req);
void WebSocketHLEBacktrace(DebuggerRequest &req);
void WebSocketHLEDataList(DebuggerRequest &req);
void WebSocketHLEDataAdd(DebuggerRequest &req);
+27 -2
View File
@@ -190,6 +190,19 @@ struct PspLibStubEntry {
PSPModule::~PSPModule() {
if (memoryBlockAddr) {
if (g_Config.bAutoSaveLoadSymbols) {
// Must happen before UnloadModule() below, while this module's symbols are still
// active (SaveModuleSymbols itself doesn't care, but GetModuleIndexByName's
// active-module lookup does).
char moduleName[29] = { 0 };
truncate_cpy(moduleName, nm.name);
int idx = g_symbolMap->GetModuleIndexByName(moduleName);
if (idx > 0) {
Path path = SymbolMap::GetModuleSymbolsPath(moduleName, g_symbolMap->GetModuleCrc(idx));
g_symbolMap->SaveModuleSymbols(idx, path, g_paramSFO.GetDiscID(), g_paramSFO.GetValueString("TITLE"));
}
}
// If it's either below user memory, or using a high kernel bit, it's in kernel.
if (memoryBlockAddr < PSP_GetUserMemoryBase() || memoryBlockAddr > PSP_GetUserMemoryEnd()) {
kernelMemory.Free(memoryBlockAddr);
@@ -292,7 +305,13 @@ void PSPModule::DoState(PointerWrap &p) {
char moduleName[29] = { 0 };
truncate_cpy(moduleName, nm.name);
if (memoryBlockAddr != 0) {
g_symbolMap->AddModule(moduleName, memoryBlockAddr, memoryBlockSize);
g_symbolMap->AddModule(moduleName, memoryBlockAddr, memoryBlockSize, crc);
if (g_Config.bAutoSaveLoadSymbols) {
int idx = g_symbolMap->GetModuleIndexByName(moduleName);
if (idx > 0) {
g_symbolMap->LoadModuleSymbols(idx, SymbolMap::GetModuleSymbolsPath(moduleName, crc));
}
}
}
}
@@ -1348,7 +1367,13 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
strncpy(moduleName, modinfo->name, ARRAY_SIZE(module->nm.name));
if (module->memoryBlockAddr != 0) {
g_symbolMap->AddModule(moduleName, module->memoryBlockAddr, module->memoryBlockSize);
g_symbolMap->AddModule(moduleName, module->memoryBlockAddr, module->memoryBlockSize, module->crc);
if (g_Config.bAutoSaveLoadSymbols) {
int idx = g_symbolMap->GetModuleIndexByName(moduleName);
if (idx > 0) {
g_symbolMap->LoadModuleSymbols(idx, SymbolMap::GetModuleSymbolsPath(moduleName, module->crc));
}
}
}
SectionID textSection = reader.GetSectionByName(".text");
-4
View File
@@ -250,10 +250,6 @@ int MIPSDebugInterface::getColor(unsigned int address, bool darkMode) const {
}
}
std::string MIPSDebugInterface::getDescription(unsigned int address) const {
return g_symbolMap->GetDescription(address);
}
std::string MIPSDebugInterface::GetRegName(int cat, int index) {
static const char * const regName[32] = {
"zero", "at", "v0", "v1",
-2
View File
@@ -23,7 +23,6 @@
#include "Core/MIPS/MIPS.h"
#include "Core/Debugger/DebugInterface.h"
class MIPSDebugInterface : public DebugInterface {
private:
MIPSState *cpu;
@@ -38,7 +37,6 @@ public:
void toggleBreakpoint(unsigned int address);
unsigned int readMemory(unsigned int address) const;
int getColor(unsigned int address, bool darkMode) const;
std::string getDescription(unsigned int address) const;
u32 GetGPR32Value(int reg) const override { return cpu->r[reg]; }
float GetFPR32Value(int reg) const { return cpu->f[reg]; }
+2
View File
@@ -201,6 +201,8 @@ void DeveloperToolsScreen::CreateGeneralTab(UI::LinearLayout *list) {
CheckBox *localDebugger = list->Add(new CheckBox(&g_Config.bRemoteDebuggerLocal, dev->T("Use locally hosted remote debugger")));
localDebugger->SetEnabledPtr(&allowDebugger_);
list->Add(new CheckBox(&g_Config.bAutoSaveLoadSymbols, dev->T("Auto save/load module symbols")));
list->Add(new Choice(dev->T("GPI/GPO switches/LEDs")))->OnClick.Add([=](UI::EventParams &e) {
screenManager()->push(new GPIGPOScreen(dev->T("GPI/GPO switches/LEDs")));
});
+1 -1
View File
@@ -115,7 +115,7 @@ file - this is just an index.
| Memory info/annotations | `memory.mapping`, `memory.info.config/set/list/search` | `MemoryInfoSubscriber.cpp` |
| Disassembly | `memory.base`, `memory.disasm` (add `compact=true` for plain-text lines instead of full per-field objects), `memory.searchDisasm` (add `findAll=true` for every match instead of just the first - e.g. "every caller of this address"), `memory.assemble` | `DisasmSubscriber.cpp` |
| GE display list disassembly | `gpu.displaylist.disasm` - like `memory.disasm` but for GE command words (`CLEARMODE`, `PRIM`, etc.) instead of CPU instructions; also supports `compact=true` | `GPUDisasmSubscriber.cpp` |
| HLE | `hle.thread.list/wake/stop`, `hle.func.list/add/remove/removeRange/rename/scan`, `hle.module.list`, `hle.backtrace` | `HLESubscriber.cpp` |
| HLE | `hle.thread.list/wake/stop`, `hle.func.list/add/remove/removeRange/rename/scan`, `hle.module.list`, `hle.module.saveSymbols/loadSymbols` (save/load one module's symbols to/from its standard `PSP/SYSTEM/SYMBOLS/<moduleName>_<crc>.ppsym` file, shared across any game that loads the same module - see `SymbolMap::GetModuleSymbolsPath`), `hle.backtrace` | `HLESubscriber.cpp` |
| Data symbols | `hle.data.list/add/remove/rename` - label discovered data (structs, tables, buffers) with a name/type, same idea as `hle.func.*` but for `ST_DATA` symbols | `HLESubscriber.cpp` |
| GPU stats | `gpu.stats.get`, `gpu.stats.feed` | `GPUStatsSubscriber.cpp` |
| GPU recording | `gpu.record.dump` | `GPURecordSubscriber.cpp` |