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.
Two fixes from PR review of the thread-exit reset-recovery:
- When restoring the hardware breakpoint after the stepped-over thread exits, rebuild
each still-suspended thread's debug registers from the authoritative DebugRegister[]
table (re-arming every enabled slot, as CREATE_THREAD does) instead of from the
exiting thread's now-gone context. The old code rebuilt DR7 from GetContextData()
(the dead thread), writing a DR7 with only the restored slot to every thread and
silently disabling any other hardware breakpoint. The restore is also moved before
the threads are resumed, since SetThreadContext does not reliably update the debug
registers of a running thread.
- Erase the per-thread single-step state in the EXIT_THREAD handler. A thread can exit
after StepInto() armed its step but before the trap event arrives; leaving the entry
behind means a reused thread id inherits it (StepInto declines to arm, or a later
single-step fires the dead thread's callback).
If a thread is terminated (e.g. by ExitProcess) while it is in the middle of a
breakpoint step-over - its breakpoint temporarily removed and a restore pending on
its next single-step - the EXIT_THREAD handler cleared ThreadBeingProcessed but left
the reset state (ResetBPX/ResetHwBPX/ResetMemBPX) armed and the breakpoint removed.
The next thread's event then consumed that orphaned reset and was misinterpreted as a
step-over reset, producing a stray single-step that was passed to the debuggee
unhandled and crashed it. This was most visible with hardware breakpoints under
multi-thread contention. Complete the pending restore when the thread-being-processed
exits.
- Replaced the implicit bit-shift logic with an explicit mapping
to prevent the OS from silently duplicating pages via Copy-on-Write.
- Added explicit cases for PAGE_WRITECOPY and PAGE_EXECUTE_WRITECOPY.