Mirror of the gdb-patches mailing list
 help / color / mirror / Atom feed
From: Markus Metzger <markus.t.metzger@intel.com>
To: gdb-patches@sourceware.org
Cc: Guinevere Larsen <blarsen@redhat.com>
Subject: [PATCH v8 4/4] gdb, infrun: fix multi-threaded reverse stepping
Date: Wed, 15 Apr 2026 07:23:11 +0000	[thread overview]
Message-ID: <20260415072311.3597558-5-markus.t.metzger@intel.com> (raw)
In-Reply-To: <20260415072311.3597558-1-markus.t.metzger@intel.com>

When reverse-stepping a thread that has a pending breakpoint event, the
thread is not resumed as part of the infcmd function.  A first resume
notices the event and returns without resuming the target.

If the corresponding breakpoint has been deleted, event processing results
in a second resume that performs the intended stepping action.  That
resume happens after the infcmd function returned and the temporarily
modified execution direction was restored.  We end up resuming in the
wrong direction.

Store the direction in a thread's control state and change most of
infrun to take it from there rather than using the global variable.

Reviewed-By: Guinevere Larsen <blarsen@redhat.com>
---
 gdb/gdbthread.h                               |  10 ++
 gdb/infrun.c                                  |  61 ++++++-----
 gdb/infrun.h                                  |   7 --
 .../gdb.btrace/implicit-stop-replaying.exp    | 101 ++++++++++++++++++
 4 files changed, 147 insertions(+), 32 deletions(-)
 create mode 100644 gdb/testsuite/gdb.btrace/implicit-stop-replaying.exp

diff --git a/gdb/gdbthread.h b/gdb/gdbthread.h
index 5c8f498347d..c95f04b91a1 100644
--- a/gdb/gdbthread.h
+++ b/gdb/gdbthread.h
@@ -120,6 +120,13 @@ enum step_over_calls_kind
     STEP_OVER_UNDEBUGGABLE
   };
 
+/* Reverse execution.  */
+enum exec_direction_kind
+  {
+    EXEC_FORWARD,
+    EXEC_REVERSE
+  };
+
 /* Inferior thread specific part of `struct infcall_control_state'.
 
    Inferior process counterpart is `struct inferior_control_state'.  */
@@ -240,6 +247,9 @@ struct thread_control_state
   /* Whether the thread was replaying when the command was issued.  */
   bool is_replaying = false;
 
+  /* The execution direction when the command was issued.  */
+  enum exec_direction_kind execution_direction = EXEC_FORWARD;
+
 private:
   /* Function the thread was in as of last it started stepping.  */
   struct symbol *m_step_start_function = nullptr;
diff --git a/gdb/infrun.c b/gdb/infrun.c
index 0c8d737b5fe..5ce854d7028 100644
--- a/gdb/infrun.c
+++ b/gdb/infrun.c
@@ -96,7 +96,8 @@ static void insert_step_resume_breakpoint_at_caller (const frame_info_ptr &);
 
 static void insert_longjmp_resume_breakpoint (struct gdbarch *, CORE_ADDR);
 
-static bool maybe_software_singlestep (struct gdbarch *gdbarch);
+static bool maybe_software_singlestep (const thread_info *tp,
+				       gdbarch *gdbarch);
 
 static void resume (gdb_signal sig);
 
@@ -2381,11 +2382,11 @@ bool sched_multi = false;
    GDBARCH the current gdbarch.  */
 
 static bool
