public inbox for gdb-patches@sourceware.org
 help / color / mirror / Atom feed
From: Pedro Alves <pedro@palves.net>
To: gdb-patches@sourceware.org
Subject: [PATCH 30/34] windows-nat.c: Avoid writing debug registers if watchpoint hit pending
Date: Wed,  8 May 2024 00:42:29 +0100	[thread overview]
Message-ID: <20240507234233.371123-31-pedro@palves.net> (raw)
In-Reply-To: <20240507234233.371123-1-pedro@palves.net>

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 in 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 what (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  |   7 +++
 gdb/windows-nat.c      | 106 ++++++++++++++++++++++++++++++-----------
 gdbserver/win32-low.cc |   8 ++++
 gdbserver/win32-low.h  |   2 +
 4 files changed, 96 insertions(+), 27 deletions(-)

diff --git a/gdb/nat/windows-nat.h b/gdb/nat/windows-nat.h
index 6283ff0a4ee..2efb54e1ce7 100644
--- a/gdb/nat/windows-nat.h
+++ b/gdb/nat/windows-nat.h
@@ -205,6 +205,13 @@ struct windows_process_info
 
   virtual bool handle_access_violation (const EXCEPTION_RECORD *rec) = 0;
 
+  /* Fill in the thread's CONTEXT/WOW64_CONTEXT, if it wasn't filled
+     in yet.
+
+     This function must be supplied by the embedding application.  */
+
+  virtual void fill_thread_context (windows_thread_info *th) = 0;
+
   handle_exception_result handle_exception
       (DEBUG_EVENT &current_event,
        struct target_waitstatus *ourstatus, bool debug_exceptions);
diff --git a/gdb/windows-nat.c b/gdb/windows-nat.c
index 5500d8b8c87..73a6237dfa7 100644
--- a/gdb/windows-nat.c
+++ b/gdb/windows-nat.c
@@ -114,6 +114,7 @@ struct windows_per_inferior : public windows_process_info
   bool handle_access_violation (const EXCEPTION_RECORD *rec) override;
 
   void invalidate_context (windows_thread_info *th);
+  void fill_thread_context (windows_thread_info *th) override;
 
   void continue_one_thread (windows_thread_info *th,
 			    windows_continue_flags cont_flags);
@@ -746,17 +747,10 @@ windows_fetch_one_register (struct regcache *regcache,
 }
 
 void
-windows_nat_target::fetch_registers (struct regcache *regcache, int r)
+windows_per_inferior::fill_thread_context (windows_thread_info *th)
 {
-  windows_thread_info *th = windows_process.find_thread (regcache->ptid ());
-
-  /* Check if TH exists.  Windows sometimes uses a non-existent
-     thread id in its events.  */
-  if (th == NULL)
-    return;
-
 #ifdef __x86_64__
-  if (windows_process.wow64_process)
+  if (wow64_process)
     {
       if (th->wow64_context.ContextFlags == 0)
 	{
@@ -773,6 +767,19 @@ windows_nat_target::fetch_registers (struct regcache *regcache, int r)
 	  CHECK (GetThreadContext (th->h, &th->context));
 	}
     }
+}
+
+void
+windows_nat_target::fetch_registers (struct regcache *regcache, int r)
+{
+  windows_thread_info *th = windows_process.find_thread (regcache->ptid ());
+
+  /* Check if TH exists.  Windows sometimes uses a non-existent
+     thread id in its events.  */
+  if (th == nullptr)
+    return;
+
+  windows_process.fill_thread_context (th);
 
   if (r < 0)
     for (r = 0; r < gdbarch_num_regs (regcache->arch()); r++)
@@ -1291,36 +1298,81 @@ windows_per_inferior::continue_one_thread (windows_thread_info *th,
   DWORD &context_flags_ref = (wow64_process
 			      ? th->wow64_context.ContextFlags
 			      : th->context.ContextFlags);
+  const DWORD64 dr6 = (wow64_process
+		       ? th->wow64_context.Dr6
+		       : th->context.Dr6);
 #else
   DWORD &context_flags_ref = th->context.ContextFlags;
+  const DWORD dr6 = th->context.Dr6;
 #endif
 
   if (th->debug_registers_changed)
     {
-      context_flags_ref |= CONTEXT_DEBUG_REGISTERS;
-#ifdef __x86_64__
-      if (wow64_process)
+      windows_process.fill_thread_context (th);
+
+      gdb_assert ((context_flags_ref & CONTEXT_DEBUG_REGISTERS) != 0);
+
+      /* Check whether the thread has Dr6 set indicating a
+	 watchpoint hit, and we haven't seen the watchpoint event
+	 yet (reported as
+	 EXCEPTION_SINGLE_STEP/STATUS_WX86_SINGLE_STEP).  In that
+	 case, don't change the debug registers.  Changing debug
+	 registers, even if to the same values, makes the kernel
+	 clear Dr6.  The result would be we would lose the
+	 unreported watchpoint hit.  */
+      if ((dr6 & ~DR6_CLEAR_VALUE) != 0)
 	{
-	  th->wow64_context.Dr0 = state->dr_mirror[0];
-	  th->wow64_context.Dr1 = state->dr_mirror[1];
-	  th->wow64_context.Dr2 = state->dr_mirror[2];
-	  th->wow64_context.Dr3 = state->dr_mirror[3];
-	  th->wow64_context.Dr6 = DR6_CLEAR_VALUE;
-	  th->wow64_context.Dr7 = state->dr_control_mirror;
+	  if (th->last_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT
+	      && (th->last_event.u.Exception.ExceptionRecord.ExceptionCode
+		  == EXCEPTION_SINGLE_STEP))
+	    {
+	      DEBUG_EVENTS ("0x%x already reported watchpoint", th->tid);
+	    }
+	  else
+	    {
+	      DEBUG_EVENTS ("0x%x last reported something else (0x%x)",
+			    th->tid,
+			    th->last_event.dwDebugEventCode);
+
+	      /* Don't touch debug registers.  Let the pending
+		 watchpoint event be reported instead.  We will
+		 update the debug registers later when the thread
+		 is re-resumed by the core after the watchpoint
+		 event.  */
+	      context_flags_ref &= ~CONTEXT_DEBUG_REGISTERS;
+	    }
 	}
       else
-#endif
+	DEBUG_EVENTS ("0x%x has no dr6 set", th->tid);
+
+      if ((context_flags_ref & CONTEXT_DEBUG_REGISTERS) != 0)
 	{
-	  th->context.Dr0 = state->dr_mirror[0];
-	  th->context.Dr1 = state->dr_mirror[1];
-	  th->context.Dr2 = state->dr_mirror[2];
-	  th->context.Dr3 = state->dr_mirror[3];
-	  th->context.Dr6 = DR6_CLEAR_VALUE;
-	  th->context.Dr7 = state->dr_control_mirror;
-	}
+	  DEBUG_EVENTS ("0x%x changing dregs", th->tid);
+#ifdef __x86_64__
+	  if (wow64_process)
+	    {
+	      th->wow64_context.Dr0 = state->dr_mirror[0];
+	      th->wow64_context.Dr1 = state->dr_mirror[1];
+	      th->wow64_context.Dr2 = state->dr_mirror[2];
+	      th->wow64_context.Dr3 = state->dr_mirror[3];
+	      th->wow64_context.Dr6 = DR6_CLEAR_VALUE;
+	      th->wow64_context.Dr7 = state->dr_control_mirror;
+	    }
+	  else
+#endif
+	    {
+	      th->context.Dr0 = state->dr_mirror[0];
+	      th->context.Dr1 = state->dr_mirror[1];
+	      th->context.Dr2 = state->dr_mirror[2];
+	      th->context.Dr3 = state->dr_mirror[3];
+	      th->context.Dr6 = DR6_CLEAR_VALUE;
+	      th->context.Dr7 = state->dr_control_mirror;
+	    }
 
-      th->debug_registers_changed = false;
+	  th->debug_registers_changed = false;
+	}
     }
+
   if (context_flags_ref != 0)
     {
       DWORD ec = 0;
diff --git a/gdbserver/win32-low.cc b/gdbserver/win32-low.cc
index 004bf94c83a..65b01dc97ac 100644
--- a/gdbserver/win32-low.cc
+++ b/gdbserver/win32-low.cc
@@ -141,6 +141,14 @@ win32_require_context (windows_thread_info *th)
 
 /* See nat/windows-nat.h.  */
 
+void
+gdbserver_windows_process::fill_thread_context (windows_thread_info *th)
+{
+  win32_require_context (th);
+}
+
+/* See nat/windows-nat.h.  */
+
 windows_thread_info *
 gdbserver_windows_process::find_thread (ptid_t ptid)
 {
diff --git a/gdbserver/win32-low.h b/gdbserver/win32-low.h
index e99e47ea829..ea2a9b4c5b6 100644
--- a/gdbserver/win32-low.h
+++ b/gdbserver/win32-low.h
@@ -181,6 +181,8 @@ struct gdbserver_windows_process : public windows_nat::windows_process_info
   void handle_unload_dll (const DEBUG_EVENT &current_event) override;
   bool handle_access_violation (const EXCEPTION_RECORD *rec) override;
 
+  void fill_thread_context (windows_nat::windows_thread_info *th) override;
+
   int attaching = 0;
 
   /* A status that hasn't been reported to the core yet, and so
-- 
2.43.2


  parent reply	other threads:[~2024-05-07 23:44 UTC|newest]

Thread overview: 64+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-05-07 23:41 [PATCH 00/34] Windows non-stop mode Pedro Alves
2024-05-07 23:42 ` [PATCH 01/34] Windows gdb: Dead code in windows_nat_target::do_initial_windows_stuff Pedro Alves
2024-05-08 14:39   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 02/34] Windows gdb: Eliminate global current_process.dr[8] global Pedro Alves
2024-05-08 15:02   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 03/34] Windows gdb+gdbserver: New find_thread, replaces thread_rec(DONT_INVALIDATE_CONTEXT) Pedro Alves
2024-05-08 15:03   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 04/34] Windows gdb: handle_output_debug_string return type Pedro Alves
2024-05-08 14:43   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 05/34] Windows gdb: Eliminate reload_context Pedro Alves
2024-05-08 14:45   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 06/34] Windows gdb+gdbserver: Eliminate thread_rec(INVALIDATE_CONTEXT) calls Pedro Alves
2024-05-08 15:08   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 07/34] Windows gdb+gdbserver: Eliminate DONT_SUSPEND Pedro Alves
2024-05-08 15:12   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 08/34] Windows gdb+gdbserver: Eliminate windows_process_info::thread_rec Pedro Alves
2024-05-08 15:12   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 09/34] Windows gdb: Simplify windows_nat_target::wait Pedro Alves
2024-05-07 23:42 ` [PATCH 10/34] Windows gdb+gdbserver: Move suspending thread to when returning event Pedro Alves
2024-05-07 23:42 ` [PATCH 11/34] Windows gdb: Introduce continue_last_debug_event_main_thread Pedro Alves
2024-05-08 14:53   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 12/34] Windows gdb: Introduce windows_continue_flags Pedro Alves
2024-05-08 15:16   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 13/34] Windows gdb: Factor code out of windows_nat_target::windows_continue Pedro Alves
2024-05-08 15:18   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 14/34] Windows gdb: Pending stop and current_event Pedro Alves
2024-05-08 15:18   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 15/34] Windows gdb+gdbserver: Elim desired_stop_thread_id / rework pending_stops Pedro Alves
2024-05-07 23:42 ` [PATCH 16/34] Windows gdb+gdbserver: Introduce get_last_debug_event_ptid Pedro Alves
2024-05-07 23:42 ` [PATCH 17/34] Windows gdb: Can't pass signal to thread other than last stopped thread Pedro Alves
2024-05-07 23:42 ` [PATCH 18/34] Windows gdbserver: Fix scheduler-locking Pedro Alves
2024-05-07 23:42 ` [PATCH 19/34] Windows gdb: Enable "set scheduler-locking on" Pedro Alves
2024-05-08 15:25   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 20/34] Windows gdbserver: Eliminate soft-interrupt mechanism Pedro Alves
2024-05-08 15:26   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 21/34] Windows gdb+gdbserver: Make current_event per-thread state Pedro Alves
2024-05-07 23:42 ` [PATCH 22/34] Windows gdb+gdbserver: Make last_sig " Pedro Alves
2024-05-07 23:42 ` [PATCH 23/34] Windows gdb+gdbserver: Make siginfo_er " Pedro Alves
2024-05-07 23:42 ` [PATCH 24/34] Add backpointer from windows_thread_info to windows_process_info Pedro Alves
2024-05-08 15:28   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 25/34] Windows gdb+gdbserver: Share $_siginfo reading code Pedro Alves
2024-05-08 15:29   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 26/34] Windows gdb+gdbserver: Eliminate struct pending_stop Pedro Alves
2024-05-07 23:42 ` [PATCH 27/34] Windows gdb: Change serial_event management Pedro Alves
2024-05-07 23:42 ` [PATCH 28/34] Windows gdb: cygwin_set_dr => windows_set_dr, etc Pedro Alves
2024-05-08 14:46   ` Tom Tromey
2024-05-07 23:42 ` [PATCH 29/34] windows_per_inferior::continue_one_thread, unify WoW64/non-WoW64 paths Pedro Alves
2024-05-07 23:42 ` Pedro Alves [this message]
2024-05-07 23:42 ` [PATCH 31/34] Windows gdb+gdbserver: Check whether DBG_REPLY_LATER is available Pedro Alves
2024-05-08 12:45   ` Eli Zaretskii
2024-05-08 21:33     ` [PATCH 31/34 v1.2] " Pedro Alves
2024-05-09 10:07       ` Hannes Domani
     [not found]         ` <86zfsz5kly.fsf@gnu.org>
2024-05-09 11:11           ` Pedro Alves
2024-05-09 11:47             ` [PATCH 31/34 v1.3] " Pedro Alves
2024-05-09 12:28               ` Eli Zaretskii
2024-05-09 14:17               ` Tom Tromey
     [not found]             ` <86r0eb5g2n.fsf@gnu.org>
2024-05-09 13:27               ` [PATCH 31/34 v1.2] " Pedro Alves
2024-05-07 23:42 ` [PATCH 32/34] Windows gdb: Add non-stop support Pedro Alves
2024-05-07 23:42 ` [PATCH 33/34] Windows gdb: Watchpoints while running (internal vs external stops) Pedro Alves
2024-05-07 23:42 ` [PATCH 34/34] Mention Windows non-stop support in NEWS Pedro Alves
2024-05-08 15:40 ` [PATCH 00/34] Windows non-stop mode Tom Tromey
2024-05-15 17:35   ` Tom Tromey
2024-05-15 20:39     ` Pedro Alves
2024-05-16 15:53       ` Tom Tromey

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20240507234233.371123-31-pedro@palves.net \
    --to=pedro@palves.net \
    --cc=gdb-patches@sourceware.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).