mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-30 01:15:08 +02:00
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:
co-authored by
Claude Sonnet 5
parent
2a1df9f0b8
commit
29a38af37e
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]; }
|
||||
|
||||
@@ -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")));
|
||||
});
|
||||
|
||||
@@ -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` |
|
||||
|
||||
Reference in New Issue
Block a user