-maybe_software_singlestep (struct gdbarch *gdbarch)
+maybe_software_singlestep (const thread_info *tp, gdbarch *gdbarch)
 {
   bool hw_step = true;
 
-  if (execution_direction == EXEC_FORWARD
+  if (tp->control.execution_direction == EXEC_FORWARD
       && gdbarch_get_next_pcs_p (gdbarch))
     hw_step = !insert_single_step_breakpoints (gdbarch);
 
@@ -2544,6 +2545,10 @@ do_target_resume (ptid_t resume_ptid, bool step, enum gdb_signal sig)
   /* Install inferior's terminal modes.  */
   target_terminal::inferior ();
 
+  scoped_restore save_exec_dir
+    = make_scoped_restore (&execution_direction,
+			   tp->control.execution_direction);
+
   /* Avoid confusing the next resume, if the next stop/resume
      happens to apply to another thread.  */
   tp->set_stop_signal (GDB_SIGNAL_0);
@@ -2805,6 +2810,7 @@ resume_1 (enum gdb_signal sig)
 	      insert_breakpoints ();
 
 	      resume_ptid = internal_resume_ptid (user_step);
+
 	      do_target_resume (resume_ptid, false, GDB_SIGNAL_0);
 	      return;
 	    }
@@ -2853,7 +2859,7 @@ resume_1 (enum gdb_signal sig)
 	  set_step_over_info (aspace, regcache_read_pc (regcache), 0,
 			      tp->global_num);
 
-	  step = maybe_software_singlestep (gdbarch);
+	  step = maybe_software_singlestep (tp, gdbarch);
 
 	  insert_breakpoints ();
 	}
@@ -2872,7 +2878,7 @@ resume_1 (enum gdb_signal sig)
 
   /* Do we need to do it the hard way, w/temp breakpoints?  */
   else if (step)
-    step = maybe_software_singlestep (gdbarch);
+    step = maybe_software_singlestep (tp, gdbarch);
 
   /* Currently, our software single-step implementation leads to different
      results than hardware single-stepping in one situation: when stepping
@@ -2943,7 +2949,7 @@ resume_1 (enum gdb_signal sig)
   else
     resume_ptid = internal_resume_ptid (user_step);
 
-  if (execution_direction != EXEC_REVERSE
+  if (tp->control.execution_direction != EXEC_REVERSE
       && step && breakpoint_inserted_here_p (aspace, pc))
     {
       /* There are two cases where we currently need to step a
@@ -3113,6 +3119,7 @@ clear_proceed_status_thread (struct thread_info *tp)
   bpstat_clear (&tp->control.stop_bpstat);
 
   tp->control.is_replaying = target_record_is_replaying (tp->ptid);
+  tp->control.execution_direction = ::execution_direction;
 }
 
 /* Notify the current interpreter and observers that the target is about to
@@ -3221,7 +3228,8 @@ schedlock_applies (struct thread_info *tp)
 	  || (scheduler_mode == schedlock_step
 	      && tp->control.stepping_command)
 	  || (scheduler_mode == schedlock_replay
-	      && target_record_will_replay (tp->ptid, execution_direction)));
+	      && target_record_will_replay (tp->ptid,
+					    tp->control.execution_direction)));
 }
 
 /* When FORCE_P is false, set process_stratum_target::COMMIT_RESUMED_STATE
@@ -3659,7 +3667,7 @@ proceed (CORE_ADDR addr, enum gdb_signal siggnal)
       if (cur_thr->stop_pc_p ()
 	  && pc == cur_thr->stop_pc ()
 	  && breakpoint_here_p (aspace, pc) == ordinary_breakpoint_here
-	  && execution_direction != EXEC_REVERSE)
+	  && cur_thr->control.execution_direction != EXEC_REVERSE)
 	/* There is a breakpoint at the address we will resume at,
 	   step one instruction before inserting breakpoints so that
 	   we do not stop right away (and report a second hit at this
@@ -4985,7 +4993,7 @@ adjust_pc_after_break (struct thread_info *thread,
      breakpoint at PC - 1.  We'd then report a hit on B1, although
      INSN1 hadn't been de-executed yet.  Doing nothing is the correct
      behavior.  */
