From 7c076015814f8ab5c90d6c136550c3c8c1ac0c79 Mon Sep 17 00:00:00 2001 From: linsmod Date: Sun, 2 Aug 2026 00:07:37 +0800 Subject: [PATCH] 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. --- TitanEngine/TitanEngine.Debugger.DebugLoop.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp b/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp index 9f92bd0..a58fa1d 100644 --- a/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp +++ b/TitanEngine/TitanEngine.Debugger.DebugLoop.cpp @@ -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);