From 30d987e256a6beeac84771aa66d6a721cecb0a43 Mon Sep 17 00:00:00 2001
From: 白 <597498687@qq.com>
Date: Fri, 14 Aug 2026 15:02:16 +0800
Subject: [PATCH] infrun: Introduce empty-loop step heuristic

PR gdb/21221

When a user single-steps a source line that contains a tight loop with
an empty body, GDB currently single-steps every iteration and only
stops after the whole loop completes.  This is confusing because a
single 'step' command can run an unbounded number of iterations.

Introduce a heuristic that detects when a single source-level step is
stopping repeatedly on the same source line, and ends the step after
one iteration.  The detection records the first stop PC observed on the
source line while the step is active, and ends the step when the
inferior returns to that PC after having left it.  This correctly handles
loops where the compiler has placed the test after the body, which
would otherwise cause a backward jump to the body before the first
iteration is complete.

A safety threshold prevents runaway steps if the heuristic somehow
fails to detect the loop.  The stop message is emitted through ui_out
so front-ends can parse it.

Tested by gdb.base/empty-loop-step.exp.
---
 gdb/NEWS                                   |   4 +
 gdb/gdbthread.h                            |  10 ++
 gdb/infrun.c                               | 106 +++++++++++++++++++++
 gdb/testsuite/gdb.base/empty-loop-step.c   |  33 +++++++
 gdb/testsuite/gdb.base/empty-loop-step.exp |  41 ++++++++
 5 files changed, 194 insertions(+)
 create mode 100644 gdb/testsuite/gdb.base/empty-loop-step.c
 create mode 100644 gdb/testsuite/gdb.base/empty-loop-step.exp

diff --git a/gdb/NEWS b/gdb/NEWS
index 10c1820..5acefdb 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -3,6 +3,10 @@
 
 *** Changes since GDB 18
 
+* GDB now detects source-level empty-loop bodies during stepping and
+  ends a single step at the end of one loop iteration, avoiding
+  single-stepping every iteration.
+
 *** Changes in GDB 18
 
 * Support for the Common Trace Format (CTF) has been removed.  GDB now
diff --git a/gdb/gdbthread.h b/gdb/gdbthread.h
index 224f1cc..d90538e 100644
--- a/gdb/gdbthread.h
+++ b/gdb/gdbthread.h
@@ -212,6 +212,16 @@ struct thread_control_state
      single-stepping.  */
   bool currently_stepping = false;
 
+  /* Counter used by the empty-loop step heuristic.  */
+  unsigned int empty_loop_step_count = 0;
+
+  /* First stop PC observed while the empty-loop step heuristic is
+     active.  */
+  CORE_ADDR empty_loop_first_pc = 0;
+
+  /* Previous stop PC while the empty-loop step heuristic is active.  */
+  CORE_ADDR last_stop_pc = 0;
+
   /* Nonzero if the thread is being proceeded for a "finish" command
      or a similar situation when return value should be printed.  */
   int proceed_to_finish = 0;
diff --git a/gdb/infrun.c b/gdb/infrun.c
index c0767e7..e3f61e0 100644
--- a/gdb/infrun.c
+++ b/gdb/infrun.c
@@ -439,6 +439,12 @@ show_stop_on_solib_events (struct ui_file *file, int from_tty,
 
 static bool stop_print_frame;
 
+/* Maximum number of single steps on the same source line before the
+   empty-loop heuristic gives up and ends the step anyway.  This is a
+   safety net for cases where the backward-branch heuristic below does
+   not detect the loop.  */
+#define EMPTY_LOOP_STEP_THRESHOLD 100
+
 /* This is a cached copy of the target/ptid/waitstatus of the last
    event returned by target_wait().
    This information is returned by get_last_target_status().  */
@@ -4958,6 +4964,9 @@ init_thread_stepping_state (struct thread_info *tss)
   tss->stepping_over_breakpoint = 0;
   tss->stepping_over_watchpoint = 0;
   tss->step_after_step_resume_breakpoint = 0;
+  tss->control.empty_loop_step_count = 0;
+  tss->control.empty_loop_first_pc = 0;
+  tss->control.last_stop_pc = 0;
 }
 
 /* See infrun.h.  */
@@ -7516,6 +7525,97 @@ private:
 
 }
 