-  if (execution_direction == EXEC_REVERSE)
+  if (thread->control.execution_direction == EXEC_REVERSE)
     return;
 
   /* If the target can tell whether the thread hit a SW breakpoint,
@@ -7578,7 +7586,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 
       delete_step_resume_breakpoint (ecs->event_thread);
       if (ecs->event_thread->control.proceed_to_finish
-	  && execution_direction == EXEC_REVERSE)
+	  && ecs->event_thread->control.execution_direction == EXEC_REVERSE)
 	{
 	  struct thread_info *tp = ecs->event_thread;
 
@@ -7593,7 +7601,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 	}
       fill_in_stop_func (gdbarch, ecs);
       if (ecs->event_thread->stop_pc () == ecs->stop_func_start
-	  && execution_direction == EXEC_REVERSE)
+	  && ecs->event_thread->control.execution_direction == EXEC_REVERSE)
 	{
 	  /* We are stepping over a function call in reverse, and just
 	     hit the step-resume breakpoint at the start address of
@@ -7713,7 +7721,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 
   if (pc_in_thread_step_range (ecs->event_thread->stop_pc (),
 			       ecs->event_thread)
-      && (execution_direction != EXEC_REVERSE
+      && (ecs->event_thread->control.execution_direction != EXEC_REVERSE
 	  || *curr_frame_id == original_frame_id))
     {
       infrun_debug_printf
@@ -7732,7 +7740,7 @@ process_event_stop_test (struct execution_control_state *ecs)
       CORE_ADDR stop_pc = ecs->event_thread->stop_pc ();
       if (stop_pc == ecs->event_thread->control.step_range_start
 	  && stop_pc != ecs->stop_func_start
-	  && execution_direction == EXEC_REVERSE)
+	  && ecs->event_thread->control.execution_direction == EXEC_REVERSE)
 	end_stepping_range (ecs);
       else
 	keep_going (ecs);
@@ -7754,7 +7762,7 @@ process_event_stop_test (struct execution_control_state *ecs)
      backward through the trampoline code, and that's handled further
      down, so there is nothing for us to do here.  */
 
