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:
Henrik Rydgård
2026-08-13 11:35:29 +02:00
co-authored by Claude Sonnet 5
parent 3f6743d40d
commit 75174af77b
8 changed files with 500 additions and 2 deletions
+164
View File
@@ -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);