Move the temporary breakpoint out of the user's breakpoint list

step-over, step-out and run-until plant a one-shot breakpoint at the address
they want execution to return to. Keeping it in breakPoints_ alongside the
user's own meant the two kept colliding:

- Adding a log-only user breakpoint at the same address hijacked the temporary
  one. AddBreakPoint() didn't match across temp-ness so both existed, and then
  ChangeBreakPoint() looked up "the first enabled breakpoint at this address" -
  a log-only breakpoint isn't enabled, so the temporary one won and had its
  action overwritten to log-only. It lost PAUSE and the step never came back.
- RemoveBreakPoint() erased up to two entries per address to catch an
  overlapping temporary one, so deleting either deleted both - including the
  interpreter's cleanup path in CheckExecBreakpoints() taking the user's
  breakpoint with it.
- ExecBreakPoint() handled one breakpoint per address, so with both at the same
  address only one of them did anything: the step completed but the user's log
  line never printed.
- Nothing dropped it when something *else* stopped us first, so an interrupted
  step left a breakpoint armed at an address nobody was waiting for anymore,
  which later fired as a phantom stop.

It's a single TempBreakPoint member now, invisible to the breakpoint lists and
untouched by user edits. One is enough: step over/out and cross-thread step into
all require the CPU to already be stepping and resume it immediately, so only
one can be in flight, and run-until now replaces rather than stacking (two
pending run-untils had no coherent meaning, and the loser stayed armed).

Behavior follows what other debuggers do. Both breakpoints at an address are
evaluated independently and their actions combine, so a log-only breakpoint
logs without stopping and still lets the step finish. Core_Break() drops the
temporary breakpoint on any stop, whatever the reason - the same way gdb deletes
its step-resume breakpoint and lldb discards the thread plan.

Two things to be careful of, both covered by the new TempBreakpoints test:
HasBreakPoints() has to account for it, or the interpreter's checked run loop
and the JIT skip breakpoint checking entirely and a step with no user
breakpoints set never returns; and IsAddressBreakPoint() (user-facing, for the
lists and disassembly markers) is now separate from NeedsBreakCheckAt() (what
the JIT frontends and interpreter ask), since only the latter should see it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-08-17 00:29:26 +02:00
1 parent 0c510ca62e
commit 35a91b757a
16 files changed
+258 -119

No files matched your search

+13 -10
View File
@@ -45,7 +45,7 @@ struct WebSocketSteppingState : public DebuggerSubscriber {
protected:
uint32_t GetNextAddress(DebugInterface *cpuDebug);
void PrepareResume();
void AddThreadCondition(uint32_t breakpointAddress, uint32_t threadID);
void AddThreadCondition(uint32_t threadID);
};
DebuggerSubscriber *WebSocketSteppingInit(DebuggerEventHandlerMap &map) {
@@ -127,8 +127,8 @@ void WebSocketSteppingState::Into(DebuggerRequest &req) {
// Note: we need to get cpuDebug again anyway (in case we ran some HLE above.)
cpuDebug = CPUFromRequest(req);
if (cpuDebug != currentDebugMIPS) {
g_breakpoints.AddBreakPoint(breakpointAddress, true);
AddThreadCondition(breakpointAddress, threadID);
g_breakpoints.SetTempBreakPoint(breakpointAddress);
AddThreadCondition(threadID);
Core_Resume();
}
}
@@ -184,9 +184,9 @@ void WebSocketSteppingState::Over(DebuggerRequest &req) {
// Could have advanced to the breakpoint already in PrepareResume().
cpuDebug = CPUFromRequest(req);
if (cpuDebug->GetPC() != breakpointAddress) {
g_breakpoints.AddBreakPoint(breakpointAddress, true);
g_breakpoints.SetTempBreakPoint(breakpointAddress);
if (cpuDebug != currentDebugMIPS)
AddThreadCondition(breakpointAddress, threadID);
AddThreadCondition(threadID);
Core_Resume();
}
});
@@ -237,9 +237,9 @@ void WebSocketSteppingState::Out(DebuggerRequest &req) {
// Could have advanced to the breakpoint already in PrepareResume().
cpuDebug = CPUFromRequest(req);
if (cpuDebug->GetPC() != breakpointAddress) {
g_breakpoints.AddBreakPoint(breakpointAddress, true);
g_breakpoints.SetTempBreakPoint(breakpointAddress);
if (cpuDebug != currentDebugMIPS)
AddThreadCondition(breakpointAddress, threadID);
AddThreadCondition(threadID);
Core_Resume();
}
});
@@ -269,7 +269,7 @@ void WebSocketSteppingState::RunUntil(DebuggerRequest &req) {
PrepareResume();
// We may have arrived already if PauseResume() stepped out of a delay slot.
if (currentMIPS->pc != address || wasAtAddress) {
g_breakpoints.AddBreakPoint(address, true);
g_breakpoints.SetTempBreakPoint(address);
Core_Resume();
}
});
@@ -325,10 +325,13 @@ void WebSocketSteppingState::PrepareResume() {
}
}
void WebSocketSteppingState::AddThreadCondition(uint32_t breakpointAddress, uint32_t threadID) {
// Restricts the temporary breakpoint a step just planted to the thread the step was requested
// for, so an unrelated thread running through the same address doesn't complete someone else's
// step. Must be called right after SetTempBreakPoint().
void WebSocketSteppingState::AddThreadCondition(uint32_t threadID) {
BreakPointCond cond;
cond.debug = currentDebugMIPS;
cond.expressionString = StringFromFormat("threadid == 0x%08x", threadID);
if (initExpression(currentDebugMIPS, cond.expressionString.c_str(), cond.expression))
g_breakpoints.ChangeBreakPointAddCond(breakpointAddress, cond);
g_breakpoints.SetTempBreakPointCond(cond);
}