-  if (execution_direction != EXEC_REVERSE
+  if (ecs->event_thread->control.execution_direction != EXEC_REVERSE
       && ecs->event_thread->control.step_over_calls == STEP_OVER_UNDEBUGGABLE
       && in_solib_dynsym_resolve_code (ecs->event_thread->stop_pc ())
       && (!ecs->event_thread->control.step_start_function_p ()
@@ -7902,7 +7910,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 
       /* Reverse stepping through solib trampolines.  */
 
-      if (execution_direction == EXEC_REVERSE
+      if (ecs->event_thread->control.execution_direction == EXEC_REVERSE
 	  && ecs->event_thread->control.step_over_calls != STEP_OVER_NONE
 	  && (gdbarch_skip_trampoline_code (gdbarch, frame, stop_pc)
 	      || (ecs->stop_func_start == 0
@@ -7930,7 +7938,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 	     stepped into (backwards), and continue to there.  When we
 	     get there, we'll need to single-step back to the caller.  */
 
-	  if (execution_direction == EXEC_REVERSE)
+	  if (ecs->event_thread->control.execution_direction == EXEC_REVERSE)
 	    {
 	      /* If we're already at the start of the function, we've either
 		 just stepped backward into a single instruction function,
@@ -7993,7 +8001,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 						  tmp_sal)
 	    && !inline_frame_is_marked_for_skip (true, ecs->event_thread))
 	  {
-	    if (execution_direction == EXEC_REVERSE)
+	    if (ecs->event_thread->control.execution_direction == EXEC_REVERSE)
 	      handle_step_into_function_backward (gdbarch, ecs);
 	    else
 	      handle_step_into_function (gdbarch, ecs);
@@ -8011,7 +8019,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 	  return;
 	}
 
-      if (execution_direction == EXEC_REVERSE)
+      if (ecs->event_thread->control.execution_direction == EXEC_REVERSE)
 	{
 	  /* If we're already at the start of the function, we've either just
 	     stepped backward into a single instruction function without line
@@ -8040,7 +8048,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 
   /* Reverse stepping through solib trampolines.  */
 
-  if (execution_direction == EXEC_REVERSE
+  if (ecs->event_thread->control.execution_direction == EXEC_REVERSE
       && ecs->event_thread->control.step_over_calls != STEP_OVER_NONE)
     {
       CORE_ADDR stop_pc = ecs->event_thread->stop_pc ();
@@ -8114,7 +8122,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 	}
     }
 
-  if (execution_direction == EXEC_REVERSE
+  if (ecs->event_thread->control.execution_direction == EXEC_REVERSE
       && ecs->event_thread->control.proceed_to_finish
       && ecs->event_thread->stop_pc () >= ecs->stop_func_alt_start
       && ecs->event_thread->stop_pc () < ecs->stop_func_start)
@@ -8254,7 +8262,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 
       if (stop_pc_sal.is_stmt)
 	{
-	  if (execution_direction == EXEC_REVERSE)
+	  if (ecs->event_thread->control.execution_direction == EXEC_REVERSE)
 	    {
 	      /* We are stepping backwards make sure we have reached the
 		 beginning of the line.  */
@@ -8312,7 +8320,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 	}
     }
 
-  if (execution_direction == EXEC_REVERSE
+  if (ecs->event_thread->control.execution_direction == EXEC_REVERSE
 	  && *curr_frame_id != original_frame_id
 	  && original_frame_id.code_addr_p && curr_frame_id->code_addr_p
 	  && original_frame_id.code_addr == curr_frame_id->code_addr)
@@ -8353,7 +8361,7 @@ process_event_stop_test (struct execution_control_state *ecs)
 
   infrun_debug_printf ("keep going");
 
-  if (execution_direction == EXEC_REVERSE)
+  if (ecs->event_thread->control.execution_direction == EXEC_REVERSE)
     {
       CORE_ADDR stop_pc = ecs->event_thread->stop_pc ();
 
@@ -8652,6 +8660,7 @@ keep_going_stepped_thread (struct thread_info *tp)
 				     tp->stop_pc ());
 
       resume_ptid = internal_resume_ptid (tp->control.stepping_command);
+
       do_target_resume (resume_ptid, false, GDB_SIGNAL_0);
     }
   else
@@ -9356,14 +9365,16 @@ print_signal_received_reason (struct ui_out *uiout, enum gdb_signal siggnal)
 void
 print_no_history_reason (struct ui_out *uiout)
 {
+  struct thread_info *tp = inferior_thread ();
+
   if (uiout->is_mi_like_p ())
     uiout->field_string ("reason", async_reason_lookup (EXEC_ASYNC_NO_HISTORY));
-  else if (execution_direction == EXEC_FORWARD)
+  else if (tp->control.execution_direction == EXEC_FORWARD)
     uiout->text ("\nReached end of recorded history; stopping.\nFollowing "
 		 "forward execution will be added to history.\n");
   else
     {
-      gdb_assert (execution_direction == EXEC_REVERSE);
+      gdb_assert (tp->control.execution_direction == EXEC_REVERSE);
       uiout->text ("\nReached end of recorded history; stopping.\nBackward "
 		   "execution from here not possible.\n");
     }
diff --git a/gdb/infrun.h b/gdb/infrun.h
index f15662d5bc9..d8aef26a88a 100644
--- a/gdb/infrun.h
+++ b/gdb/infrun.h
@@ -106,13 +106,6 @@ extern bool disable_randomization;
    current location.  */
 extern ULONGEST get_stop_id (void);
 
-/* Reverse execution.  */
-enum exec_direction_kind
-  {
-    EXEC_FORWARD,
-    EXEC_REVERSE
-  };
-
 /* The current execution direction.  */
 extern enum exec_direction_kind execution_direction;
 
diff --git a/gdb/testsuite/gdb.btrace/implicit-stop-replaying.exp b/gdb/testsuite/gdb.btrace/implicit-stop-replaying.exp
new file mode 100644
index 00000000000..bbe2d8140a7
--- /dev/null
+++ b/gdb/testsuite/gdb.btrace/implicit-stop-replaying.exp
@@ -0,0 +1,101 @@
+# This testcase is part of GDB, the GNU debugger.
+#
+# Copyright 2025 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test that we stop replaying other threads when stepping a thread at the
+# end of its execution history.
+#
+# This is similar to the last test in multi-thread-step.exp, except that
+# we reverse-step instead of record goto begin to start replaying and we
+# step instead of continuing.
+#
+# This triggered a bug where GDB confused the execution direction and kept
+# stepping both threads backwards instead of forwards.
+
+require allow_btrace_tests
+
+standard_testfile multi-thread-step.c
+if {[prepare_for_testing "failed to prepare" $testfile $srcfile \
+	 {debug pthreads}]} {
+    return -1
+}
+
+if {![runto_main]} {
+    return -1
+}
+
+# Set up breakpoints.
+set bp_1 [gdb_get_line_number "bp.1" $srcfile]
+set bp_2 [gdb_get_line_number "bp.2" $srcfile]
+
+# Trace the code between the two breakpoints.
+gdb_breakpoint $srcfile:$bp_1
+gdb_continue_to_breakpoint "continue to bp.1" ".*$srcfile:$bp_1\r\n.*"
+
+# Make sure GDB knows about the new thread.
+gdb_test "info threads"
+gdb_test_no_output "record btrace"
+
+# We have two threads at or close to bp.1 but handled only one stop event.
+# Remove the breakpoint so we do not need to deal with the 2nd event.
+delete_breakpoints
+gdb_breakpoint $srcfile:$bp_2
+gdb_continue_to_breakpoint "continue to bp.2" ".*$srcfile:$bp_2\r\n.*"
+
+# Determine the thread that reported the breakpoint.
+set thread [get_integer_valueof "\$_thread" bad]
+
+# Determine the other thread.
+set other "bad"
+if {$thread == 1} {
+    set other 2
+} elseif {$thread == 2} {
+    set other 1
+}
+
+# This test only works for scheduler-locking 'replay'.
+gdb_test_no_output "set scheduler-locking replay"
+
+# Remove breakpoints or we might run into it right away.
+delete_breakpoints
+
+# Start replaying the other thread.
+gdb_test "thread apply $other reverse-stepi"
+gdb_test "thread apply $other info record" "Replay in progress.*" \
+    "other thread is replaying"
+
+# Step the thread that reported the breakpoint, which is not replaying.
+#
+# There is a chance that the other thread exits while we step.  We could
+# slow it down to make this less likely, but we can also handle this case.
+set other_exited 0
+gdb_test_multiple "next" {} {
+    -re "Thread.*exited" {
+	set other_exited 1
+	exp_continue
+    }
+    -re -wrap "return arg;.*" {
+	pass $gdb_test_name
+    }
+}
+
+# The other thread stopped replaying.  If it still exists.
+if {$other_exited == 1} {
+    pass "other thread stopped replaying"
+} else {
+    gdb_test "thread apply $other info record" "Recorded \[^\\\r\\\n\]*" \
+	"other thread stopped replaying"
+}
-- 
2.34.1

Intel Deutschland GmbH
Registered Address: Dornacher Strasse 1, 85622 Feldkirchen, Germany
Tel: +49 89 991 430, www.intel.de
Managing Directors: Harry Demas, Jeffrey Schneiderman, Yin Chong Sorrell
Chairperson of the Supervisory Board: Nicole Lau
Registered Seat: Munich
Commercial Register: Amtsgericht Muenchen HRB 186928


      parent reply	other threads:[~2026-04-15  7:24 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-15  7:23 [PATCH v8 0/4] PR gdb/31353 Markus Metzger
2026-04-15  7:23 ` [PATCH v8 1/4] gdb, infrun, btrace: fix reverse/replay stepping at end of execution history Markus Metzger
2026-04-15  7:23 ` [PATCH v8 2/4] gdb, infrun, record: fix hang when step-over fails with no-history Markus Metzger
2026-04-15  7:23 ` [PATCH v8 3/4] gdb, infrun, record: move no-history notification into normal_stop Markus Metzger
2026-04-15  7:23 ` Markus Metzger [this message]

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=20260415072311.3597558-5-markus.t.metzger@intel.com \
    --to=markus.t.metzger@intel.com \
    --cc=blarsen@redhat.com \
    --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