DebugLoop: continue past a program's DebugBreak()/int 3 like Visual Studio

A program-generated breakpoint exception (DebugBreak(), __debugbreak() or an
inline `int 3`) that is not in the engine's breakpoint table is currently
answered with DBG_EXCEPTION_NOT_HANDLED in the "breakpoint not in list" branch.
For a real single-byte int 3 the saved RIP already points past the instruction,
so passing it back to the debuggee re-raises it there: the program's own SEH
handler runs (or it becomes a second-chance/unhandled stop) and the debugger
cannot simply continue past the program's DebugBreak() the way Visual Studio
does (VS consumes it with DBG_CONTINUE and resumes at the next instruction).

This went through two iterations:

1. First attempt answered DBG_CONTINUE for every stray breakpoint exception.
   That fixed DebugBreak() but regressed RaiseException(STATUS_BREAKPOINT):
   the exception was consumed and the debuggee's __except handler no longer
   ran.

2. Final version distinguishes a genuine single-byte int 3 from a programmatic
   raise by reading the byte at ExceptionRecord.ExceptionAddress:
   - 0xCC (a real int 3 instruction the program executed): DBG_CONTINUE, so the
     thread resumes at the instruction after the int 3 and the debugger can
     continue past the program's own DebugBreak() (no register surgery needed -
     the CPU already advanced RIP; the Rip -= BreakPointSize rewind only applies
     to breakpoints the engine itself patched in).
   - otherwise (e.g. RaiseException(STATUS_BREAKPOINT)): DBG_EXCEPTION_NOT_HANDLED
     is kept so the debuggee's SEH still runs.

Every other exception path is untouched (debugger-set breakpoints, the system
breakpoint, single-stepping, hardware/memory breakpoints, AV/div-zero etc.).

Verified on Windows x64 with a 21-case debugger test suite: DebugBreak(),
__debugbreak(), DebugBreak() with an SEH handler and
RaiseException(STATUS_BREAKPOINT)+__except all behave correctly, and the suite
shows no regressions. Related-but-distinct step-over-int3 cleanup was addressed
separately in PR #32 / issue #33.
This commit is contained in:
linsmod 2026-08-02 00:07:37 +08:00
parent ccac889f27
commit 7c07601581
1 changed files with 16 additions and 2 deletions

View File

@ -694,8 +694,22 @@ __declspec(dllexport) void TITCALL DebugLoop()
}
else
{
//not a recently deleted breakpoint - pass to debuggee
DBGCode = DBG_EXCEPTION_NOT_HANDLED;
// A program-generated breakpoint exception not raised by a debugger-set
// breakpoint. Distinguish a real single-byte `int 3` instruction
// (DebugBreak()/__debugbreak(): the byte at ExceptionAddress is 0xCC and
// the CPU already advanced RIP past it) from a programmatic exception
// such as RaiseException(STATUS_BREAKPOINT) (no 0xCC at that address).
//
// - real int 3: consume it (DBG_CONTINUE) like Visual Studio, so the
// thread resumes at the instruction after the int 3 and execution
// continues past the program's own DebugBreak.
// - otherwise: pass it to the debuggee (DBG_EXCEPTION_NOT_HANDLED) so
// the program's SEH handlers run (e.g. RaiseException+__except).
unsigned char opcode = 0;
bool isInt3 = MemoryReadSafe(dbgProcessInformation.hProcess,
(LPVOID)exceptionAddress, &opcode, 1, 0) &&
opcode == 0xCC;
DBGCode = isInt3 ? DBG_CONTINUE : DBG_EXCEPTION_NOT_HANDLED;
if(DBGCustomHandler->chBreakPoint != NULL)
{
myCustomHandler = (fCustomHandler)((LPVOID)DBGCustomHandler->chBreakPoint);