+/* Report that we have stopped because we detected an empty-loop body.
+   FUNC_NAME may be NULL.  */
+
+static void
+report_empty_loop_stop (struct ui_out *uiout, const char *func_name,
+	struct symtab *symtab, int line)
+{
+  const char *file_name = symtab != nullptr ? symtab->filename () : "?";
+
+  uiout->message (_("%pF () at %pF:%pF\n"),
+	  string_field ("function", func_name ? func_name : "??"),
+	  string_field ("file", file_name),
+	  signed_field ("line", line));
+}
+
+/* Return true if we have completed one iteration of an empty loop body
+   while stepping.  If so, end the step.  */
+
+static bool
+maybe_end_empty_loop_step (struct execution_control_state *ecs)
+{
+  struct thread_info *tp = ecs->event_thread;
+  struct symtab_and_line start_sal =
+    find_sal_for_pc (tp->control.step_range_start, 0);
+  struct symtab_and_line current_sal = find_sal_for_pc (tp->stop_pc (), 0);
+
+  if (start_sal.line != 0
+      && current_sal.line == start_sal.line
+      && current_sal.symtab == start_sal.symtab)
+    {
+      bool loop_iter_done = false;
+      CORE_ADDR stop_pc = tp->stop_pc ();
+      CORE_ADDR last_stop_pc = tp->control.last_stop_pc;
+
+      tp->control.empty_loop_step_count++;
+
+      /* If this is the first stop on this line since the step began,
+	 record the PC.  One complete iteration is detected when we
+	 return to this PC after having left it, i.e. the loop has
+	 circled back to its starting point.  */
+      if (last_stop_pc == 0)
+	tp->control.empty_loop_first_pc = stop_pc;
+
+      /* A completed iteration is indicated by returning to the first
+	 stop PC observed on this line, after at least one other stop.
+	 This is more reliable than looking for backward branches,
+	 because compilers may place the loop test after the body, causing
+	 a backward jump to the body before it has executed for the first
+	 iteration.  */
+      if (last_stop_pc != 0
+	  && stop_pc == tp->control.empty_loop_first_pc)
+	loop_iter_done = true;
+
+      if (loop_iter_done)
+	{
+	  report_empty_loop_stop (current_uiout, ecs->stop_func_name,
+				  start_sal.symtab, start_sal.line);
+	  tp->control.empty_loop_step_count = 0;
+	  tp->control.empty_loop_first_pc = 0;
+	  tp->control.last_stop_pc = 0;
+	  end_stepping_range (ecs);
+	  return true;
+	}
+
+      /* Fallback: if we keep stopping on the same line without a
+	 backward jump (unusual), avoid hanging by ending the step after a
+	 fixed number of stops.  */
+      if (tp->control.empty_loop_step_count > EMPTY_LOOP_STEP_THRESHOLD)
+	{
+	  report_empty_loop_stop (current_uiout, ecs->stop_func_name,
+				  start_sal.symtab, start_sal.line);
+	  tp->control.empty_loop_step_count = 0;
+	  tp->control.empty_loop_first_pc = 0;
+	  tp->control.last_stop_pc = 0;
+	  end_stepping_range (ecs);
+	  return true;
+	}
+    }
+  else
+    {
+      /* No longer on the same source line.  Reset both the counter
+	 and the last-stop marker.  */
+      tp->control.empty_loop_step_count = 0;
+      tp->control.empty_loop_first_pc = 0;
+      tp->control.last_stop_pc = 0;
+    }
+
+  tp->control.last_stop_pc = tp->stop_pc ();
+  return false;
+}
+
 /* Come here when we've got some debug event / signal we can explain
    (IOW, not a random signal), and test whether it should cause a
    stop, or whether we should resume the inferior (transparently).
@@ -7827,6 +7927,12 @@ process_event_stop_test (struct execution_control_state *ecs)
 	  && stop_pc != ecs->stop_func_start
 	  && execution_direction == EXEC_REVERSE)
 	end_stepping_range (ecs);
+      else if (execution_direction == EXEC_FORWARD)
+	{
+	  if (maybe_end_empty_loop_step (ecs))
+	    return;
+	  keep_going (ecs);
+	}
       else
 	keep_going (ecs);
 
diff --git a/gdb/testsuite/gdb.base/empty-loop-step.c b/gdb/testsuite/gdb.base/empty-loop-step.c
new file mode 100644
index 0000000..28cad02
--- /dev/null
+++ b/gdb/testsuite/gdb.base/empty-loop-step.c
@@ -0,0 +1,33 @@
+/* Test empty-loop step heuristic.
+
+   Copyright (C) 2026 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   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/>.  */
+
+volatile int i;
+
+void
+foo (void)
+{
+  for (i = 0; i < 10; i++);  /* loop-line */
+}
+
+int
+main (void)
+{
+  foo ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/empty-loop-step.exp b/gdb/testsuite/gdb.base/empty-loop-step.exp
new file mode 100644
index 0000000..fee1d5d
--- /dev/null
+++ b/gdb/testsuite/gdb.base/empty-loop-step.exp
@@ -0,0 +1,41 @@
+# Copyright (C) 2026 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 GDB ends a source-level "step" after one iteration of an
+# empty loop body, rather than running the whole loop.
+
+standard_testfile
+
+if {[prepare_for_testing "failed to prepare" $testfile $srcfile debug]} {
+    return -1
+}
+
+if ![runto_main] then {
+    fail "can't run to main"
+    return 0
+}
+
+# Continue to the empty for-loop line.
+set loop_line [gdb_get_line_number "for (i = 0;"]
+gdb_test "break $loop_line" ".*" "set breakpoint at loop"
+gdb_test "continue" ".*" "continue to loop"
+
+# A single "step" should end after one loop iteration, not run all
+# 10 iterations.
+gdb_test "step" ".*" "step empty loop"
+
+# If the heuristic worked, i should be 1.  If the heuristic failed,
+# the whole loop would have run and i would be 10.
+gdb_test "print i" ".*= 1.*" "stopped after one iteration"
-- 
2.53.0.windows.3

