Debugger: add memory.disasm compact mode and memory.searchDisasm findAll

Two real gaps hit repeatedly while investigating the VSH boot path (see
docs/VSHBootInvestigation.md):

- memory.disasm's response is the full per-field JSON (type, address,
  addressSize, encoding, macroEncoding, backgroundColor, name, params,
  symbol, function, dataSymbol, breakpoint, isCurrentPC, branch,
  relevantData, conditionMet, dataAccess - ~15 fields per line). Reading
  disassembly by hand meant writing a throwaway script each time to reduce
  this down to "ADDR: name params" - and at least once, a bug in one of
  those scripts produced misleading output that wasn't caught immediately.
  Added compact=true: returns "lines" as an array of plain strings
  ("M AAAAAAAA  [symbol: ]name params", M = '>' for current PC, '*'/'o'
  for an enabled/disabled breakpoint) instead, computed once correctly
  here instead of ad hoc every time.

- memory.searchDisasm already existed but only ever returned the first
  match - genuinely limiting for "find every caller of this address"
  call-graph-style queries, which came up directly while trying to trace
  which function builds VSH's GE display list. Added findAll=true: scans
  the whole range and returns every match in a new "addresses" array
  (capped at 1000), instead of stopping at the first. Default behavior
  (address: first match or null) is unchanged for existing callers.

Verified live via PPSSPPHeadless + wsdbg: compact mode against a real
demo ELF's entry point produces clean, correctly-marked text lines;
findAll=true against the same range found all 11 jal instructions instead
of just the first. UnitTest.exe all: 49/49 passed.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This commit is contained in:
Henrik RydgårdandClaude Sonnet 5 committed 2026-08-14 11:04:32 +02:00
1 parent be236d47c8
commit a502b55fea
2 files changed
+83 -9

No files matched your search

