mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
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:
1 parent
be236d47c8
commit
a502b55fea
2 files changed
+83
-9
No files matched your search
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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` |
|
||||
|
||||
Reference in new issue
Block a user