mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-04 11:45:18 +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
@@ -213,6 +213,7 @@ const char *BreakReasonToString(BreakReason reason) {
|
||||
case BreakReason::SavestateCrash: return "savestate.crash";
|
||||
case BreakReason::MemoryBreakpoint: return "memory.breakpoint";
|
||||
case BreakReason::CpuBreakpoint: return "cpu.breakpoint";
|
||||
case BreakReason::GPRBreakpoint: return "cpu.gprBreakpoint";
|
||||
case BreakReason::MemoryAccess: return "memory.access"; // ???
|
||||
case BreakReason::JitBranchDebug: return "jit.branchdebug";
|
||||
case BreakReason::RABreak: return "ra.break";
|
||||
|
||||
@@ -64,6 +64,7 @@ enum class BreakReason {
|
||||
SavestateCrash,
|
||||
MemoryBreakpoint,
|
||||
CpuBreakpoint,
|
||||
GPRBreakpoint,
|
||||
MemoryAccess, // ???
|
||||
JitBranchDebug,
|
||||
BreakOnBoot,
|
||||
|
||||
@@ -94,6 +94,15 @@ size_t BreakpointManager::FindMemCheck(u32 start, u32 end) {
|
||||
return INVALID_MEMCHECK;
|
||||
}
|
||||
|
||||
size_t BreakpointManager::FindGPRBreakpoint(int reg) {
|
||||
for (size_t i = 0; i < gprBreakpoints_.size(); ++i) {
|
||||
if (gprBreakpoints_[i].reg == reg)
|
||||
return i;
|
||||
}
|
||||
|
||||
return INVALID_GPR_BREAKPOINT;
|
||||
}
|
||||
|
||||
bool BreakpointManager::IsAddressBreakPoint(u32 addr)
|
||||
{
|
||||
if (!anyBreakPoints_)
|
||||
@@ -493,6 +502,161 @@ BreakAction BreakpointManager::ExecOpMemCheck(u32 address, u32 pc) {
|
||||
return BREAK_ACTION_IGNORE;
|
||||
}
|
||||
|
||||
void BreakpointManager::RecomputeGPRBreakpointMask() {
|
||||
u32 mask = 0;
|
||||
for (const auto &bp : gprBreakpoints_) {
|
||||
if (bp.result != BREAK_ACTION_IGNORE)
|
||||
mask |= 1u << bp.reg;
|
||||
}
|
||||
gprBreakpointMask_ = mask;
|
||||
}
|
||||
|
||||
int BreakpointManager::AddGPRBreakpoint(int reg) {
|
||||
size_t bp = FindGPRBreakpoint(reg);
|
||||
if (bp == INVALID_GPR_BREAKPOINT) {
|
||||
GPRBreakpoint pt;
|
||||
pt.reg = reg;
|
||||
pt.result |= BREAK_ACTION_PAUSE;
|
||||
|
||||
gprBreakpoints_.push_back(pt);
|
||||
RecomputeGPRBreakpointMask();
|
||||
Update(INVALID_ADDRESS); // Not baked into JIT code, no cache invalidation needed.
|
||||
return (int)gprBreakpoints_.size() - 1;
|
||||
} else if (!gprBreakpoints_[bp].IsEnabled()) {
|
||||
gprBreakpoints_[bp].result |= BREAK_ACTION_PAUSE;
|
||||
gprBreakpoints_[bp].hasCond = false;
|
||||
RecomputeGPRBreakpointMask();
|
||||
Update(INVALID_ADDRESS);
|
||||
return (int)bp;
|
||||
} else {
|
||||
return (int)bp;
|
||||
}
|
||||
}
|
||||
|
||||
void BreakpointManager::RemoveGPRBreakpoint(int reg) {
|
||||
size_t bp = FindGPRBreakpoint(reg);
|
||||
if (bp != INVALID_GPR_BREAKPOINT) {
|
||||
gprBreakpoints_.erase(gprBreakpoints_.begin() + bp);
|
||||
RecomputeGPRBreakpointMask();
|
||||
Update(INVALID_ADDRESS);
|
||||
}
|
||||
}
|
||||
|
||||
void BreakpointManager::ChangeGPRBreakpoint(int reg, bool status) {
|
||||
size_t bp = FindGPRBreakpoint(reg);
|
||||
if (bp != INVALID_GPR_BREAKPOINT) {
|
||||
if (status)
|
||||
gprBreakpoints_[bp].result |= BREAK_ACTION_PAUSE;
|
||||
else
|
||||
gprBreakpoints_[bp].result = BreakAction(gprBreakpoints_[bp].result & ~BREAK_ACTION_PAUSE);
|
||||
RecomputeGPRBreakpointMask();
|
||||
Update(INVALID_ADDRESS);
|
||||
}
|
||||
}
|
||||
|
||||
void BreakpointManager::ChangeGPRBreakpoint(int reg, BreakAction result) {
|
||||
size_t bp = FindGPRBreakpoint(reg);
|
||||
if (bp != INVALID_GPR_BREAKPOINT) {
|
||||
gprBreakpoints_[bp].result = result;
|
||||
RecomputeGPRBreakpointMask();
|
||||
Update(INVALID_ADDRESS);
|
||||
}
|
||||
}
|
||||
|
||||
void BreakpointManager::ClearAllGPRBreakpoints() {
|
||||
if (!gprBreakpoints_.empty()) {
|
||||
gprBreakpoints_.clear();
|
||||
gprBreakpointMask_ = 0;
|
||||
Update(INVALID_ADDRESS);
|
||||
}
|
||||
}
|
||||
|
||||
void BreakpointManager::ChangeGPRBreakpointAddCond(int reg, const BreakPointCond &cond) {
|
||||
size_t bp = FindGPRBreakpoint(reg);
|
||||
if (bp != INVALID_GPR_BREAKPOINT) {
|
||||
gprBreakpoints_[bp].hasCond = true;
|
||||
gprBreakpoints_[bp].cond = cond;
|
||||
// No need to update jit for a condition add/remove, they're not baked in.
|
||||
Update(INVALID_ADDRESS);
|
||||
}
|
||||
}
|
||||
|
||||
void BreakpointManager::ChangeGPRBreakpointRemoveCond(int reg) {
|
||||
size_t bp = FindGPRBreakpoint(reg);
|
||||
if (bp != INVALID_GPR_BREAKPOINT) {
|
||||
gprBreakpoints_[bp].hasCond = false;
|
||||
Update(INVALID_ADDRESS);
|
||||
}
|
||||
}
|
||||
|
||||
BreakPointCond *BreakpointManager::GetGPRBreakpointCondition(int reg) {
|
||||
size_t bp = FindGPRBreakpoint(reg);
|
||||
if (bp != INVALID_GPR_BREAKPOINT && gprBreakpoints_[bp].hasCond)
|
||||
return &gprBreakpoints_[bp].cond;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void BreakpointManager::ChangeGPRBreakpointLogFormat(int reg, const std::string &fmt) {
|
||||
size_t bp = FindGPRBreakpoint(reg);
|
||||
if (bp != INVALID_GPR_BREAKPOINT) {
|
||||
gprBreakpoints_[bp].logFormat = fmt;
|
||||
Update(INVALID_ADDRESS);
|
||||
}
|
||||
}
|
||||
|
||||
bool BreakpointManager::IsGPRBreakpoint(int reg) {
|
||||
return (gprBreakpointMask_ & (1u << reg)) != 0;
|
||||
}
|
||||
|
||||
bool BreakpointManager::GetGPRBreakpoint(int reg, GPRBreakpoint *check) {
|
||||
size_t bp = FindGPRBreakpoint(reg);
|
||||
if (bp != INVALID_GPR_BREAKPOINT) {
|
||||
*check = gprBreakpoints_[bp];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<GPRBreakpoint> BreakpointManager::GetGPRBreakpoints() {
|
||||
return gprBreakpoints_;
|
||||
}
|
||||
|
||||
BreakAction BreakpointManager::ExecGPRBreakpoint(int reg, u32 pc) {
|
||||
// Callers are expected to have already checked GetGPRBreakpointMask() themselves (that's
|
||||
// the whole point of exposing it - a single shift+and in the hot interpreter loop, skipping
|
||||
// a function call entirely in the overwhelmingly common no-breakpoint case), but check again
|
||||
// here too since this is also reachable directly.
|
||||
if ((gprBreakpointMask_ & (1u << reg)) == 0)
|
||||
return BREAK_ACTION_IGNORE;
|
||||
size_t bp = FindGPRBreakpoint(reg);
|
||||
if (bp == INVALID_GPR_BREAKPOINT)
|
||||
return BREAK_ACTION_IGNORE;
|
||||
|
||||
GPRBreakpoint &info = gprBreakpoints_[bp];
|
||||
if (info.result == BREAK_ACTION_IGNORE)
|
||||
return BREAK_ACTION_IGNORE;
|
||||
|
||||
if (info.hasCond && !info.cond.Evaluate())
|
||||
return BREAK_ACTION_IGNORE;
|
||||
|
||||
++info.numHits;
|
||||
|
||||
if (info.result & BREAK_ACTION_LOG) {
|
||||
if (info.logFormat.empty()) {
|
||||
NOTICE_LOG(Log::JIT, "BKP GPR write r%d, PC=%08x (%s)", reg, pc, g_symbolMap->GetDescription(pc).c_str());
|
||||
} else {
|
||||
std::string formatted;
|
||||
BreakpointManager::EvaluateLogFormat(currentDebugMIPS, info.logFormat, formatted);
|
||||
NOTICE_LOG(Log::JIT, "BKP GPR write r%d, PC=%08x: %s", reg, pc, formatted.c_str());
|
||||
}
|
||||
}
|
||||
if (info.result & BREAK_ACTION_PAUSE) {
|
||||
Core_Break(BreakReason::GPRBreakpoint, pc);
|
||||
}
|
||||
|
||||
return info.result;
|
||||
}
|
||||
|
||||
void BreakpointManager::SetSkipFirst(u32 pc) {
|
||||
breakSkipFirstAt_ = pc;
|
||||
breakSkipFirstTicks_ = CoreTiming::GetTicks(currentMIPS);
|
||||
|
||||
@@ -114,6 +114,30 @@ struct MemCheck {
|
||||
}
|
||||
};
|
||||
|
||||
// A breakpoint that trips whenever a general-purpose register is written to by an
|
||||
// instruction, regardless of address - identified by register index (0-31), not addr/range.
|
||||
// Interpreter-only for now (see RunUntilDowncountZeroWithChecks in MIPSTables.cpp) - the JITs
|
||||
// don't check this at all, so it has no effect unless running with the plain interpreter core.
|
||||
struct GPRBreakpoint {
|
||||
int reg = 0; // 0-31, general-purpose register index (matches OUT_RT/OUT_RD/OUT_RA fields).
|
||||
|
||||
BreakAction result = BREAK_ACTION_IGNORE;
|
||||
std::string logFormat;
|
||||
|
||||
bool hasCond = false;
|
||||
BreakPointCond cond;
|
||||
|
||||
u32 numHits = 0;
|
||||
|
||||
bool IsEnabled() const {
|
||||
return (result & BREAK_ACTION_PAUSE) != 0;
|
||||
}
|
||||
|
||||
bool operator == (const GPRBreakpoint &other) const {
|
||||
return reg == other.reg;
|
||||
}
|
||||
};
|
||||
|
||||
// BreakPoints cannot overlap, only one is allowed per address.
|
||||
// MemChecks can overlap, as long as their ends are different.
|
||||
// WARNING: MemChecks are not always tracked in HLE currently.
|
||||
@@ -121,6 +145,7 @@ class BreakpointManager {
|
||||
public:
|
||||
static const size_t INVALID_BREAKPOINT = -1;
|
||||
static const size_t INVALID_MEMCHECK = -1;
|
||||
static const size_t INVALID_GPR_BREAKPOINT = -1;
|
||||
|
||||
bool IsAddressBreakPoint(u32 addr);
|
||||
bool IsAddressBreakPoint(u32 addr, bool* enabled);
|
||||
@@ -158,6 +183,27 @@ public:
|
||||
BreakAction ExecMemCheck(u32 address, bool write, int size, u32 pc, const char *reason);
|
||||
BreakAction ExecOpMemCheck(u32 address, u32 pc);
|
||||
|
||||
// GPR write breakpoints - see GPRBreakpoint above. reg is a 0-31 GPR index.
|
||||
int AddGPRBreakpoint(int reg); // Returns the breakpoint index.
|
||||
void RemoveGPRBreakpoint(int reg);
|
||||
void ChangeGPRBreakpoint(int reg, bool enable);
|
||||
void ChangeGPRBreakpoint(int reg, BreakAction result);
|
||||
void ClearAllGPRBreakpoints();
|
||||
|
||||
void ChangeGPRBreakpointAddCond(int reg, const BreakPointCond &cond);
|
||||
void ChangeGPRBreakpointRemoveCond(int reg);
|
||||
BreakPointCond *GetGPRBreakpointCondition(int reg);
|
||||
|
||||
void ChangeGPRBreakpointLogFormat(int reg, const std::string &fmt);
|
||||
|
||||
bool IsGPRBreakpoint(int reg);
|
||||
bool GetGPRBreakpoint(int reg, GPRBreakpoint *bp);
|
||||
std::vector<GPRBreakpoint> GetGPRBreakpoints();
|
||||
|
||||
// Called from the interpreter (RunUntilDowncountZeroWithChecks) right before executing an
|
||||
// instruction that would write to reg - does not itself execute the instruction.
|
||||
BreakAction ExecGPRBreakpoint(int reg, u32 pc);
|
||||
|
||||
void SetSkipFirst(u32 pc);
|
||||
u32 CheckSkipFirst();
|
||||
|
||||
@@ -182,6 +228,15 @@ public:
|
||||
bool HasMemChecks() const {
|
||||
return anyMemChecks_;
|
||||
}
|
||||
bool HasGPRBreakpoints() const {
|
||||
return gprBreakpointMask_ != 0;
|
||||
}
|
||||
// Bit i set means register i has an active (non-ignored) GPR breakpoint - a cheap way for
|
||||
// the interpreter's hot per-instruction loop to test "would this write trip anything" with
|
||||
// a single shift+and, without touching gprBreakpoints_ at all in the common no-match case.
|
||||
u32 GetGPRBreakpointMask() const {
|
||||
return gprBreakpointMask_;
|
||||
}
|
||||
|
||||
void Frame();
|
||||
|
||||
@@ -200,9 +255,12 @@ private:
|
||||
// Finds a memcheck covering (part of) a range, unlike FindMemCheck() above.
|
||||
MemCheck *FindMemCheckInRange(u32 address, int size);
|
||||
void UpdateCachedMemCheckRanges();
|
||||
size_t FindGPRBreakpoint(int reg);
|
||||
void RecomputeGPRBreakpointMask();
|
||||
|
||||
std::atomic<bool> anyBreakPoints_;
|
||||
std::atomic<bool> anyMemChecks_;
|
||||
std::atomic<u32> gprBreakpointMask_;
|
||||
|
||||
std::vector<BreakPoint> breakPoints_;
|
||||
u32 breakSkipFirstAt_ = 0;
|
||||
@@ -212,6 +270,8 @@ private:
|
||||
std::vector<MemCheck> memCheckRangesRead_;
|
||||
std::vector<MemCheck> memCheckRangesWrite_;
|
||||
|
||||
std::vector<GPRBreakpoint> gprBreakpoints_;
|
||||
|
||||
bool needsUpdate_ = true;
|
||||
u32 updateAddr_ = 0;
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,3 +30,8 @@ void WebSocketMemoryBreakpointAdd(DebuggerRequest &req);
|
||||
void WebSocketMemoryBreakpointUpdate(DebuggerRequest &req);
|
||||
void WebSocketMemoryBreakpointRemove(DebuggerRequest &req);
|
||||
void WebSocketMemoryBreakpointList(DebuggerRequest &req);
|
||||
|
||||
void WebSocketGPRBreakpointAdd(DebuggerRequest &req);
|
||||
void WebSocketGPRBreakpointUpdate(DebuggerRequest &req);
|
||||
void WebSocketGPRBreakpointRemove(DebuggerRequest &req);
|
||||
void WebSocketGPRBreakpointList(DebuggerRequest &req);
|
||||
|
||||
@@ -1142,9 +1142,24 @@ static void RunUntilDowncountZeroFast(MIPSState *mips) {
|
||||
}
|
||||
|
||||
#define _RS(op) ((op>>21) & 0x1F)
|
||||
// Returns the 0-31 GPR index an instruction is about to write, or -1 if it doesn't write a GPR.
|
||||
// OUT_RA is always $ra (31) - jal/bltzal/bgezal/etc. have no encoded destination register field,
|
||||
// unlike jalr (which uses OUT_RD, since its destination is chosen via the rd field).
|
||||
static inline int GetGPRWriteTarget(const MIPSInstruction *instr, MIPSOpcode op) {
|
||||
if (instr->flags & OUT_RT)
|
||||
return (op >> 16) & 0x1F;
|
||||
if (instr->flags & OUT_RD)
|
||||
return (op >> 11) & 0x1F;
|
||||
if (instr->flags & OUT_RA)
|
||||
return 31;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void RunUntilDowncountZeroWithChecks(MIPSState *mips, u64 globalTicks) {
|
||||
bool hasBPs = g_breakpoints.HasBreakPoints();
|
||||
bool hasMCs = g_breakpoints.HasMemChecks();
|
||||
// Bit i set means register i has an active GPR breakpoint - see GetGPRBreakpointMask().
|
||||
u32 gprBPMask = g_breakpoints.GetGPRBreakpointMask();
|
||||
while (mips->downcount >= 0 && coreState == CORE_RUNNING_CPU) {
|
||||
// Don't stop in a delay slot! Well, unless we hit a memcheck in one, of course.
|
||||
do {
|
||||
@@ -1181,6 +1196,15 @@ static void RunUntilDowncountZeroWithChecks(MIPSState *mips, u64 globalTicks) {
|
||||
if (coreState == CORE_STEPPING_CPU)
|
||||
break;
|
||||
}
|
||||
if (gprBPMask != 0 && (instr->flags & (OUT_RT | OUT_RD | OUT_RA)) != 0 && g_breakpoints.CheckSkipFirst() != mips->pc) {
|
||||
int gprTarget = GetGPRWriteTarget(instr, op);
|
||||
if (gprTarget >= 0 && (gprBPMask & (1u << gprTarget)) != 0) {
|
||||
g_breakpoints.ExecGPRBreakpoint(gprTarget, mips->pc);
|
||||
// If it tripped, bail without running - same convention as memchecks above.
|
||||
if (coreState == CORE_STEPPING_CPU)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool wasInDelaySlot = mips->inDelaySlot;
|
||||
Interpret(mips, instr, op);
|
||||
@@ -1204,7 +1228,7 @@ int MIPSInterpret_RunUntil(MIPSState *mips, u64 globalTicks) {
|
||||
CoreTiming::Advance(mips);
|
||||
|
||||
uint64_t ticksLeft = globalTicks - CoreTiming::GetTicks(mips);
|
||||
if (g_breakpoints.HasBreakPoints() || g_breakpoints.HasMemChecks() || ticksLeft <= mips->downcount) {
|
||||
if (g_breakpoints.HasBreakPoints() || g_breakpoints.HasMemChecks() || g_breakpoints.HasGPRBreakpoints() || ticksLeft <= mips->downcount) {
|
||||
RunUntilDowncountZeroWithChecks(mips, globalTicks);
|
||||
} else {
|
||||
RunUntilDowncountZeroFast(mips);
|
||||
|
||||
@@ -105,7 +105,7 @@ file - this is just an index.
|
||||
| Game/version | `game.reset`, `game.status`, `version` | `GameSubscriber.cpp` |
|
||||
| CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status`, `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` |
|
||||
| Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.nextHLE` | `SteppingSubscriber.cpp` |
|
||||
| Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list` | `BreakpointSubscriber.cpp` |
|
||||
| Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list`, `cpu.gprBreakpoint.add/update/remove/list` (break when a GPR is written to, by any instruction anywhere - interpreter-only, no effect under a JIT backend) | `BreakpointSubscriber.cpp` |
|
||||
| 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` |
|
||||
|
||||
Reference in New Issue
Block a user