+81 -8
View File
@@ -49,6 +49,7 @@ public:
protected:
void WriteDisasmLine(JsonWriter &json, const DisassemblyLineInfo &l);
void WriteBranchGuide(JsonWriter &json, const BranchLine &l);
std::string FormatDisasmLineCompact(const DisassemblyLineInfo &l, DebugInterface *cpuDebug);
};
DebuggerSubscriber *WebSocketDisasmInit(DebuggerEventHandlerMap &map) {
@@ -234,6 +235,46 @@ void WebSocketDisasmState::WriteDisasmLine(JsonWriter &json, const DisassemblyLi
json.pop();
}
// One line of "ADDR name params" text, with a leading marker column ('>' = current PC,
// '*' = enabled breakpoint here, 'o' = disabled breakpoint here) and the symbol name inlined
// if known. Meant for compact=true: full-JSON disasm lines (WriteDisasmLine above, ~15 fields
// each) are precise but slow to skim by hand - every manual disassembly read this session (see
// docs/VSHBootInvestigation.md) ended up needing a throwaway script to reduce them to exactly
// this, and at least once that script's own parsing bug produced misleading output. Doing the
// reduction here instead means there's one correct implementation instead of a new ad hoc one
// each time.
std::string WebSocketDisasmState::FormatDisasmLineCompact(const DisassemblyLineInfo &l, DebugInterface *cpuDebug) {
u32 addr = l.info.opcodeAddress;
bool enabled = false;
bool hasBreakpoint = false;
for (u32 i = 0; i < l.totalSize; i += 4) {
if (g_breakpoints.IsAddressBreakPoint(addr + i, &enabled)) {
hasBreakpoint = true;
if (enabled)
break;
}
}
char marker = ' ';
if (cpuDebug->GetPC() == addr)
marker = '>';
else if (hasBreakpoint)
marker = enabled ? '*' : 'o';
std::string line = StringFromFormat("%c %08x ", marker, addr);
const std::string symbol = g_symbolMap->GetLabelString(addr);
if (!symbol.empty()) {
line += symbol;
line += ": ";
}
line += l.name;
if (!l.params.empty()) {
line += " ";
line += l.params;
}
return line;
}
void WebSocketDisasmState::WriteBranchGuide(JsonWriter &json, const BranchLine &l) {
json.pushDict();
json.writeUint("top", l.first);
@@ -269,12 +310,14 @@ void WebSocketDisasmState::Base(DebuggerRequest &req) {
// - address: number specifying the start address.
// - count: number of lines to return (may be clamped to an internal limit.)
// - displaySymbols: boolean true to show symbol names in instruction params.
// - compact: optional boolean, default false. See "lines" below.
//
// Parameters (by end address):
// - thread: optional number indicating the thread id for branch info.
// - address: number specifying the start address.
// - end: number which must be after the start address (may be clamped to an internal limit.)
// - displaySymbols: boolean true to show symbol names in instruction params.
// - compact: optional boolean, default false. See "lines" below.
//
// Response (same event name):
// - range: object with result "start" and "end" properties, the addresses actually used.
@@ -284,7 +327,7 @@ void WebSocketDisasmState::Base(DebuggerRequest &req) {
// - bottom: the later address as a number.
// - direction: "up", "down", or "right" depending on the flow of the branch.
// - lane: number index to avoid overlapping guides.
// - lines: array of objects:
// - lines: with compact=false (default), array of objects:
// - type: "opcode", "macro", "data", or "other".
// - address: address of first actual instruction.
// - addressSize: bytes used by this line (might be more than 4.)
@@ -293,6 +336,10 @@ void WebSocketDisasmState::Base(DebuggerRequest &req) {
// - name: string name of the instruction.
// - params: formatted parameters for the instruction.
// - (other info about the disassembled line.)
// with compact=true, array of strings instead, one per line, formatted as
// "M AAAAAAAA [symbol: ]name params" where M is '>' for the current PC, '*'/'o' for an
// enabled/disabled breakpoint at that address, or ' ' otherwise - meant for skimming a
// range by eye by hand instead of parsing the full per-field JSON.
void WebSocketDisasmState::Disasm(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
@@ -351,6 +398,9 @@ void WebSocketDisasmState::Disasm(DebuggerRequest &req) {
bool displaySymbols = true;
if (!req.ParamBool("displaySymbols", &displaySymbols, DebuggerParamType::OPTIONAL))
return;
bool compact = false;
if (!req.ParamBool("compact", &compact, DebuggerParamType::OPTIONAL))
return;
JsonWriter &json = req.Respond();
json.pushDict("range");
@@ -363,7 +413,10 @@ void WebSocketDisasmState::Disasm(DebuggerRequest &req) {
uint32_t addr = start;
for (uint32_t i = 0; i < count; ++i) {
g_disassemblyManager.getLine(addr, displaySymbols, line, cpuDebug);
WriteDisasmLine(json, line);
if (compact)
json.writeString(FormatDisasmLineCompact(line, cpuDebug));
else
WriteDisasmLine(json, line);
addr += line.totalSize;
// These are pretty long, so let's grease the wheels a bit.
@@ -388,9 +441,18 @@ void WebSocketDisasmState::Disasm(DebuggerRequest &req) {
// - end: optional end address as a number (otherwise uses start address.)
// - match: string to search for.
// - displaySymbols: optional, specify false to hide symbols in the searched parameters.
// - findAll: optional boolean, default false. When true, scans the whole range instead of
// stopping at the first match - e.g. for "every jal/jalr/j targeting this address" call-graph
// style queries (search for the target's hex address, or its symbol name if it has one),
// where the first match alone isn't the answer. Capped at 1000 results; a huge range with a
// very common match (e.g. an empty/near-universal 'match' string) will still stop there
// rather than building an unbounded response.
//
// Response (same event name):
// - address: number address of match or null if none was found.
// - address: number address of the first match, or null if none was found (same as before
// findAll existed - set from the same scan even when findAll is true).
// - addresses: array of numbers, every match found. With findAll=false this has at most one
// entry (the same value as "address"); with findAll=true, up to 1000.
void WebSocketDisasmState::SearchDisasm(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
@@ -407,6 +469,10 @@ void WebSocketDisasmState::SearchDisasm(DebuggerRequest &req) {
bool displaySymbols = true;
if (!req.ParamBool("displaySymbols", &displaySymbols, DebuggerParamType::OPTIONAL))
return;
bool findAll = false;
if (!req.ParamBool("findAll", &findAll, DebuggerParamType::OPTIONAL))
return;
static const size_t MAX_RESULTS = 1000;
bool loopSearch = end <= start;
start = RoundMemAddressUp(start);
@@ -414,6 +480,8 @@ void WebSocketDisasmState::SearchDisasm(DebuggerRequest &req) {
// We must've passed end by rounding up.
JsonWriter &json = req.Respond();
json.writeNull("address");
json.pushArray("addresses");
json.pop();
return;
}
@@ -432,7 +500,7 @@ void WebSocketDisasmState::SearchDisasm(DebuggerRequest &req) {
return;
DisassemblyLineInfo line;
bool found = false;
std::vector<uint32_t> matches;
uint32_t addr = start;
do {
g_disassemblyManager.getLine(addr, displaySymbols, line, cpuDebug);
@@ -454,18 +522,23 @@ void WebSocketDisasmState::SearchDisasm(DebuggerRequest &req) {
inserter = std::transform(line.params.begin(), line.params.end(), inserter, ::tolower);
if (mergeForSearch.find(match) != mergeForSearch.npos) {
found = true;
break;
matches.push_back(addr);
if (!findAll || matches.size() >= MAX_RESULTS)
break;
}
addr = RoundMemAddressUp(addr + line.totalSize);
} while (addr != end);
JsonWriter &json = req.Respond();
if (found)
json.writeUint("address", addr);
if (!matches.empty())
json.writeUint("address", matches[0]);
else
json.writeNull("address");
json.pushArray("addresses");
for (uint32_t m : matches)
json.writeUint(m);
json.pop();
});
}
+2 -1
View File
@@ -109,7 +109,8 @@ file - this is just an index.
| Memory read/write | `memory.read_u8/u16/u32`, `memory.read`, `memory.readString`, `memory.write_u8/u16/u32`, `memory.write` | `MemorySubscriber.cpp` |
| Memory search | `memory.search` - scan a range for a `u8`/`u16`/`u32`/`float` value or a `bytes` pattern (with an optional wildcard mask), for narrowing down where an unknown value lives (Cheat Engine style) | `MemorySubscriber.cpp` |
| Memory info/annotations | `memory.mapping`, `memory.info.config/set/list/search` | `MemoryInfoSubscriber.cpp` |
| Disassembly | `memory.base`, `memory.disasm`, `memory.searchDisasm`, `memory.assemble` | `DisasmSubscriber.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` |
| 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` |