]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commit
Windows gdb: Avoid writing debug registers if watchpoint hit pending
authorPedro Alves <pedro@palves.net>
Tue, 7 May 2024 19:41:37 +0000 (20:41 +0100)
committerPedro Alves <pedro@palves.net>
Mon, 9 Jun 2025 17:09:15 +0000 (18:09 +0100)
commit2ab626c01bb1629abb02b8596b6f90fa95f9f7ee
treef475c6b65d9cf1f16e4e98b5cf0b8c157e51fe4f
parent02fda092e5d43609a8dbcb51a6be4c58e7cc9327
Windows gdb: Avoid writing debug registers if watchpoint hit pending

Several watchpoint-related testcases, such as
gdb.threads/watchthreads.exp for example, when tested with the backend
in non-stop mode, exposed an interesting detail of the Windows debug
API that wasn't considered before.  The symptom observed is spurious
SIGTRAPs, like:

  Thread 1 "watchthreads" received signal SIGTRAP, Trace/breakpoint trap.
  0x00000001004010b1 in main () at .../src/gdb/testsuite/gdb.threads/watchthreads.c:48
  48              args[i] = 1; usleep (1); /* Init value.  */

After a good amount of staring at logs and headscratching, I realized
the problem:

 #0 - It all starts with the fact that multiple threads can hit an
      event at the same time.  Say, a watchpoint for thread A, and a
      breakpoint for thread B.

 #1 - Say, WaitForDebugEvent reports the breakpoint hit for thread B
      first, then GDB for some reason decides to update debug
      registers, and continue.  Updating debug registers means writing
      the debug registers to _all_ threads, with SetThreadContext.

 #2 - WaitForDebugEvent reports the watchpoint hit for thread A.
      Watchpoint hits are reported as EXCEPTION_SINGLE_STEP.

 #3 - windows-nat checks the Dr6 debug register to check if the step
      was a watchpoint or hardware breakpoint stop, and finds that Dr6
      is completely cleared.  So windows-nat reports a plain SIGTRAP
      (given EXCEPTION_SINGLE_STEP) to the core.

 #4 - Thread A was not supposed to be stepping, so infrun reports the
      SIGTRAP to the user as a random signal.

The strange part is #3 above.  Why was Dr6 cleared?

Turns out that (at least in Windows 10 & 11), writing to _any_ debug
register has the side effect of clearing Dr6, even if you write the
same values the registers already had, back to the registers.

I confirmed it clearly by adding this hack to GDB:

  if (th->context.ContextFlags == 0)
    {
      th->context.ContextFlags = CONTEXT_DEBUGGER_DR;

      /* Get current values of debug registers.  */
      CHECK (GetThreadContext (th->h, &th->context));

      DEBUG_EVENTS ("For 0x%x (once),  Dr6=0x%llx", th->tid, th->context.Dr6);

      /* Write debug registers back to thread, same values,
 and re-read them.  */
      CHECK (SetThreadContext (th->h, &th->context));
      CHECK (GetThreadContext (th->h, &th->context));

      DEBUG_EVENTS ("For 0x%x (twice), Dr6=0x%llx", th->tid, th->context.Dr6);
    }

Which showed Dr6=0 after the write + re-read:

  [windows events] fill_thread_context: For 0x6a0 (once),  Dr6=0xffff0ff1
  [windows events] fill_thread_context: For 0x6a0 (twice), Dr6=0x0

This commit fixes the issue by detecting that a thread has a pending
watchpoint hit to report (Dr6 has interesting bits set), and if so,
avoid mofiying any debug register.  Instead, let the pending
watchpoint hit be reported by WaitForDebugEvent.  If infrun did want
to modify watchpoints, it will still be done when the thread is
eventually re-resumed after the pending watchpoint hit is reported.
(infrun knows how to gracefully handle the case of a watchpoint hit
for a watchpoint that has since been deleted.)

Change-Id: I21a3daa9e34eecfa054f0fea706e5ab40aabe70a
gdb/nat/windows-nat.h
gdb/windows-nat.c
gdbserver/win32-low.cc
gdbserver/win32-low.h