diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 11e8f77281..17ac8028a1 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -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()); } diff --git a/Core/CmdLine.h b/Core/CmdLine.h index d9bc472fd5..1859ab961c 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -47,6 +47,11 @@ struct CommandLineOptions { // Also breaks the CPU at start in the headless build. See docs/WebSocketDebugger.md. std::optional 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 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 bootVSH; diff --git a/Core/Config.cpp b/Core/Config.cpp index 57d78d4cfe..c7000c41ad 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -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[] = { diff --git a/Core/Config.h b/Core/Config.h index 479c1dafed..881435ec58 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -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 diff --git a/Core/Debugger/MemBlockInfo.cpp b/Core/Debugger/MemBlockInfo.cpp index cc65d3af60..bd0838e3dc 100644 --- a/Core/Debugger/MemBlockInfo.cpp +++ b/Core/Debugger/MemBlockInfo.cpp @@ -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 diff --git a/Core/Debugger/SymbolMap.cpp b/Core/Debugger/SymbolMap.cpp index 3cedcc7250..a9a11b6780 100644 --- a/Core/Debugger/SymbolMap.cpp +++ b/Core/Debugger/SymbolMap.cpp @@ -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 +// # game -- informational only, see SaveModuleSymbols +// F -- function +// D -- data (type: byte/halfword/word/ascii) +// L -- 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 SymbolMap::GetAllActiveSymbols(SymbolType symmask) { +std::vector SymbolMap::GetAllActiveSymbols(SymbolType symbolMask) { if (activeNeedUpdate_) UpdateActiveSymbols(); std::vector 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 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 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 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; diff --git a/Core/Debugger/SymbolMap.h b/Core/Debugger/SymbolMap.h index d7f45fcb21..5693a898d3 100644 --- a/Core/Debugger/SymbolMap.h +++ b/Core/Debugger/SymbolMap.h @@ -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: /PSP/SYSTEM/SYMBOLS/_.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 GetAllActiveSymbols(SymbolType symmask); #ifdef _WIN32 @@ -92,15 +130,26 @@ public: #endif void GetLabels(std::vector &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 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. diff --git a/Core/Debugger/WebSocket/HLESubscriber.cpp b/Core/Debugger/WebSocket/HLESubscriber.cpp index 9920c045ed..f88c5f5a99 100644 --- a/Core/Debugger/WebSocket/HLESubscriber.cpp +++ b/Core/Debugger/WebSocket/HLESubscriber.cpp @@ -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 /PSP/SYSTEM/SYMBOLS/_.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: diff --git a/Core/Debugger/WebSocket/HLESubscriber.h b/Core/Debugger/WebSocket/HLESubscriber.h index d23479e965..58355d4ffb 100644 --- a/Core/Debugger/WebSocket/HLESubscriber.h +++ b/Core/Debugger/WebSocket/HLESubscriber.h @@ -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); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index cd4a3cace7..bdb7086491 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -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"); diff --git a/Core/MIPS/MIPSDebugInterface.cpp b/Core/MIPS/MIPSDebugInterface.cpp index 1e8cbd60df..57a9189c36 100644 --- a/Core/MIPS/MIPSDebugInterface.cpp +++ b/Core/MIPS/MIPSDebugInterface.cpp @@ -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", diff --git a/Core/MIPS/MIPSDebugInterface.h b/Core/MIPS/MIPSDebugInterface.h index 9b4b10c0c3..1eb2ee2e52 100644 --- a/Core/MIPS/MIPSDebugInterface.h +++ b/Core/MIPS/MIPSDebugInterface.h @@ -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]; } diff --git a/UI/DeveloperToolsScreen.cpp b/UI/DeveloperToolsScreen.cpp index 7e5eff89c2..d0f2280bee 100644 --- a/UI/DeveloperToolsScreen.cpp +++ b/UI/DeveloperToolsScreen.cpp @@ -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"))); }); diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index 7750bbf51d..ba529af390 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -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/_.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` |