Skip to content

Add windows native debug adapter#974

Merged
xusheng6 merged 23 commits intodevfrom
test_windows-native-adapter
Jan 23, 2026
Merged

Add windows native debug adapter#974
xusheng6 merged 23 commits intodevfrom
test_windows-native-adapter

Conversation

@xusheng6
Copy link
Member

Fixes #965

xusheng6 and others added 23 commits January 20, 2026 16:11
Create a new debug adapter that uses the native Windows Debug API
(CreateProcess with DEBUG_PROCESS, WaitForDebugEvent, ContinueDebugEvent,
etc.) for debugging Windows PE binaries.

Features:
- Process creation and attach support
- Software breakpoints using INT3 instruction
- Hardware breakpoints using debug registers (DR0-DR3)
- Register read/write for x86 and x64
- Memory read/write
- Module tracking (DLL load/unload events)
- Thread tracking (create/exit thread events)
- Single stepping support
- Exception handling

Fixes #965

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move process creation to debug thread (required by Windows Debug API)
- Add proper condition variable notification on Quit/Detach to prevent deadlock
- Add debug logging for troubleshooting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Shadow breakpoint bytes in ReadMemory for correct disassembly
- Fix step-over breakpoint logic for Go() and StepInto()
- Fix originalByte not being saved (add bp to vector before ApplyBreakpoint)
- Add Reset() function for proper state cleanup on restart
- Implement proper stack walking with StackWalk64
- Populate module name in stack frames

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add IsCallInstruction() to decode x86/x64 call opcodes
- Add GetReturnAddress() to read return address from stack
- Add SetTempBreakpoint()/RemoveTempBreakpoint() for one-time breakpoints
- StepOver: set temp breakpoint after call instruction, or single step
- StepReturn: set temp breakpoint at return address
- Handle temp breakpoint in exception handler
- Shadow temp breakpoint in ReadMemory

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Load dbghelp.dll via delay load instead of IAT to avoid conflicts with the
DbgEng adapter. A delay load hook tries to load from debugger.x64dbgEngPath
or the bundled dbgeng folder first, falling back to system32 if unavailable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When applying breakpoints, always read the original byte from target memory
instead of relying on previously stored values. This fixes issues where:
- Breakpoints added before debugging weren't properly applied
- Double-application could overwrite the saved original with INT3

The fix ensures ApplyBreakpoint reads from memory and only saves the byte
if it's not already an INT3, protecting against corruption.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Windows Native adapter now honors the debugger settings:
- If stopAtEntryPoint is true, adds a breakpoint at program entry point
- If stopAtSystemEntryPoint is false, continues past the initial breakpoint

This matches the behavior of the DbgEng adapter.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:
1. In Reset(), also clear originalByte to 0 since it's stale for a new process
2. In ApplyPendingBreakpoints(), re-apply existing breakpoints that have
   isActive=false (from previous debug session) before processing pending ones

This ensures breakpoints persist and work correctly across debug session restarts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two fixes for hardware breakpoints:

1. Step-over stuck issue: When a hardware breakpoint is hit, temporarily
   disable it in DR7, single-step past it, then re-enable. This prevents
   the CPU from immediately hitting the same breakpoint again on resume.

2. Restart issue: Re-apply inactive hardware breakpoints when a new debug
   session starts, similar to software breakpoints. Also fixed
   AddHardwareBreakpoint to re-apply existing inactive breakpoints.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Detect WOW64 processes using IsWow64Process and cache the result
- Use Wow64GetThreadContext/Wow64SetThreadContext with WOW64_CONTEXT
  for 32-bit processes running on 64-bit Windows
- Update register operations to use correct context type (eax vs rax, etc.)
- Handle STATUS_WX86_BREAKPOINT (0x4000001F) and STATUS_WX86_SINGLE_STEP
  (0x4000001E) exception codes used by WOW64 instead of the standard
  EXCEPTION_BREAKPOINT and EXCEPTION_SINGLE_STEP
- Update hardware breakpoint operations to use WOW64_CONTEXT
- Fix stack walking to use correct machine type and context for 32-bit
- Replace compile-time _WIN64 checks with runtime m_isTargetWow64 checks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
WOW64 processes have two system breakpoints: the 64-bit ntdll breakpoint
and a second one in 32-bit ntdll (LdrpDoDebuggerBreak). Add tracking for
the WOW64 initial breakpoint so it respects the stopAtSystemEntryPoint
setting instead of causing an unexpected stop.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use GetFramesOfThread (StackWalk64) to get the return address reliably
instead of just reading from the stack pointer. This handles functions
with frame pointer omission, different calling conventions, and optimized
code. Falls back to simple stack read if unwinding fails.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix Attach() to use the pid parameter directly instead of reading from
  settings, which would be 0 since the adapter doesn't exist yet when the
  PID is set via the debugger state
