mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-01 18:25:19 +02:00
Add GPR write breakpoints (break when a register is written, anywhere)
New debugging primitive: break whenever any instruction writes to a
given general-purpose register (0-31), regardless of which address
executes the write. Requested for continuing the reboot.bin trace,
where the actual blocker is "what sets $s3 to this bad value", not
"what happens at a specific address" - existing address/memory
breakpoints can't express that directly.
- GPRBreakpoint (Core/Debugger/Breakpoints.h) mirrors the existing
BreakPoint/MemCheck shape (result/condition/logFormat/hit count),
keyed by register index instead of address/range.
- BreakpointManager keeps a u32 bitmask (bit i = register i has an
active breakpoint) alongside the GPRBreakpoint vector, so the
interpreter loop can test "would this write trip anything" with a
single shift+and against a value already cached in a local.
- RunUntilDowncountZeroWithChecks (Core/MIPS/MIPSTables.cpp) computes
the about-to-be-written register from the current instruction's
OUT_RT/OUT_RD/OUT_RA flags (GetGPRWriteTarget()) and checks it
against the mask, same convention as the existing memcheck handling
right above it (checked before the instruction executes, bails via
CORE_STEPPING_CPU without running it if tripped).
- New BreakReason::GPRBreakpoint ("cpu.gprBreakpoint") for Core_Break.
- WebSocket API: cpu.gprBreakpoint.add/update/remove/list, accepting
either a 0-31 'register' index or a case-insensitive 'name' (e.g.
"s3"), documented in docs/WebSocketDebugger.md.
Interpreter-only for now, deliberately.
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
3f6743d40d
commit
75174af77b
@@ -36,9 +36,42 @@ DebuggerSubscriber *WebSocketBreakpointInit(DebuggerEventHandlerMap &map) {
|
||||
map["memory.breakpoint.remove"] = &WebSocketMemoryBreakpointRemove;
|
||||
map["memory.breakpoint.list"] = &WebSocketMemoryBreakpointList;
|
||||
|
||||
map["cpu.gprBreakpoint.add"] = &WebSocketGPRBreakpointAdd;
|
||||
map["cpu.gprBreakpoint.update"] = &WebSocketGPRBreakpointUpdate;
|
||||
map["cpu.gprBreakpoint.remove"] = &WebSocketGPRBreakpointRemove;
|
||||
map["cpu.gprBreakpoint.list"] = &WebSocketGPRBreakpointList;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Resolves a GPR by name (e.g. "s3", case-insensitive) or 0-31 index. Interpreter-only feature -
|
||||
// see GPRBreakpoint in Breakpoints.h - has no effect while running under a JIT backend.
|
||||
static bool ParseGPRBreakpointReg(DebuggerRequest &req, int *reg) {
|
||||
if (req.HasParam("name")) {
|
||||
std::string name;
|
||||
if (!req.ParamString("name", &name))
|
||||
return false;
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
if (!strcasecmp(name.c_str(), MIPSDebugInterface::GetRegName(0, i).c_str())) {
|
||||
*reg = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
req.Fail(StringFromFormat("Unknown register name: %s", name.c_str()));
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t regU32;
|
||||
if (!req.ParamU32("register", ®U32))
|
||||
return false;
|
||||
if (regU32 >= 32) {
|
||||
req.Fail("Invalid 'register' parameter, must be 0-31");
|
||||
return false;
|
||||
}
|
||||
*reg = (int)regU32;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct WebSocketCPUBreakpointParams {
|
||||
uint32_t address = 0;
|
||||
bool hasEnabled = false;
|
||||
@@ -516,3 +549,213 @@ void WebSocketMemoryBreakpointList(DebuggerRequest &req) {
|
||||
json.pop();
|
||||
});
|
||||
}
|
||||
|
||||
struct WebSocketGPRBreakpointParams {
|
||||
int reg = 0;
|
||||
bool hasEnabled = false;
|
||||
bool hasLog = false;
|
||||
bool hasCondition = false;
|
||||
bool hasLogFormat = false;
|
||||
|
||||
bool enabled;
|
||||
bool log;
|
||||
std::string condition;
|
||||
PostfixExpression compiledCondition;
|
||||
std::string logFormat;
|
||||
|
||||
bool Parse(DebuggerRequest &req) {
|
||||
if (!currentDebugMIPS->isAlive()) {
|
||||
req.Fail("CPU not started");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ParseGPRBreakpointReg(req, ®))
|
||||
return false;
|
||||
|
||||
hasEnabled = req.HasParam("enabled");
|
||||
if (hasEnabled) {
|
||||
if (!req.ParamBool("enabled", &enabled))
|
||||
return false;
|
||||
}
|
||||
hasLog = req.HasParam("log");
|
||||
if (hasLog) {
|
||||
if (!req.ParamBool("log", &log))
|
||||
return false;
|
||||
}
|
||||
hasCondition = req.HasParam("condition");
|
||||
if (hasCondition) {
|
||||
if (!req.ParamString("condition", &condition))
|
||||
return false;
|
||||
if (!initExpression(currentDebugMIPS, condition.c_str(), compiledCondition)) {
|
||||
req.Fail(StringFromFormat("Could not parse expression syntax: %s", getExpressionError()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
hasLogFormat = req.HasParam("logFormat");
|
||||
if (hasLogFormat) {
|
||||
if (!req.ParamString("logFormat", &logFormat))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Apply() {
|
||||
if (hasCondition && !condition.empty()) {
|
||||
BreakPointCond cond;
|
||||
cond.debug = currentDebugMIPS;
|
||||
cond.expressionString = condition;
|
||||
cond.expression = compiledCondition;
|
||||
g_breakpoints.ChangeGPRBreakpointAddCond(reg, cond);
|
||||
} else if (hasCondition && condition.empty()) {
|
||||
g_breakpoints.ChangeGPRBreakpointRemoveCond(reg);
|
||||
}
|
||||
|
||||
if (hasLogFormat) {
|
||||
g_breakpoints.ChangeGPRBreakpointLogFormat(reg, logFormat);
|
||||
}
|
||||
|
||||
if (hasLog && !hasEnabled) {
|
||||
GPRBreakpoint bp;
|
||||
if (g_breakpoints.GetGPRBreakpoint(reg, &bp))
|
||||
enabled = bp.IsEnabled();
|
||||
hasEnabled = true;
|
||||
}
|
||||
if (hasLog && hasEnabled) {
|
||||
BreakAction result = BREAK_ACTION_IGNORE;
|
||||
if (log)
|
||||
result |= BREAK_ACTION_LOG;
|
||||
if (enabled)
|
||||
result |= BREAK_ACTION_PAUSE;
|
||||
g_breakpoints.ChangeGPRBreakpoint(reg, result);
|
||||
} else if (hasEnabled) {
|
||||
g_breakpoints.ChangeGPRBreakpoint(reg, enabled);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add a new GPR write breakpoint (cpu.gprBreakpoint.add)
|
||||
//
|
||||
// Interpreter-only for now - see GPRBreakpoint in Core/Debugger/Breakpoints.h. Has no effect
|
||||
// while running under a JIT backend (force the interpreter core, e.g. -i on the command line).
|
||||
//
|
||||
// Parameters:
|
||||
// - register: unsigned integer 0-31 GPR index to break on write to. Ignored if name given.
|
||||
// - name: string register name (e.g. "s3"), case-insensitive. Takes priority over 'register'.
|
||||
// - enabled: optional boolean, whether to actually enter stepping when this breakpoint trips.
|
||||
// - log: optional boolean, whether to log when this breakpoint trips.
|
||||
// - condition: optional string expression to evaluate - breakpoint does not trip if false.
|
||||
// - logFormat: optional string to log when breakpoint trips, may include {expression} parts.
|
||||
//
|
||||
// Response (same event name) with no extra data.
|
||||
//
|
||||
// Note: will replace any GPR breakpoint already set on the same register.
|
||||
void WebSocketGPRBreakpointAdd(DebuggerRequest &req) {
|
||||
WebSocketGPRBreakpointParams params;
|
||||
if (!params.Parse(req))
|
||||
return;
|
||||
|
||||
// Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly
|
||||
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
|
||||
Core_RunOnCPUThread([&] {
|
||||
g_breakpoints.AddGPRBreakpoint(params.reg);
|
||||
params.Apply();
|
||||
});
|
||||
req.Respond();
|
||||
}
|
||||
|
||||
// Update a GPR write breakpoint (cpu.gprBreakpoint.update)
|
||||
//
|
||||
// Parameters: same as cpu.gprBreakpoint.add.
|
||||
//
|
||||
// Response (same event name) with no extra data.
|
||||
void WebSocketGPRBreakpointUpdate(DebuggerRequest &req) {
|
||||
WebSocketGPRBreakpointParams params;
|
||||
if (!params.Parse(req))
|
||||
return;
|
||||
|
||||
// Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly
|
||||
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
|
||||
bool found = false;
|
||||
Core_RunOnCPUThread([&] {
|
||||
GPRBreakpoint bp;
|
||||
found = g_breakpoints.GetGPRBreakpoint(params.reg, &bp);
|
||||
if (found)
|
||||
params.Apply();
|
||||
});
|
||||
|
||||
if (!found)
|
||||
return req.Fail("Breakpoint not found");
|
||||
req.Respond();
|
||||
}
|
||||
|
||||
// Remove a GPR write breakpoint (cpu.gprBreakpoint.remove)
|
||||
//
|
||||
// Parameters:
|
||||
// - register: unsigned integer 0-31 GPR index. Ignored if name given.
|
||||
// - name: string register name (e.g. "s3"), case-insensitive. Takes priority over 'register'.
|
||||
//
|
||||
// Response (same event name) with no extra data.
|
||||
void WebSocketGPRBreakpointRemove(DebuggerRequest &req) {
|
||||
if (!currentDebugMIPS->isAlive()) {
|
||||
return req.Fail("CPU not started");
|
||||
}
|
||||
|
||||
int reg;
|
||||
if (!ParseGPRBreakpointReg(req, ®))
|
||||
return;
|
||||
|
||||
// Route the actual breakpoint manipulation to the CPU thread instead of poking at it directly
|
||||
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
|
||||
Core_RunOnCPUThread([&] {
|
||||
g_breakpoints.RemoveGPRBreakpoint(reg);
|
||||
});
|
||||
req.Respond();
|
||||
}
|
||||
|
||||
// List all GPR write breakpoints (cpu.gprBreakpoint.list)
|
||||
//
|
||||
// No parameters.
|
||||
//
|
||||
// Response (same event name):
|
||||
// - breakpoints: array of objects, each with properties:
|
||||
// - register: unsigned integer 0-31 GPR index.
|
||||
// - name: string register name (e.g. "s3").
|
||||
// - enabled: boolean, whether to actually enter stepping when this breakpoint trips.
|
||||
// - log: boolean, whether to log when this breakpoint trips.
|
||||
// - hits: unsigned integer, number of times this breakpoint has tripped (regardless of
|
||||
// whether it paused - i.e. even with enabled false, if log is true.)
|
||||
// - condition: null, or string expression to evaluate - breakpoint does not trip if false.
|
||||
// - logFormat: null, or string to log when breakpoint trips, may include {expression} parts.
|
||||
void WebSocketGPRBreakpointList(DebuggerRequest &req) {
|
||||
if (!currentDebugMIPS->isAlive()) {
|
||||
return req.Fail("CPU not started");
|
||||
}
|
||||
|
||||
// Route the breakpoint reads to the CPU thread instead of poking at them directly from this
|
||||
// WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
|
||||
Core_RunOnCPUThread([&] {
|
||||
JsonWriter &json = req.Respond();
|
||||
json.pushArray("breakpoints");
|
||||
std::vector<GPRBreakpoint> bps = g_breakpoints.GetGPRBreakpoints();
|
||||
for (const GPRBreakpoint &bp : bps) {
|
||||
json.pushDict();
|
||||
json.writeInt("register", bp.reg);
|
||||
json.writeString("name", MIPSDebugInterface::GetRegName(0, bp.reg));
|
||||
json.writeBool("enabled", bp.IsEnabled());
|
||||
json.writeBool("log", (bp.result & BREAK_ACTION_LOG) != 0);
|
||||
json.writeUint("hits", bp.numHits);
|
||||
if (bp.hasCond)
|
||||
json.writeString("condition", bp.cond.expressionString);
|
||||
else
|
||||
json.writeNull("condition");
|
||||
if (!bp.logFormat.empty())
|
||||
json.writeString("logFormat", bp.logFormat);
|
||||
else
|
||||
json.writeNull("logFormat");
|
||||
|
||||
json.pop();
|
||||
}
|
||||
json.pop();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user