- Always stop at the initial breakpoint when attaching to a running process,
  regardless of stopAtSystemEntryPoint setting (that setting only applies
  when launching a new process)
- Add detailed logging to ApplyBreakpoint to help diagnose breakpoint
  application failures (logs memory read, protection change, and INT3 write
  failures with error codes)
- Post ResumeEventType event in Go() so the UI shows the target as running
- Post StepIntoEventType event in StepInto() so the UI properly updates

These fixes enable proper attach workflow and ensure the UI correctly reflects
the target's running state.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When stepping over a software breakpoint, suspend all other threads before
removing the INT3 instruction, then resume them after re-applying it.

This prevents race conditions where:
- Another thread could execute the breakpoint location while INT3 is removed
- Temporary breakpoints might not be removed properly
- INT3 breakpoints might not be restored before resuming

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add GetProcessCommandLine() helper that uses NtQueryInformationProcess with
ProcessCommandLineInformation (available since Windows 8.1). This is cleaner
than manually reading the PEB and only requires PROCESS_QUERY_LIMITED_INFORMATION
rights, allowing it to work on more processes.

Fallback to just the executable name if:
- Cannot open the process handle (e.g., protected processes)
- NtQueryInformationProcess fails
- System processes (PID 0 or 4)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When stepping (StepInto, StepOver, StepReturn), suspend all other threads
to prevent them from hitting breakpoints unexpectedly. Only the thread being
stepped executes. When using Go() without a temp breakpoint, all threads run
freely.

This implements behavior similar to GDB's "scheduler-locking step" mode:
- StepInto: Only current thread runs
- StepOver/StepReturn: Only current thread runs (via temp breakpoint + Go)
- Go() from user: All threads run freely
- When step completes: Resume all threads

Fixes the issue where stepping one thread would unexpectedly stop because
another thread hit a breakpoint.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When stopped at a breakpoint location (e.g., stepped onto it via single-step),
the INT3 instruction is still in memory. When StepInto() or Go() tries to step
over the breakpoint, it sets the trap flag but the INT3 is still there.

Result: CPU executes and immediately hits the INT3, causing a BREAKPOINT
exception instead of single-stepping the actual instruction. This required TWO
steps to advance past the breakpoint:
1. First step: Hit INT3, remove it, set IP back - stop at same location
2. Second step: Actually execute the instruction and advance

Fix: Call RemoveBreakpointInternal() before setting trap flag in both
StepInto() and Go(). Now one step executes exactly one instruction.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…kpoint thread suspension

When StepOver() steps over a call with a breakpoint, it:
1. Sets temp breakpoint at return address (m_hasTempBreakpoint = true)
2. Calls Go()
3. Go() was suspending ALL other threads for the entire function execution
4. If the function waits on another thread or acquires a lock held by suspended thread → DEADLOCK

The bug: Go() suspended threads when m_hasTempBreakpoint was set, keeping them
suspended while the entire function executes, not just for a single instruction.

The fix: Remove thread suspension for temp breakpoints (StepOver/StepReturn).
Scheduler-locking should only apply to StepInto() which executes one instruction.
StepOver/StepReturn internally do a "continue" operation, so all threads should
run normally. If another thread hits a breakpoint during StepOver, the debugger
stops - this is expected behavior, matching GDB and other debuggers.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Previously, all exceptions including EXCEPTION_INT_DIVIDE_BY_ZERO and
EXCEPTION_FLT_DIVIDE_BY_ZERO were incorrectly mapped to AccessViolation.
This caused the test_exception_divzero test to fail.

Now properly categorize exceptions:
- Calculation exceptions (divide by zero, overflow, FPU errors) → Calculation
- Illegal instruction exceptions → IllegalInstruction
- Memory access violations and fatal exceptions → AccessViolation

This matches the expected behavior where divide-by-zero should report
DebugStopReason.Calculation, not AccessViolation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added a new "common.verboseLogging" setting (defaults to false) to control
extensive debug output from the Windows Native adapter.

Changes:
- Added LogVerbose() template method that checks m_verboseLogging flag
- Registered "common.verboseLogging" boolean setting in adapter settings
- Replaced verbose LogWarn/LogDebug calls with LogVerbose()
- Kept actual warnings (errors, failures) as LogWarn

When verbose logging is disabled (default), the adapter only logs errors and
warnings. When enabled, it logs detailed information about debug events,
process creation, module loading, exception handling, etc.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add debugger.defaultWindowsAdapter setting to allow users to choose
between WINDOWS_NATIVE and DBGENG adapters. The setting defaults to
WINDOWS_NATIVE.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@xusheng6 xusheng6 merged commit ecacb01 into dev Jan 23, 2026
@xusheng6 xusheng6 deleted the test_windows-native-adapter branch January 23, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create a native Windows debug adapter

1 participant