Mirror of the gdb-patches mailing list
 help / color / mirror / Atom feed
* [PATCH 0/4] Fixes for tail call, until, and FinishBreakpoints
@ 2026-01-24 11:29 Andrew Burgess
  2026-01-24 11:29 ` [PATCH 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls Andrew Burgess
                   ` (4 more replies)
  0 siblings, 5 replies; 14+ messages in thread
From: Andrew Burgess @ 2026-01-24 11:29 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

I've been looking at places where we use get_frame_pc when we should
be using get_frame_address_in_block to handle tail calls correctly.
This got me looking at gdb.FinishBreakpoint, and I found a bunch of
things that needed fixing.

Thanks,
Andrew

---

Andrew Burgess (4):
  gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls
  gdb/python: fix FinishBreakpoint.return_value for tail call functions
  gdb/python: don't allow FinishBreakpoints for inline frames
  gdb/python: fix gdb.FinishBreakpoint returning to a tail call frame

 gdb/doc/python.texi                           |   3 +
 gdb/frame.c                                   |  35 +++--
 gdb/python/py-finishbreakpoint.c              |  17 +-
 gdb/testsuite/gdb.base/until-in-tailcall.c    |  53 +++++++
 gdb/testsuite/gdb.base/until-in-tailcall.exp  | 108 +++++++++++++
 .../gdb.python/py-finish-breakpoint-inline.c  |  58 +++++++
 .../py-finish-breakpoint-inline.exp           | 147 ++++++++++++++++++
 .../gdb.python/py-finish-breakpoint-inline.py |  27 ++++
 .../py-finish-breakpoint-tailcall.c           |  45 ++++++
 .../py-finish-breakpoint-tailcall.exp         | 118 ++++++++++++++
 .../py-finish-breakpoint-tailcall.py          |  27 ++++
 11 files changed, 623 insertions(+), 15 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/until-in-tailcall.c
 create mode 100644 gdb/testsuite/gdb.base/until-in-tailcall.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py


base-commit: 9810a515fe3aed6046760b8c1c1d3daa9ebc4334
-- 
2.25.4


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls
  2026-01-24 11:29 [PATCH 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
@ 2026-01-24 11:29 ` Andrew Burgess
  2026-01-27 16:37   ` Andrew Burgess
  2026-01-24 11:29 ` [PATCH 2/4] gdb/python: fix FinishBreakpoint.return_value for tail call functions Andrew Burgess
                   ` (3 subsequent siblings)
  4 siblings, 1 reply; 14+ messages in thread
From: Andrew Burgess @ 2026-01-24 11:29 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

The 3 frame_unwind_caller_WHAT functions:

  + frame_unwind_caller_id
  + frame_unwind_caller_pc
  + frame_unwind_caller_arch

Are, I believe, currently all broken with respect to inline and tail
call functions.

The Python FinishBreakpoint type creates a breakpoint in the caller
function which, when triggered, indicates that the FinishBreakpoint
has gone out of scope.

I was writing a test for the FinishBreakpoint type which included a
tail call function, and the FinishBreakpoint was being created for the
tail call function frame.  What I observed is that the out of scope
breakpoint was never being hit.

The call stack in my new test looked like this:

  main -> tailcall_function -> normal_function

I would stop in normal_function, and then create a FinishBreakpoint
for the parent (tailcall_function) frame.  The FinishBreakpoint's out
of scope breakpoint was being correctly placed in the 'main' function,
but would never trigger.

The problem is that the breakpoint placed in 'main' holds a frame-id.
This frame-id is the frame in which the breakpoint should trigger.
This frame-id exists to prevent premature stops due to recursion.  But
in this case, when the breakpoint in 'main' was hit, despite no
recursion having occurred, the frame-id didn't match, and so the
breakpoint was ignored.

The problem is that in bpfinishpy_init we call frame_unwind_caller_id
to compute the frame-id of the frame in which we should stop, and
frame_unwind_caller_id was returning the wrong frame-id.  As far as I
can tell frame_unwind_caller_id has been broken since it was updated
for inline functions in commit edb3359dff90ef8a.

The frame_unwind_caller_id function, and all the
frame_unwind_caller_WHAT functions, are intended to return the
previous frame, but should skip over any inline, or tail call frames.
Let's look at an example call stack:

  #0 A		// A normal function.
  #1 B		// An inline function.
  #2 C		// An inline function.
  #3 D		// A normal function.
  #4 E		// A normal function.

Starting from #0, a normal function, frame_unwind_caller_id, should
return the frame-id for #3, and this is what happens.

But if we start in #1 and call frame_unwind_caller_id, then we should
still return the frame-id for #3, but this is not what happens.
Instead we return the frame-id for #4, skipping a frame.

The problem is that frame_unwind_caller_id starts by calling
skip_artificial_frames, which calls get_prev_frame_always until we
reach a non-inline (or non-tail call) frame, this moves us from #1 to

Then, back in frame_unwind_caller_id we call get_prev_frame_always,
which moves us to #4.

Then frame_unwind_caller_id finishes with a call to
skip_artificial_frames, this could potentially result in additional
frames being skipped, but in my example above this isn't the case.

The problem here is that if skip_artificial_frames skips anything,
then we have already unwound to the caller frame, and the
get_prev_frame_always call in frame_unwind_caller_id is unnecessary.

I propose to add a new helper function frame_unwind_caller_frame,
which should do the correct thing; it unwinds one frame and then calls
skip_artificial_frames.  This should do exactly what is needed.

Then all the frame_unwind_caller_WHAT functions will be updated to use
this helper function, and just extract the required property from the
resulting frame.

With this fix in place I could then write the FinishBreakpoint test,
which now works.

I took a look for other places where frame_unwind_caller_id is used
and spotted that the 'until' command does much the same thing, placing
a breakpoint in the caller frame.  As predicted, the 'until' command
is also broken when used within a tail call frame.  This patch fixes
that issue too.  There's also a test for the until command.
---
 gdb/frame.c                                   |  35 ++++--
 gdb/testsuite/gdb.base/until-in-tailcall.c    |  53 +++++++++
 gdb/testsuite/gdb.base/until-in-tailcall.exp  | 108 ++++++++++++++++++
 .../py-finish-breakpoint-tailcall.c           |  45 ++++++++
 .../py-finish-breakpoint-tailcall.exp         |  93 +++++++++++++++
 .../py-finish-breakpoint-tailcall.py          |  26 +++++
 6 files changed, 349 insertions(+), 11 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/until-in-tailcall.c
 create mode 100644 gdb/testsuite/gdb.base/until-in-tailcall.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py

diff --git a/gdb/frame.c b/gdb/frame.c
index 8cb1d0a5c42..f80b3e281b6 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -699,6 +699,23 @@ get_stack_frame_id (const frame_info_ptr &next_frame)
   return get_frame_id (skip_artificial_frames (next_frame));
 }
 
+/* Helper for the various frame_unwind_caller_* functions.  Unwind
+   INITIAL_NEXT_FRAME at least one frame, but skip any artificial frames,
+   that is inline or tailcall frames.
+
+   Return the Nth previous frame (as required to find a non-artificial
+   frame), or NULL if no previous frame could be found.  */
+
+static frame_info_ptr
+frame_unwind_caller_frame (const frame_info_ptr &initial_next_frame)
+{
+  frame_info_ptr this_frame = get_prev_frame_always (initial_next_frame);
+  if (this_frame == nullptr)
+    return nullptr;
+
+  return skip_artificial_frames (this_frame);
+}
+
 struct frame_id
 frame_unwind_caller_id (const frame_info_ptr &initial_next_frame)
 {
@@ -707,15 +724,11 @@ frame_unwind_caller_id (const frame_info_ptr &initial_next_frame)
      unintentionally returning a null_frame_id (e.g., when a caller
      requests the frame ID of "main()"s caller.  */
 
-  frame_info_ptr next_frame = skip_artificial_frames (initial_next_frame);
-  if (next_frame == NULL)
+  frame_info_ptr this_frame = frame_unwind_caller_frame (initial_next_frame);
+  if (this_frame == nullptr)
     return null_frame_id;
 
-  frame_info_ptr this_frame = get_prev_frame_always (next_frame);
-  if (this_frame)
-    return get_frame_id (skip_artificial_frames (this_frame));
-  else
-    return null_frame_id;
+  return get_frame_id (this_frame);
 }
 
 const struct frame_id null_frame_id = { 0 }; /* All zeros.  */
@@ -1074,14 +1087,14 @@ frame_unwind_pc (const frame_info_ptr &this_frame)
 CORE_ADDR
 frame_unwind_caller_pc (const frame_info_ptr &initial_this_frame)
 {
-  frame_info_ptr this_frame = skip_artificial_frames (initial_this_frame);
+  frame_info_ptr this_frame = frame_unwind_caller_frame (initial_this_frame);
 
   /* We must have a non-artificial frame.  The caller is supposed to check
      the result of frame_unwind_caller_id (), which returns NULL_FRAME_ID
      in this case.  */
   gdb_assert (this_frame != nullptr);
 
-  return frame_unwind_pc (this_frame);
+  return get_frame_pc (this_frame);
 }
 
 bool
@@ -3135,14 +3148,14 @@ frame_unwind_arch (const frame_info_ptr &next_frame)
 struct gdbarch *
 frame_unwind_caller_arch (const frame_info_ptr &initial_next_frame)
 {
-  frame_info_ptr next_frame = skip_artificial_frames (initial_next_frame);
+  frame_info_ptr next_frame = frame_unwind_caller_frame (initial_next_frame);
 
   /* We must have a non-artificial frame.  The caller is supposed to check
      the result of frame_unwind_caller_id (), which returns NULL_FRAME_ID
      in this case.  */
   gdb_assert (next_frame != nullptr);
 
-  return frame_unwind_arch (next_frame);
+  return get_frame_arch (next_frame);
 }
 
 /* Gets the language of FRAME.  */
diff --git a/gdb/testsuite/gdb.base/until-in-tailcall.c b/gdb/testsuite/gdb.base/until-in-tailcall.c
new file mode 100644
index 00000000000..3e3853ba6df
--- /dev/null
+++ b/gdb/testsuite/gdb.base/until-in-tailcall.c
@@ -0,0 +1,53 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+/* This test relies on tailcall_function actually compiling to a tail call
+   function.  we try to force this by preventing tailcall_function and
+   normal_function from being inlined, then compiling this file at -O2.
+   Still, that's no guarantee.  If tailcall_function isn't a tail call,
+   then the test becomes pointless.  */
+
+volatile int global_var = 42;
+
+int __attribute__ ((noinline, noclone))
+normal_function (int x)
+{
+  return x + 1;
+}
+
+int __attribute__ ((noinline, noclone))
+tailcall_function (int x)
+{
+  return normal_function (x);
+}
+
+void __attribute__ ((noinline, noclone))
+worker_func (void)
+{
+  ++global_var;
+  ++global_var;			/* Run until here.  */
+  ++global_var;
+}
+
+int
+main (void)
+{
+  int result = tailcall_function (42);
+  result -= global_var;		/* Should stop here.  */
+  worker_func ();
+  return result;
+}
diff --git a/gdb/testsuite/gdb.base/until-in-tailcall.exp b/gdb/testsuite/gdb.base/until-in-tailcall.exp
new file mode 100644
index 00000000000..cd66d0ceaf8
--- /dev/null
+++ b/gdb/testsuite/gdb.base/until-in-tailcall.exp
@@ -0,0 +1,108 @@
+# 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/>.
+
+# Run until we have a tail call frame in the stack.  The stack looks like this:
+#
+# main -> tailcall_function -> normal_function
+#
+# We stop on normal_function, and then use 'up' to go to the
+# tailcall_function frame.  We then use 'until NN' where 'NN' is a
+# line that should not be reached until after the inferior has
+# returned to main.
+#
+# However, when the inferior returns to main, GDB should stop the
+# inferior as the 'until' has gone out of scope.
+#
+# At one point this was not happening as GDB would place the out of
+# scope 'until' guard breakpoint in the wrong place.
+#
+# This test relies on the source file compiling with a tail call in
+# place.
+
+standard_testfile
+
+if {[prepare_for_testing "failed to prepare" $testfile $srcfile \
+	 {debug optimize=-O2}]} {
+    return
+}
+
+# Run until there is a tail call frame in the backtrace.  Then use the
+# 'until' command with a line that will not be reached until after the
+# current stack has unwound back to main.  We expect the until's out
+# of scope breakpoint to be hit first.
+#
+# When USE_PARENT_FRAME_P is true we move up to the tail call frame
+# and issue the 'until' command while that frame is selected.
+#
+# When USE_PARENT_FRAME_P is false we issue the 'until' command while
+# normal_function is selected, this function is called from the tail
+# call function.
+#
+# In both cases, we expect the out of scope breakpoint to be placed
+# back in main, which is where the inferior should be stopped.
+proc run_test { use_parent_frame_p } {
+    clean_restart $::testfile
+
+    if {![runto normal_function]} {
+	return
+    }
+
+    # Check the stack looks correct.
+    gdb_test "bt" \
+	[multi_line \
+	     "#0\\s+normal_function\[^\r\n\]+" \
+	     "#1\\s+(?:$::hex in )?tailcall_function\[^\r\n\]+" \
+	     "#2\\s+(?:$::hex in )?main\[^\r\n\]+"] \
+	"check call stack"
+
+    # Move up to tailcall_function if needed.
+    if { $use_parent_frame_p } {
+	gdb_test "up" ".* in tailcall_function .*" \
+	    "move up to tailcall_function"
+    }
+
+    # Where we ask the 'until' to run to.
+    set until_lineno [gdb_get_line_number "Run until here."]
+
+    # Where we actually expect the inferior to stop.
+    set stop_lineno [gdb_get_line_number "Should stop here."]
+
+    # Issue the 'until' command and check that the inferior stops in main
+    # when the until guard breakpoint notices we have left the
+    # tailcall_function.
+    set saw_stop_location false
+    set saw_stop_source false
+    gdb_test_multiple "until $until_lineno" "run until worker_func" {
+	-re "^until $until_lineno\r\n" {
+	    exp_continue
+	}
+	-re "^$::gdb_prompt $" {
+	    gdb_assert { $saw_stop_location && $saw_stop_source } \
+		$gdb_test_name
+	}
+	-re "^main \\(\\) at \[^\r\n\]+/$::srcfile:$stop_lineno\r\n" {
+	    set saw_stop_location true
+	    exp_continue
+	}
+	-re "^$stop_lineno\\s+\[^\r\n\]+\r\n" {
+	    set saw_stop_source true
+	    exp_continue
+	}
+    }
+}
+
+foreach_with_prefix use_parent_frame { true false } {
+    run_test $use_parent_frame
+}
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c
new file mode 100644
index 00000000000..d129d7e41b4
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c
@@ -0,0 +1,45 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+/* This test relies on tailcall_function actually compiling to a tail call
+   function.  we try to force this by preventing tailcall_function and
+   normal_function from being inlined, then compiling this file at -O2.
+   Still, that's no guarantee.  If tailcall_function isn't a tail call,
+   then the test becomes pointless.  */
+
+volatile int global_var = 42;
+
+int __attribute__ ((noinline, noclone))
+normal_function (int x)
+{
+  return x + 1;
+}
+
+int __attribute__ ((noinline, noclone))
+tailcall_function (int x)
+{
+  ++global_var;
+  return normal_function (x);
+}
+
+int
+main (void)
+{
+  int result = tailcall_function (42);
+  result -= global_var;			/* Temporary breakpoint here.  */
+  return result;
+}
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
new file mode 100644
index 00000000000..e5b5ebc5183
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
@@ -0,0 +1,93 @@
+# 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/>.
+
+# Check that FinishBreakpoints are reached when set for a tailcall
+# frame.
+#
+# Frame #0 is never a tailcall frame.  As such, we need to do more
+# than just stop in a tailcall function and create a finish
+# breakpoint.  Instead, we need to stop in a function called from a
+# tail call function, then walk back up the stack and create the
+# finish breakpoint on the tail call frame.
+#
+# At one point, GDB would create the breakpoint in the correct
+# location, but set the frame-id on the breakpoint for the wrong
+# frame, with the result that the breakpoint would never trigger.
+
+load_lib gdb-python.exp
+
+require allow_python_tests
+
+standard_testfile
+
+if {[build_executable "failed to build" $testfile $srcfile \
+	 {debug optimize=-O2}]} {
+    return
+}
+
+# For remote host testing.
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Run to 'normal_function' and then try to create a FinishBreakpoint
+# in the parent frame.
+proc run_test {} {
+    clean_restart $::testfile
+
+    if {![runto normal_function]} {
+	return
+    }
+
+    gdb_test "bt" \
+	[multi_line \
+	     "#0\\s+normal_function\[^\r\n\]+" \
+	     "#1\\s+(?:$::hex in )?tailcall_function\[^\r\n\]+" \
+	     "#2\\s+(?:$::hex in )?main\[^\r\n\]+"] \
+	"check call stack"
+
+    gdb_test "source $::pyfile" "Python script imported" "import python scripts"
+
+    set lineno [gdb_get_line_number "Temporary breakpoint here."]
+    gdb_test "python MyFinishBreakpoint(gdb.selected_frame().older())" \
+	"Temporary breakpoint $::decimal at $::hex: file \[^\r\n\]+/$::srcfile, line $lineno\\." \
+	"create finish breakpoint"
+
+    set saw_stopped_message false
+    set saw_breakpoint_line false
+    set saw_source_line false
+    gdb_test_multiple "continue" "" {
+	-re "^Stopped at MyFinishBreakpoint\r\n" {
+	    set saw_stopped_message true
+	    exp_continue
+	}
+	-re "^Breakpoint $::decimal, main \\(\\) at \[^\r\n\]+/$::srcfile:$lineno\r\n" {
+	    set saw_breakpoint_line true
+	    exp_continue
+	}
+	-re "^$lineno\\s+\[^\r\n\]+\r\n" {
+	    set saw_source_line true
+	    exp_continue
+	}
+	-re "^$::gdb_prompt $" {
+	    gdb_assert { $saw_stopped_message \
+			     && $saw_breakpoint_line \
+			     && $saw_source_line } $gdb_test_name
+	}
+	-re "^\[^\r\n\]*\r\n" {
+	    exp_continue
+	}
+    }
+}
+
+run_test
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
new file mode 100644
index 00000000000..bd10109a3dc
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
@@ -0,0 +1,26 @@
+# 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/>.
+
+
+import gdb
+
+
+class MyFinishBreakpoint(gdb.FinishBreakpoint):
+    def stop(self):
+        print("Stopped at MyFinishBreakpoint")
+        return True
+
+
+print("Python script imported")
-- 
2.25.4


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH 2/4] gdb/python: fix FinishBreakpoint.return_value for tail call functions
  2026-01-24 11:29 [PATCH 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
  2026-01-24 11:29 ` [PATCH 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls Andrew Burgess
@ 2026-01-24 11:29 ` Andrew Burgess
  2026-01-24 11:29 ` [PATCH 3/4] gdb/python: don't allow FinishBreakpoints for inline frames Andrew Burgess
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 14+ messages in thread
From: Andrew Burgess @ 2026-01-24 11:29 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

The FinishBreakpoint.return_value attribute will not be populated
correctly for tail call functions.

In bpfinishpy_init (python/py-finishbreakpoint.c) we use the function
get_frame_pc_if_available to return an address, and then use this
address to lookup a function symbol.

The problem is that, for tail call functions, the address returned by
get_frame_pc_if_available can be outside the bounds of the function,
as a result GDB might find no function symbol at all, or might find
the wrong function symbol, if the tail call function is immediately
adjacent to the next function.

Fix this by using get_frame_address_in_block_if_available instead.
For tail call functions this will return an address within the bounds
of the function, which means that GDB should find the correct function
symbol, and from this the correct return type.

I've extended the existing FinishBreakpoint with tail call test case
to include printing the return value, this test fails without this
patch, but now works.
---
 gdb/python/py-finishbreakpoint.c                          | 6 +++---
 .../gdb.python/py-finish-breakpoint-tailcall.exp          | 8 +++++++-
 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py | 1 +
 3 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/gdb/python/py-finishbreakpoint.c b/gdb/python/py-finishbreakpoint.c
index 14e9f18c863..84d2abeb433 100644
--- a/gdb/python/py-finishbreakpoint.c
+++ b/gdb/python/py-finishbreakpoint.c
@@ -174,7 +174,6 @@ bpfinishpy_init (PyObject *self, PyObject *args, PyObject *kwargs)
   struct frame_id frame_id;
   PyObject *internal = NULL;
   int internal_bp = 0;
-  std::optional<CORE_ADDR> pc;
 
   if (!gdb_PyArg_ParseTupleAndKeywords (args, kwargs, "|OO", keywords,
 					&frame_obj, &internal))
@@ -248,9 +247,10 @@ bpfinishpy_init (PyObject *self, PyObject *args, PyObject *kwargs)
 
   try
     {
-      if ((pc = get_frame_pc_if_available (frame)))
+      CORE_ADDR pc;
+      if (get_frame_address_in_block_if_available (frame, &pc))
 	{
-	  struct symbol *function = find_symbol_for_pc (*pc);
+	  struct symbol *function = find_symbol_for_pc (pc);
 	  if (function != nullptr)
 	    {
 	      struct type *ret_type =
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
index e5b5ebc5183..2ae61f389ef 100644
--- a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
@@ -66,11 +66,16 @@ proc run_test {} {
     set saw_stopped_message false
     set saw_breakpoint_line false
     set saw_source_line false
+    set saw_return_value false
     gdb_test_multiple "continue" "" {
 	-re "^Stopped at MyFinishBreakpoint\r\n" {
 	    set saw_stopped_message true
 	    exp_continue
 	}
+	-re "^Return value is 43\r\n" {
+	    set saw_return_value true
+	    exp_continue
+	}
 	-re "^Breakpoint $::decimal, main \\(\\) at \[^\r\n\]+/$::srcfile:$lineno\r\n" {
 	    set saw_breakpoint_line true
 	    exp_continue
@@ -82,7 +87,8 @@ proc run_test {} {
 	-re "^$::gdb_prompt $" {
 	    gdb_assert { $saw_stopped_message \
 			     && $saw_breakpoint_line \
-			     && $saw_source_line } $gdb_test_name
+			     && $saw_source_line \
+			     && $saw_return_value } $gdb_test_name
 	}
 	-re "^\[^\r\n\]*\r\n" {
 	    exp_continue
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
index bd10109a3dc..e493dfa4017 100644
--- a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
@@ -20,6 +20,7 @@ import gdb
 class MyFinishBreakpoint(gdb.FinishBreakpoint):
     def stop(self):
         print("Stopped at MyFinishBreakpoint")
+        print("Return value is {}".format(self.return_value))
         return True
 
 
-- 
2.25.4


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH 3/4] gdb/python: don't allow FinishBreakpoints for inline frames
  2026-01-24 11:29 [PATCH 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
  2026-01-24 11:29 ` [PATCH 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls Andrew Burgess
  2026-01-24 11:29 ` [PATCH 2/4] gdb/python: fix FinishBreakpoint.return_value for tail call functions Andrew Burgess
@ 2026-01-24 11:29 ` Andrew Burgess
  2026-01-24 12:23   ` Eli Zaretskii
  2026-01-24 11:29 ` [PATCH 4/4] gdb/python: fix gdb.FinishBreakpoint returning to a tail call frame Andrew Burgess
  2026-03-05 13:37 ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
  4 siblings, 1 reply; 14+ messages in thread
From: Andrew Burgess @ 2026-01-24 11:29 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

Creating a Python gdb.FinishBreakpoint for an inline frame doesn't
work.

If we look at the 'finish' command, in the finish_command
function (infcmd.c) then we see that GDB handles inline frames very
different to non-inline frames.

For non-inline frames GDB creates a temporary breakpoint and then
resumes the inferior until the breakpoint is hit.

But for inline frames, GDB steps forward until we have left the inline
frame.

When it comes to gdb.FinishBreakpoint we only have the "create a
temporary breakpoint" mechanism; that is, after all, what the
FinishBreakpoint is, it's a temporary breakpoint placed at the
return address in the caller.

Currently, when a FinishBreakpoint is created within an inline frame,
GDB ends up creating the breakpoint at the current $pc.  As a result
the breakpoint will not be hit before the current function
exits (unless there's a loop going on, but that's not the point).

We could imagine what a solution to this problem would look like, GDB
would need to figure out the set of addresses for all possible exit
points from the inline function, and place a breakpoint at each of
these locations.  I don't propose doing that in this commit.

Instead, I plan to update the docs to note that creating a
FinishBreakpoint within an inline frame is not allowed, and I will
catch this case within bpfinishpy_init (python/py-finishbreakpoint.c)
and throw an error.

Though the error is new, all I'm doing is raising an error for a case
that never worked.

There's a new test to cover this case.
---
 gdb/doc/python.texi                           |   3 +
 gdb/python/py-finishbreakpoint.c              |   8 +-
 .../gdb.python/py-finish-breakpoint-inline.c  |  58 +++++++
 .../py-finish-breakpoint-inline.exp           | 147 ++++++++++++++++++
 .../gdb.python/py-finish-breakpoint-inline.py |  27 ++++
 5 files changed, 242 insertions(+), 1 deletion(-)
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py

diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 1f88ea7e9ad..eccb7e523c7 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -7221,6 +7221,9 @@ Finish Breakpoints in Python
 Finish breakpoints are thread specific and must be create with the right
 thread selected.
 
+It is not possible to create a @code{gdb.FinishBreakpoint} for an
+inline frame (@pxref{Inline Functions}).
+
 @defun FinishBreakpoint.__init__ (@r{[}frame@r{]} @r{[}, internal@r{]})
 Create a finish breakpoint at the return address of the @code{gdb.Frame}
 object @var{frame}.  If @var{frame} is not provided, this defaults to the
diff --git a/gdb/python/py-finishbreakpoint.c b/gdb/python/py-finishbreakpoint.c
index 84d2abeb433..84d55c470a2 100644
--- a/gdb/python/py-finishbreakpoint.c
+++ b/gdb/python/py-finishbreakpoint.c
@@ -192,10 +192,16 @@ bpfinishpy_init (PyObject *self, PyObject *args, PyObject *kwargs)
 	  PyErr_SetString (PyExc_ValueError,
 			   _("Invalid ID for the `frame' object."));
 	}
+      else if (get_frame_type (frame) == INLINE_FRAME)
+	{
+	  PyErr_SetString
+	    (PyExc_ValueError,
+	     _("Unable to create FinishBreakpoint for inline frame."));
+	}
       else
 	{
 	  prev_frame = get_prev_frame (frame);
-	  if (prev_frame == 0)
+	  if (prev_frame == nullptr)
 	    {
 	      PyErr_SetString (PyExc_ValueError,
 			       _("\"FinishBreakpoint\" not "
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c
new file mode 100644
index 00000000000..c955917a873
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c
@@ -0,0 +1,58 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+/* Global used to create filler code within functions.  */
+volatile int global_var = 1;
+
+static int  __attribute__ ((noinline, noclone))
+baz (int arg)
+{
+  arg += global_var;
+  return arg;
+}
+
+static inline int __attribute__ ((__always_inline__))
+bar (int arg)
+{
+  arg += global_var;
+  arg = baz (arg);		/* Finish location.  */
+  arg -= global_var;
+  return arg;
+}
+
+static inline int __attribute__ ((__always_inline__))
+foo (int arg)
+{
+  arg += global_var;
+  arg = bar (arg);
+  arg -= global_var;
+  return arg;
+}
+
+int
+main (void)
+{
+  int ans;
+
+  ++global_var;
+  ++global_var;
+  ans = foo (42);
+  ++global_var;
+  ++global_var;
+  ans += global_var;
+  return ans;		/* Final breakpoint.  */
+}
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp
new file mode 100644
index 00000000000..d1c3f3de8f0
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp
@@ -0,0 +1,147 @@
+# 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/>.
+
+# Check that attempting to create a FinishBreakpoint within an inline
+# function will fail.
+#
+# For inline functions the 'finish' command steps forward until we are
+# outside the inline function.
+#
+# For FinishBreakpoints though we need to pick an address an place a
+# breakpoint there.  Currently GDB doesn't know where to place such a
+# breakpoint for an inline function, so our solution is to prevent
+# creation of FinishBreakpoints for inline frames.
+
+load_lib gdb-python.exp
+
+require allow_python_tests
+
+standard_testfile
+
+if {[prepare_for_testing "failed to prepare" $testfile $srcfile]} {
+    return
+}
+
+if {![runto_main]} {
+    return
+}
+
+# Source the Python script.
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+gdb_test "source $pyfile" "Python script imported" "import python scripts"
+
+# Breakpoint locations needed for this test.
+gdb_breakpoint foo
+gdb_breakpoint bar
+gdb_breakpoint baz
+set final_lineno [gdb_get_line_number "Final breakpoint."]
+gdb_breakpoint $final_lineno
+
+# Depending on how the code is compiled, and exactly where the finish
+# breakpoint is placed, the breakpoint could potentially be reported
+# on either of these lines.
+set lineno_1 [gdb_get_line_number "Finish location."]
+set lineno_2 [expr {$lineno_1 + 1}]
+set lineno_re "(?:$lineno_1|$lineno_2)"
+
+# Run to 'foo', which is an inline function called from a normal
+# function, and try to create a MyFinishBreakpoint.  This should fail.
+gdb_continue_to_breakpoint "breakpoint in foo"
+gdb_test "python MyFinishBreakpoint(gdb.selected_frame())" \
+    "Error occurred in Python: Unable to create FinishBreakpoint for inline frame\\." \
+    "try to create FinishBreakpoint for inline frame, caller is a normal frame"
+
+# Continue to 'bar', which is an inline function called from another
+# inline function, and try to create a MyFinishBreakpoint.  This
+# should fail.
+gdb_continue_to_breakpoint "breakpoint in bar"
+gdb_test "python MyFinishBreakpoint(gdb.selected_frame())" \
+    "Error occurred in Python: Unable to create FinishBreakpoint for inline frame\\." \
+    "try to create FinishBreakpoint for inline frame, caller is an inline frame"
+
+# Continue to 'baz', which is a normal function called from an inline
+# function, and create a MyFinishBreakpoint, which we expect to succeed.
+gdb_continue_to_breakpoint "breakpoint in baz"
+gdb_test "python MyFinishBreakpoint(gdb.selected_frame())" \
+    "Temporary breakpoint $decimal at $hex: file \[^\r\n\]+/$srcfile, line $lineno_re\\." \
+    "create FinishBreakpoint normal function, caller is an inline frame"
+
+# Continue and make sure we hit the MyFinishBreakpoint.
+set saw_finish_breakpoint false
+set saw_return_value false
+set saw_breakpoint_location false
+set saw_source_line false
+gdb_test_multiple "continue" "continue to finish breakpoint" {
+    -re "^Stopped at MyFinishBreakpoint\r\n" {
+	set saw_finish_breakpoint true
+	exp_continue
+    }
+    -re "^Return value is 51\r\n" {
+	set saw_return_value true
+	exp_continue
+    }
+    -re "^Breakpoint $decimal, ($hex in )?bar \\(arg=$decimal\\) at \[^\r\n\]+/$srcfile:$lineno_re\r\n" {
+	set saw_breakpoint_location true
+	exp_continue
+    }
+    -re "^$lineno_re\\s+\[^\r\n\]+\r\n" {
+	set saw_source_line true
+	exp_continue
+    }
+    -re "^$gdb_prompt $" {
+	gdb_assert {$saw_finish_breakpoint \
+			&& $saw_return_value \
+			&& $saw_breakpoint_location \
+			&& $saw_source_line } $gdb_test_name
+    }
+    -re "^\[^\r\n\]*\r\n" {
+	exp_continue
+    }
+}
+
+# Continue to the final breakpoint location.  We don't expect to see
+# any of the MyFinishBreakpoint output here.  If we do then we've hit
+# an unexpected FinishBreakpoint.
+set saw_finish_breakpoint false
+set saw_return_value false
+set saw_breakpoint_location false
+set saw_source_line false
+gdb_test_multiple "continue" "continue to final breakpoint" {
+    -re "^Stopped at MyFinishBreakpoint\r\n" {
+	set saw_finish_breakpoint true
+	exp_continue
+    }
+    -re "^Return value is 51\r\n" {
+	set saw_return_value true
+	exp_continue
+    }
+    -re "^Breakpoint $decimal, main \\(\\) at \[^\r\n\]+/$srcfile:$final_lineno\r\n" {
+	set saw_breakpoint_location true
+	exp_continue
+    }
+    -re "^$final_lineno\\s+\[^\r\n\]+\r\n" {
+	set saw_source_line true
+	exp_continue
+    }
+    -re "^$gdb_prompt $" {
+	gdb_assert {!$saw_finish_breakpoint \
+			&& !$saw_return_value \
+			&& $saw_breakpoint_location \
+			&& $saw_source_line } $gdb_test_name
+    }
+    -re "^\[^\r\n\]*\r\n" {
+	exp_continue
+    }
+}
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py
new file mode 100644
index 00000000000..e493dfa4017
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py
@@ -0,0 +1,27 @@
+# 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/>.
+
+
+import gdb
+
+
+class MyFinishBreakpoint(gdb.FinishBreakpoint):
+    def stop(self):
+        print("Stopped at MyFinishBreakpoint")
+        print("Return value is {}".format(self.return_value))
+        return True
+
+
+print("Python script imported")
-- 
2.25.4


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH 4/4] gdb/python: fix gdb.FinishBreakpoint returning to a tail call frame
  2026-01-24 11:29 [PATCH 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
                   ` (2 preceding siblings ...)
  2026-01-24 11:29 ` [PATCH 3/4] gdb/python: don't allow FinishBreakpoints for inline frames Andrew Burgess
@ 2026-01-24 11:29 ` Andrew Burgess
  2026-03-05 13:37 ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
  4 siblings, 0 replies; 14+ messages in thread
From: Andrew Burgess @ 2026-01-24 11:29 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

I noticed that gdb.FinishBreakpoint doesn't work if the parent
function is a tail call function.  In bpfinishpy_init we use
get_frame_pc to find the address at which the finish breakpoint should
be placed within the previous frame.

However, if the previous frame is a tail call frame, then get_frame_pc
will return an address outside of the tail call function, an address
which will not be reached on the return path.

Unlike other recent tail call fixes I've made, we cannot switch to
using something like get_frame_address_in_block here as in the tail
call case this will return an address within the function, but not an
address that will be executed when we return.

What we need to do in the tail call case is create the finish
breakpoint in the frame that called the tail call function.  Or if
that frame is itself a tail call, then we should walk back up the call
stack until we find a non-tail call function.

This can be achieved by adding a call to skip_tailcall_frames into
bpfinishpy_init after our existing call to get_prev_frame.

I've extended the existing test case to cover this additional
situation.
---
 gdb/python/py-finishbreakpoint.c              |  3 ++
 .../py-finish-breakpoint-tailcall.exp         | 29 +++++++++++++++----
 2 files changed, 27 insertions(+), 5 deletions(-)

diff --git a/gdb/python/py-finishbreakpoint.c b/gdb/python/py-finishbreakpoint.c
index 84d55c470a2..cc864f2e85c 100644
--- a/gdb/python/py-finishbreakpoint.c
+++ b/gdb/python/py-finishbreakpoint.c
@@ -201,6 +201,9 @@ bpfinishpy_init (PyObject *self, PyObject *args, PyObject *kwargs)
       else
 	{
 	  prev_frame = get_prev_frame (frame);
+	  if (prev_frame != nullptr)
+	    prev_frame = skip_tailcall_frames (prev_frame);
+
 	  if (prev_frame == nullptr)
 	    {
 	      PyErr_SetString (PyExc_ValueError,
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
index 2ae61f389ef..6a266abd790 100644
--- a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
@@ -40,9 +40,17 @@ if {[build_executable "failed to build" $testfile $srcfile \
 # For remote host testing.
 set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
 
-# Run to 'normal_function' and then try to create a FinishBreakpoint
-# in the parent frame.
-proc run_test {} {
+# Run to 'normal_function' and then try to create a FinishBreakpoint.
+#
+# When USE_PARENT_FRAME_P is true we use the parent frame, which is
+# for tailcall_function, to create the FinishBreakpoint.
+#
+# When USE_PARENT_FRAME_P is false we use the frame of normal_function
+# to create the FinishBreakpoint.
+#
+# In both cases the finish breakpoint should be placed back in main,
+# which is where the inferior should stop when resumed.
+proc run_test { use_parent_frame_p } {
     clean_restart $::testfile
 
     if {![runto normal_function]} {
@@ -59,7 +67,16 @@ proc run_test {} {
     gdb_test "source $::pyfile" "Python script imported" "import python scripts"
 
     set lineno [gdb_get_line_number "Temporary breakpoint here."]
-    gdb_test "python MyFinishBreakpoint(gdb.selected_frame().older())" \
+
+    if { $use_parent_frame_p } {
+	gdb_test_no_output "python frame=gdb.selected_frame().older()" \
+	    "store a reference to the parent frame"
+    } else {
+	gdb_test_no_output "python frame=gdb.selected_frame()" \
+	    "store a reference to the current frame"
+    }
+
+    gdb_test "python MyFinishBreakpoint(frame)" \
 	"Temporary breakpoint $::decimal at $::hex: file \[^\r\n\]+/$::srcfile, line $lineno\\." \
 	"create finish breakpoint"
 
@@ -96,4 +113,6 @@ proc run_test {} {
     }
 }
 
-run_test
+foreach_with_prefix parent_frame { true false } {
+    run_test $parent_frame
+}
-- 
2.25.4


^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH 3/4] gdb/python: don't allow FinishBreakpoints for inline frames
  2026-01-24 11:29 ` [PATCH 3/4] gdb/python: don't allow FinishBreakpoints for inline frames Andrew Burgess
@ 2026-01-24 12:23   ` Eli Zaretskii
  0 siblings, 0 replies; 14+ messages in thread
From: Eli Zaretskii @ 2026-01-24 12:23 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

> From: Andrew Burgess <aburgess@redhat.com>
> Cc: Andrew Burgess <aburgess@redhat.com>
> Date: Sat, 24 Jan 2026 11:29:13 +0000
> 
>  gdb/doc/python.texi                           |   3 +
>  gdb/python/py-finishbreakpoint.c              |   8 +-
>  .../gdb.python/py-finish-breakpoint-inline.c  |  58 +++++++
>  .../py-finish-breakpoint-inline.exp           | 147 ++++++++++++++++++
>  .../gdb.python/py-finish-breakpoint-inline.py |  27 ++++
>  5 files changed, 242 insertions(+), 1 deletion(-)
>  create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c
>  create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp
>  create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py

Thanks, the python.texi part is okay.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>

^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls
  2026-01-24 11:29 ` [PATCH 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls Andrew Burgess
@ 2026-01-27 16:37   ` Andrew Burgess
  0 siblings, 0 replies; 14+ messages in thread
From: Andrew Burgess @ 2026-01-27 16:37 UTC (permalink / raw)
  To: gdb-patches


In another thread Tom T. pointed my to bug PR gdb/28683 which should (I
think) be fixed by this patch.

I've updated the commit message to mention this bug, and add the Bug:
link.  I've not changed anything else about the patch.

Thanks,
Andrew

---

commit 4222ade61869c7550a4ed28ab81ff58dc70069c5
Author: Andrew Burgess <aburgess@redhat.com>
Date:   Thu Jan 22 17:51:11 2026 +0000

    gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls
    
    The 3 frame_unwind_caller_WHAT functions:
    
      + frame_unwind_caller_id
      + frame_unwind_caller_pc
      + frame_unwind_caller_arch
    
    Are, I believe, currently all broken with respect to inline and tail
    call functions.
    
    The Python FinishBreakpoint type creates a breakpoint in the caller
    function which, when triggered, indicates that the FinishBreakpoint
    has gone out of scope.
    
    I was writing a test for the FinishBreakpoint type which included a
    tail call function, and the FinishBreakpoint was being created for the
    tail call function frame.  What I observed is that the out of scope
    breakpoint was never being hit.
    
    The call stack in my new test looked like this:
    
      main -> tailcall_function -> normal_function
    
    I would stop in normal_function, and then create a FinishBreakpoint
    for the parent (tailcall_function) frame.  The FinishBreakpoint's out
    of scope breakpoint was being correctly placed in the 'main' function,
    but would never trigger.
    
    The problem is that the breakpoint placed in 'main' holds a frame-id.
    This frame-id is the frame in which the breakpoint should trigger.
    This frame-id exists to prevent premature stops due to recursion.  But
    in this case, when the breakpoint in 'main' was hit, despite no
    recursion having occurred, the frame-id didn't match, and so the
    breakpoint was ignored.
    
    The problem is that in bpfinishpy_init we call frame_unwind_caller_id
    to compute the frame-id of the frame in which we should stop, and
    frame_unwind_caller_id was returning the wrong frame-id.  As far as I
    can tell frame_unwind_caller_id has been broken since it was updated
    for inline functions in commit edb3359dff90ef8a.
    
    The frame_unwind_caller_id function, and all the
    frame_unwind_caller_WHAT functions, are intended to return the
    previous frame, but should skip over any inline, or tail call frames.
    Let's look at an example call stack:
    
      #0 A          // A normal function.
      #1 B          // An inline function.
      #2 C          // An inline function.
      #3 D          // A normal function.
      #4 E          // A normal function.
    
    Starting from #0, a normal function, frame_unwind_caller_id, should
    return the frame-id for #3, and this is what happens.
    
    But if we start in #1 and call frame_unwind_caller_id, then we should
    still return the frame-id for #3, but this is not what happens.
    Instead we return the frame-id for #4, skipping a frame.
    
    The problem is that frame_unwind_caller_id starts by calling
    skip_artificial_frames, which calls get_prev_frame_always until we
    reach a non-inline (or non-tail call) frame, this moves us from #1 to
    
    Then, back in frame_unwind_caller_id we call get_prev_frame_always,
    which moves us to #4.
    
    Then frame_unwind_caller_id finishes with a call to
    skip_artificial_frames, this could potentially result in additional
    frames being skipped, but in my example above this isn't the case.
    
    The problem here is that if skip_artificial_frames skips anything,
    then we have already unwound to the caller frame, and the
    get_prev_frame_always call in frame_unwind_caller_id is unnecessary.
    
    I propose to add a new helper function frame_unwind_caller_frame,
    which should do the correct thing; it unwinds one frame and then calls
    skip_artificial_frames.  This should do exactly what is needed.
    
    Then all the frame_unwind_caller_WHAT functions will be updated to use
    this helper function, and just extract the required property from the
    resulting frame.
    
    With this fix in place I could then write the FinishBreakpoint test,
    which now works.
    
    I took a look for other places where frame_unwind_caller_id is used
    and spotted that the 'until' command does much the same thing, placing
    a breakpoint in the caller frame.  As predicted, the 'until' command
    is also broken when used within a tail call frame.  This patch fixes
    that issue too.  There's also a test for the until command.
    
    The bug PR gdb/28683 seems to describe this exact problem with a
    specific AArch64 case given.  I haven't actually setup the environment
    needed to test this bug, but I'm reasonably sure that this patch will
    fix the bug.  Even if it doesn't then it's certainly related and worth
    linking into the bug report.
    
    Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=28683

diff --git a/gdb/frame.c b/gdb/frame.c
index 8cb1d0a5c42..f80b3e281b6 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -699,6 +699,23 @@ get_stack_frame_id (const frame_info_ptr &next_frame)
   return get_frame_id (skip_artificial_frames (next_frame));
 }
 
+/* Helper for the various frame_unwind_caller_* functions.  Unwind
+   INITIAL_NEXT_FRAME at least one frame, but skip any artificial frames,
+   that is inline or tailcall frames.
+
+   Return the Nth previous frame (as required to find a non-artificial
+   frame), or NULL if no previous frame could be found.  */
+
+static frame_info_ptr
+frame_unwind_caller_frame (const frame_info_ptr &initial_next_frame)
+{
+  frame_info_ptr this_frame = get_prev_frame_always (initial_next_frame);
+  if (this_frame == nullptr)
+    return nullptr;
+
+  return skip_artificial_frames (this_frame);
+}
+
 struct frame_id
 frame_unwind_caller_id (const frame_info_ptr &initial_next_frame)
 {
@@ -707,15 +724,11 @@ frame_unwind_caller_id (const frame_info_ptr &initial_next_frame)
      unintentionally returning a null_frame_id (e.g., when a caller
      requests the frame ID of "main()"s caller.  */
 
-  frame_info_ptr next_frame = skip_artificial_frames (initial_next_frame);
-  if (next_frame == NULL)
+  frame_info_ptr this_frame = frame_unwind_caller_frame (initial_next_frame);
+  if (this_frame == nullptr)
     return null_frame_id;
 
-  frame_info_ptr this_frame = get_prev_frame_always (next_frame);
-  if (this_frame)
-    return get_frame_id (skip_artificial_frames (this_frame));
-  else
-    return null_frame_id;
+  return get_frame_id (this_frame);
 }
 
 const struct frame_id null_frame_id = { 0 }; /* All zeros.  */
@@ -1074,14 +1087,14 @@ frame_unwind_pc (const frame_info_ptr &this_frame)
 CORE_ADDR
 frame_unwind_caller_pc (const frame_info_ptr &initial_this_frame)
 {
-  frame_info_ptr this_frame = skip_artificial_frames (initial_this_frame);
+  frame_info_ptr this_frame = frame_unwind_caller_frame (initial_this_frame);
 
   /* We must have a non-artificial frame.  The caller is supposed to check
      the result of frame_unwind_caller_id (), which returns NULL_FRAME_ID
      in this case.  */
   gdb_assert (this_frame != nullptr);
 
-  return frame_unwind_pc (this_frame);
+  return get_frame_pc (this_frame);
 }
 
 bool
@@ -3135,14 +3148,14 @@ frame_unwind_arch (const frame_info_ptr &next_frame)
 struct gdbarch *
 frame_unwind_caller_arch (const frame_info_ptr &initial_next_frame)
 {
-  frame_info_ptr next_frame = skip_artificial_frames (initial_next_frame);
+  frame_info_ptr next_frame = frame_unwind_caller_frame (initial_next_frame);
 
   /* We must have a non-artificial frame.  The caller is supposed to check
      the result of frame_unwind_caller_id (), which returns NULL_FRAME_ID
      in this case.  */
   gdb_assert (next_frame != nullptr);
 
-  return frame_unwind_arch (next_frame);
+  return get_frame_arch (next_frame);
 }
 
 /* Gets the language of FRAME.  */
diff --git a/gdb/testsuite/gdb.base/until-in-tailcall.c b/gdb/testsuite/gdb.base/until-in-tailcall.c
new file mode 100644
index 00000000000..3e3853ba6df
--- /dev/null
+++ b/gdb/testsuite/gdb.base/until-in-tailcall.c
@@ -0,0 +1,53 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+/* This test relies on tailcall_function actually compiling to a tail call
+   function.  we try to force this by preventing tailcall_function and
+   normal_function from being inlined, then compiling this file at -O2.
+   Still, that's no guarantee.  If tailcall_function isn't a tail call,
+   then the test becomes pointless.  */
+
+volatile int global_var = 42;
+
+int __attribute__ ((noinline, noclone))
+normal_function (int x)
+{
+  return x + 1;
+}
+
+int __attribute__ ((noinline, noclone))
+tailcall_function (int x)
+{
+  return normal_function (x);
+}
+
+void __attribute__ ((noinline, noclone))
+worker_func (void)
+{
+  ++global_var;
+  ++global_var;			/* Run until here.  */
+  ++global_var;
+}
+
+int
+main (void)
+{
+  int result = tailcall_function (42);
+  result -= global_var;		/* Should stop here.  */
+  worker_func ();
+  return result;
+}
diff --git a/gdb/testsuite/gdb.base/until-in-tailcall.exp b/gdb/testsuite/gdb.base/until-in-tailcall.exp
new file mode 100644
index 00000000000..cd66d0ceaf8
--- /dev/null
+++ b/gdb/testsuite/gdb.base/until-in-tailcall.exp
@@ -0,0 +1,108 @@
+# 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/>.
+
+# Run until we have a tail call frame in the stack.  The stack looks like this:
+#
+# main -> tailcall_function -> normal_function
+#
+# We stop on normal_function, and then use 'up' to go to the
+# tailcall_function frame.  We then use 'until NN' where 'NN' is a
+# line that should not be reached until after the inferior has
+# returned to main.
+#
+# However, when the inferior returns to main, GDB should stop the
+# inferior as the 'until' has gone out of scope.
+#
+# At one point this was not happening as GDB would place the out of
+# scope 'until' guard breakpoint in the wrong place.
+#
+# This test relies on the source file compiling with a tail call in
+# place.
+
+standard_testfile
+
+if {[prepare_for_testing "failed to prepare" $testfile $srcfile \
+	 {debug optimize=-O2}]} {
+    return
+}
+
+# Run until there is a tail call frame in the backtrace.  Then use the
+# 'until' command with a line that will not be reached until after the
+# current stack has unwound back to main.  We expect the until's out
+# of scope breakpoint to be hit first.
+#
+# When USE_PARENT_FRAME_P is true we move up to the tail call frame
+# and issue the 'until' command while that frame is selected.
+#
+# When USE_PARENT_FRAME_P is false we issue the 'until' command while
+# normal_function is selected, this function is called from the tail
+# call function.
+#
+# In both cases, we expect the out of scope breakpoint to be placed
+# back in main, which is where the inferior should be stopped.
+proc run_test { use_parent_frame_p } {
+    clean_restart $::testfile
+
+    if {![runto normal_function]} {
+	return
+    }
+
+    # Check the stack looks correct.
+    gdb_test "bt" \
+	[multi_line \
+	     "#0\\s+normal_function\[^\r\n\]+" \
+	     "#1\\s+(?:$::hex in )?tailcall_function\[^\r\n\]+" \
+	     "#2\\s+(?:$::hex in )?main\[^\r\n\]+"] \
+	"check call stack"
+
+    # Move up to tailcall_function if needed.
+    if { $use_parent_frame_p } {
+	gdb_test "up" ".* in tailcall_function .*" \
+	    "move up to tailcall_function"
+    }
+
+    # Where we ask the 'until' to run to.
+    set until_lineno [gdb_get_line_number "Run until here."]
+
+    # Where we actually expect the inferior to stop.
+    set stop_lineno [gdb_get_line_number "Should stop here."]
+
+    # Issue the 'until' command and check that the inferior stops in main
+    # when the until guard breakpoint notices we have left the
+    # tailcall_function.
+    set saw_stop_location false
+    set saw_stop_source false
+    gdb_test_multiple "until $until_lineno" "run until worker_func" {
+	-re "^until $until_lineno\r\n" {
+	    exp_continue
+	}
+	-re "^$::gdb_prompt $" {
+	    gdb_assert { $saw_stop_location && $saw_stop_source } \
+		$gdb_test_name
+	}
+	-re "^main \\(\\) at \[^\r\n\]+/$::srcfile:$stop_lineno\r\n" {
+	    set saw_stop_location true
+	    exp_continue
+	}
+	-re "^$stop_lineno\\s+\[^\r\n\]+\r\n" {
+	    set saw_stop_source true
+	    exp_continue
+	}
+    }
+}
+
+foreach_with_prefix use_parent_frame { true false } {
+    run_test $use_parent_frame
+}
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c
new file mode 100644
index 00000000000..d129d7e41b4
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c
@@ -0,0 +1,45 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+/* This test relies on tailcall_function actually compiling to a tail call
+   function.  we try to force this by preventing tailcall_function and
+   normal_function from being inlined, then compiling this file at -O2.
+   Still, that's no guarantee.  If tailcall_function isn't a tail call,
+   then the test becomes pointless.  */
+
+volatile int global_var = 42;
+
+int __attribute__ ((noinline, noclone))
+normal_function (int x)
+{
+  return x + 1;
+}
+
+int __attribute__ ((noinline, noclone))
+tailcall_function (int x)
+{
+  ++global_var;
+  return normal_function (x);
+}
+
+int
+main (void)
+{
+  int result = tailcall_function (42);
+  result -= global_var;			/* Temporary breakpoint here.  */
+  return result;
+}
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
new file mode 100644
index 00000000000..e5b5ebc5183
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
@@ -0,0 +1,93 @@
+# 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/>.
+
+# Check that FinishBreakpoints are reached when set for a tailcall
+# frame.
+#
+# Frame #0 is never a tailcall frame.  As such, we need to do more
+# than just stop in a tailcall function and create a finish
+# breakpoint.  Instead, we need to stop in a function called from a
+# tail call function, then walk back up the stack and create the
+# finish breakpoint on the tail call frame.
+#
+# At one point, GDB would create the breakpoint in the correct
+# location, but set the frame-id on the breakpoint for the wrong
+# frame, with the result that the breakpoint would never trigger.
+
+load_lib gdb-python.exp
+
+require allow_python_tests
+
+standard_testfile
+
+if {[build_executable "failed to build" $testfile $srcfile \
+	 {debug optimize=-O2}]} {
+    return
+}
+
+# For remote host testing.
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Run to 'normal_function' and then try to create a FinishBreakpoint
+# in the parent frame.
+proc run_test {} {
+    clean_restart $::testfile
+
+    if {![runto normal_function]} {
+	return
+    }
+
+    gdb_test "bt" \
+	[multi_line \
+	     "#0\\s+normal_function\[^\r\n\]+" \
+	     "#1\\s+(?:$::hex in )?tailcall_function\[^\r\n\]+" \
+	     "#2\\s+(?:$::hex in )?main\[^\r\n\]+"] \
+	"check call stack"
+
+    gdb_test "source $::pyfile" "Python script imported" "import python scripts"
+
+    set lineno [gdb_get_line_number "Temporary breakpoint here."]
+    gdb_test "python MyFinishBreakpoint(gdb.selected_frame().older())" \
+	"Temporary breakpoint $::decimal at $::hex: file \[^\r\n\]+/$::srcfile, line $lineno\\." \
+	"create finish breakpoint"
+
+    set saw_stopped_message false
+    set saw_breakpoint_line false
+    set saw_source_line false
+    gdb_test_multiple "continue" "" {
+	-re "^Stopped at MyFinishBreakpoint\r\n" {
+	    set saw_stopped_message true
+	    exp_continue
+	}
+	-re "^Breakpoint $::decimal, main \\(\\) at \[^\r\n\]+/$::srcfile:$lineno\r\n" {
+	    set saw_breakpoint_line true
+	    exp_continue
+	}
+	-re "^$lineno\\s+\[^\r\n\]+\r\n" {
+	    set saw_source_line true
+	    exp_continue
+	}
+	-re "^$::gdb_prompt $" {
+	    gdb_assert { $saw_stopped_message \
+			     && $saw_breakpoint_line \
+			     && $saw_source_line } $gdb_test_name
+	}
+	-re "^\[^\r\n\]*\r\n" {
+	    exp_continue
+	}
+    }
+}
+
+run_test
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
new file mode 100644
index 00000000000..bd10109a3dc
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
@@ -0,0 +1,26 @@
+# 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/>.
+
+
+import gdb
+
+
+class MyFinishBreakpoint(gdb.FinishBreakpoint):
+    def stop(self):
+        print("Stopped at MyFinishBreakpoint")
+        return True
+
+
+print("Python script imported")


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints
  2026-01-24 11:29 [PATCH 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
                   ` (3 preceding siblings ...)
  2026-01-24 11:29 ` [PATCH 4/4] gdb/python: fix gdb.FinishBreakpoint returning to a tail call frame Andrew Burgess
@ 2026-03-05 13:37 ` Andrew Burgess
  2026-03-05 13:37   ` [PATCHv2 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls Andrew Burgess
                     ` (4 more replies)
  4 siblings, 5 replies; 14+ messages in thread
From: Andrew Burgess @ 2026-03-05 13:37 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

In v2:

  - Rebased to current upstream HEAD.

  - Retested.

I've been looking at places where we use get_frame_pc when we should
be using get_frame_address_in_block to handle tail calls correctly.
This got me looking at gdb.FinishBreakpoint, and I found a bunch of
things that needed fixing.

Thanks,
Andrew

---

Andrew Burgess (4):
  gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls
  gdb/python: fix FinishBreakpoint.return_value for tail call functions
  gdb/python: don't allow FinishBreakpoints for inline frames
  gdb/python: fix gdb.FinishBreakpoint returning to a tail call frame

 gdb/doc/python.texi                           |   3 +
 gdb/frame.c                                   |  35 +++--
 gdb/python/py-finishbreakpoint.c              |  17 +-
 gdb/testsuite/gdb.base/until-in-tailcall.c    |  53 +++++++
 gdb/testsuite/gdb.base/until-in-tailcall.exp  | 108 +++++++++++++
 .../gdb.python/py-finish-breakpoint-inline.c  |  58 +++++++
 .../py-finish-breakpoint-inline.exp           | 147 ++++++++++++++++++
 .../gdb.python/py-finish-breakpoint-inline.py |  27 ++++
 .../py-finish-breakpoint-tailcall.c           |  45 ++++++
 .../py-finish-breakpoint-tailcall.exp         | 118 ++++++++++++++
 .../py-finish-breakpoint-tailcall.py          |  27 ++++
 11 files changed, 623 insertions(+), 15 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/until-in-tailcall.c
 create mode 100644 gdb/testsuite/gdb.base/until-in-tailcall.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py


base-commit: 030f56e0e82cd98f52a4afa468e6612d3454c57f
-- 
2.25.4


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCHv2 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls
  2026-03-05 13:37 ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
@ 2026-03-05 13:37   ` Andrew Burgess
  2026-03-05 13:37   ` [PATCHv2 2/4] gdb/python: fix FinishBreakpoint.return_value for tail call functions Andrew Burgess
                     ` (3 subsequent siblings)
  4 siblings, 0 replies; 14+ messages in thread
From: Andrew Burgess @ 2026-03-05 13:37 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

The 3 frame_unwind_caller_WHAT functions:

  + frame_unwind_caller_id
  + frame_unwind_caller_pc
  + frame_unwind_caller_arch

Are, I believe, currently all broken with respect to inline and tail
call functions.

The Python FinishBreakpoint type creates a breakpoint in the caller
function which, when triggered, indicates that the FinishBreakpoint
has gone out of scope.

I was writing a test for the FinishBreakpoint type which included a
tail call function, and the FinishBreakpoint was being created for the
tail call function frame.  What I observed is that the out of scope
breakpoint was never being hit.

The call stack in my new test looked like this:

  main -> tailcall_function -> normal_function

I would stop in normal_function, and then create a FinishBreakpoint
for the parent (tailcall_function) frame.  The FinishBreakpoint's out
of scope breakpoint was being correctly placed in the 'main' function,
but would never trigger.

The problem is that the breakpoint placed in 'main' holds a frame-id.
This frame-id is the frame in which the breakpoint should trigger.
This frame-id exists to prevent premature stops due to recursion.  But
in this case, when the breakpoint in 'main' was hit, despite no
recursion having occurred, the frame-id didn't match, and so the
breakpoint was ignored.

The problem is that in bpfinishpy_init we call frame_unwind_caller_id
to compute the frame-id of the frame in which we should stop, and
frame_unwind_caller_id was returning the wrong frame-id.  As far as I
can tell frame_unwind_caller_id has been broken since it was updated
for inline functions in commit edb3359dff90ef8a.

The frame_unwind_caller_id function, and all the
frame_unwind_caller_WHAT functions, are intended to return the
previous frame, but should skip over any inline, or tail call frames.
Let's look at an example call stack:

  #0 A		// A normal function.
  #1 B		// An inline function.
  #2 C		// An inline function.
  #3 D		// A normal function.
  #4 E		// A normal function.

Starting from #0, a normal function, frame_unwind_caller_id, should
return the frame-id for #3, and this is what happens.

But if we start in #1 and call frame_unwind_caller_id, then we should
still return the frame-id for #3, but this is not what happens.
Instead we return the frame-id for #4, skipping a frame.

The problem is that frame_unwind_caller_id starts by calling
skip_artificial_frames, which calls get_prev_frame_always until we
reach a non-inline (or non-tail call) frame, this moves us from #1 to

Then, back in frame_unwind_caller_id we call get_prev_frame_always,
which moves us to #4.

Then frame_unwind_caller_id finishes with a call to
skip_artificial_frames, this could potentially result in additional
frames being skipped, but in my example above this isn't the case.

The problem here is that if skip_artificial_frames skips anything,
then we have already unwound to the caller frame, and the
get_prev_frame_always call in frame_unwind_caller_id is unnecessary.

I propose to add a new helper function frame_unwind_caller_frame,
which should do the correct thing; it unwinds one frame and then calls
skip_artificial_frames.  This should do exactly what is needed.

Then all the frame_unwind_caller_WHAT functions will be updated to use
this helper function, and just extract the required property from the
resulting frame.

With this fix in place I could then write the FinishBreakpoint test,
which now works.

I took a look for other places where frame_unwind_caller_id is used
and spotted that the 'until' command does much the same thing, placing
a breakpoint in the caller frame.  As predicted, the 'until' command
is also broken when used within a tail call frame.  This patch fixes
that issue too.  There's also a test for the until command.

The bug PR gdb/28683 seems to describe this exact problem with a
specific AArch64 case given.  I haven't actually setup the environment
needed to test this bug, but I'm reasonably sure that this patch will
fix the bug.  Even if it doesn't then it's certainly related and worth
linking into the bug report.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=28683
---
 gdb/frame.c                                   |  35 ++++--
 gdb/testsuite/gdb.base/until-in-tailcall.c    |  53 +++++++++
 gdb/testsuite/gdb.base/until-in-tailcall.exp  | 108 ++++++++++++++++++
 .../py-finish-breakpoint-tailcall.c           |  45 ++++++++
 .../py-finish-breakpoint-tailcall.exp         |  93 +++++++++++++++
 .../py-finish-breakpoint-tailcall.py          |  26 +++++
 6 files changed, 349 insertions(+), 11 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/until-in-tailcall.c
 create mode 100644 gdb/testsuite/gdb.base/until-in-tailcall.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py

diff --git a/gdb/frame.c b/gdb/frame.c
index 5509e5ab7d7..14d43f93e7f 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -699,6 +699,23 @@ get_stack_frame_id (const frame_info_ptr &next_frame)
   return get_frame_id (skip_artificial_frames (next_frame));
 }
 
+/* Helper for the various frame_unwind_caller_* functions.  Unwind
+   INITIAL_NEXT_FRAME at least one frame, but skip any artificial frames,
+   that is inline or tailcall frames.
+
+   Return the Nth previous frame (as required to find a non-artificial
+   frame), or NULL if no previous frame could be found.  */
+
+static frame_info_ptr
+frame_unwind_caller_frame (const frame_info_ptr &initial_next_frame)
+{
+  frame_info_ptr this_frame = get_prev_frame_always (initial_next_frame);
+  if (this_frame == nullptr)
+    return nullptr;
+
+  return skip_artificial_frames (this_frame);
+}
+
 struct frame_id
 frame_unwind_caller_id (const frame_info_ptr &initial_next_frame)
 {
@@ -707,15 +724,11 @@ frame_unwind_caller_id (const frame_info_ptr &initial_next_frame)
      unintentionally returning a null_frame_id (e.g., when a caller
      requests the frame ID of "main()"s caller.  */
 
-  frame_info_ptr next_frame = skip_artificial_frames (initial_next_frame);
-  if (next_frame == NULL)
+  frame_info_ptr this_frame = frame_unwind_caller_frame (initial_next_frame);
+  if (this_frame == nullptr)
     return null_frame_id;
 
-  frame_info_ptr this_frame = get_prev_frame_always (next_frame);
-  if (this_frame)
-    return get_frame_id (skip_artificial_frames (this_frame));
-  else
-    return null_frame_id;
+  return get_frame_id (this_frame);
 }
 
 const struct frame_id null_frame_id = { 0 }; /* All zeros.  */
@@ -1074,14 +1087,14 @@ frame_unwind_pc (const frame_info_ptr &this_frame)
 CORE_ADDR
 frame_unwind_caller_pc (const frame_info_ptr &initial_this_frame)
 {
-  frame_info_ptr this_frame = skip_artificial_frames (initial_this_frame);
+  frame_info_ptr this_frame = frame_unwind_caller_frame (initial_this_frame);
 
   /* We must have a non-artificial frame.  The caller is supposed to check
      the result of frame_unwind_caller_id (), which returns NULL_FRAME_ID
      in this case.  */
   gdb_assert (this_frame != nullptr);
 
-  return frame_unwind_pc (this_frame);
+  return get_frame_pc (this_frame);
 }
 
 bool
@@ -3135,14 +3148,14 @@ frame_unwind_arch (const frame_info_ptr &next_frame)
 struct gdbarch *
 frame_unwind_caller_arch (const frame_info_ptr &initial_next_frame)
 {
-  frame_info_ptr next_frame = skip_artificial_frames (initial_next_frame);
+  frame_info_ptr next_frame = frame_unwind_caller_frame (initial_next_frame);
 
   /* We must have a non-artificial frame.  The caller is supposed to check
      the result of frame_unwind_caller_id (), which returns NULL_FRAME_ID
      in this case.  */
   gdb_assert (next_frame != nullptr);
 
-  return frame_unwind_arch (next_frame);
+  return get_frame_arch (next_frame);
 }
 
 /* Gets the language of FRAME.  */
diff --git a/gdb/testsuite/gdb.base/until-in-tailcall.c b/gdb/testsuite/gdb.base/until-in-tailcall.c
new file mode 100644
index 00000000000..3e3853ba6df
--- /dev/null
+++ b/gdb/testsuite/gdb.base/until-in-tailcall.c
@@ -0,0 +1,53 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+/* This test relies on tailcall_function actually compiling to a tail call
+   function.  we try to force this by preventing tailcall_function and
+   normal_function from being inlined, then compiling this file at -O2.
+   Still, that's no guarantee.  If tailcall_function isn't a tail call,
+   then the test becomes pointless.  */
+
+volatile int global_var = 42;
+
+int __attribute__ ((noinline, noclone))
+normal_function (int x)
+{
+  return x + 1;
+}
+
+int __attribute__ ((noinline, noclone))
+tailcall_function (int x)
+{
+  return normal_function (x);
+}
+
+void __attribute__ ((noinline, noclone))
+worker_func (void)
+{
+  ++global_var;
+  ++global_var;			/* Run until here.  */
+  ++global_var;
+}
+
+int
+main (void)
+{
+  int result = tailcall_function (42);
+  result -= global_var;		/* Should stop here.  */
+  worker_func ();
+  return result;
+}
diff --git a/gdb/testsuite/gdb.base/until-in-tailcall.exp b/gdb/testsuite/gdb.base/until-in-tailcall.exp
new file mode 100644
index 00000000000..cd66d0ceaf8
--- /dev/null
+++ b/gdb/testsuite/gdb.base/until-in-tailcall.exp
@@ -0,0 +1,108 @@
+# 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/>.
+
+# Run until we have a tail call frame in the stack.  The stack looks like this:
+#
+# main -> tailcall_function -> normal_function
+#
+# We stop on normal_function, and then use 'up' to go to the
+# tailcall_function frame.  We then use 'until NN' where 'NN' is a
+# line that should not be reached until after the inferior has
+# returned to main.
+#
+# However, when the inferior returns to main, GDB should stop the
+# inferior as the 'until' has gone out of scope.
+#
+# At one point this was not happening as GDB would place the out of
+# scope 'until' guard breakpoint in the wrong place.
+#
+# This test relies on the source file compiling with a tail call in
+# place.
+
+standard_testfile
+
+if {[prepare_for_testing "failed to prepare" $testfile $srcfile \
+	 {debug optimize=-O2}]} {
+    return
+}
+
+# Run until there is a tail call frame in the backtrace.  Then use the
+# 'until' command with a line that will not be reached until after the
+# current stack has unwound back to main.  We expect the until's out
+# of scope breakpoint to be hit first.
+#
+# When USE_PARENT_FRAME_P is true we move up to the tail call frame
+# and issue the 'until' command while that frame is selected.
+#
+# When USE_PARENT_FRAME_P is false we issue the 'until' command while
+# normal_function is selected, this function is called from the tail
+# call function.
+#
+# In both cases, we expect the out of scope breakpoint to be placed
+# back in main, which is where the inferior should be stopped.
+proc run_test { use_parent_frame_p } {
+    clean_restart $::testfile
+
+    if {![runto normal_function]} {
+	return
+    }
+
+    # Check the stack looks correct.
+    gdb_test "bt" \
+	[multi_line \
+	     "#0\\s+normal_function\[^\r\n\]+" \
+	     "#1\\s+(?:$::hex in )?tailcall_function\[^\r\n\]+" \
+	     "#2\\s+(?:$::hex in )?main\[^\r\n\]+"] \
+	"check call stack"
+
+    # Move up to tailcall_function if needed.
+    if { $use_parent_frame_p } {
+	gdb_test "up" ".* in tailcall_function .*" \
+	    "move up to tailcall_function"
+    }
+
+    # Where we ask the 'until' to run to.
+    set until_lineno [gdb_get_line_number "Run until here."]
+
+    # Where we actually expect the inferior to stop.
+    set stop_lineno [gdb_get_line_number "Should stop here."]
+
+    # Issue the 'until' command and check that the inferior stops in main
+    # when the until guard breakpoint notices we have left the
+    # tailcall_function.
+    set saw_stop_location false
+    set saw_stop_source false
+    gdb_test_multiple "until $until_lineno" "run until worker_func" {
+	-re "^until $until_lineno\r\n" {
+	    exp_continue
+	}
+	-re "^$::gdb_prompt $" {
+	    gdb_assert { $saw_stop_location && $saw_stop_source } \
+		$gdb_test_name
+	}
+	-re "^main \\(\\) at \[^\r\n\]+/$::srcfile:$stop_lineno\r\n" {
+	    set saw_stop_location true
+	    exp_continue
+	}
+	-re "^$stop_lineno\\s+\[^\r\n\]+\r\n" {
+	    set saw_stop_source true
+	    exp_continue
+	}
+    }
+}
+
+foreach_with_prefix use_parent_frame { true false } {
+    run_test $use_parent_frame
+}
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c
new file mode 100644
index 00000000000..d129d7e41b4
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.c
@@ -0,0 +1,45 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+/* This test relies on tailcall_function actually compiling to a tail call
+   function.  we try to force this by preventing tailcall_function and
+   normal_function from being inlined, then compiling this file at -O2.
+   Still, that's no guarantee.  If tailcall_function isn't a tail call,
+   then the test becomes pointless.  */
+
+volatile int global_var = 42;
+
+int __attribute__ ((noinline, noclone))
+normal_function (int x)
+{
+  return x + 1;
+}
+
+int __attribute__ ((noinline, noclone))
+tailcall_function (int x)
+{
+  ++global_var;
+  return normal_function (x);
+}
+
+int
+main (void)
+{
+  int result = tailcall_function (42);
+  result -= global_var;			/* Temporary breakpoint here.  */
+  return result;
+}
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
new file mode 100644
index 00000000000..e5b5ebc5183
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
@@ -0,0 +1,93 @@
+# 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/>.
+
+# Check that FinishBreakpoints are reached when set for a tailcall
+# frame.
+#
+# Frame #0 is never a tailcall frame.  As such, we need to do more
+# than just stop in a tailcall function and create a finish
+# breakpoint.  Instead, we need to stop in a function called from a
+# tail call function, then walk back up the stack and create the
+# finish breakpoint on the tail call frame.
+#
+# At one point, GDB would create the breakpoint in the correct
+# location, but set the frame-id on the breakpoint for the wrong
+# frame, with the result that the breakpoint would never trigger.
+
+load_lib gdb-python.exp
+
+require allow_python_tests
+
+standard_testfile
+
+if {[build_executable "failed to build" $testfile $srcfile \
+	 {debug optimize=-O2}]} {
+    return
+}
+
+# For remote host testing.
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Run to 'normal_function' and then try to create a FinishBreakpoint
+# in the parent frame.
+proc run_test {} {
+    clean_restart $::testfile
+
+    if {![runto normal_function]} {
+	return
+    }
+
+    gdb_test "bt" \
+	[multi_line \
+	     "#0\\s+normal_function\[^\r\n\]+" \
+	     "#1\\s+(?:$::hex in )?tailcall_function\[^\r\n\]+" \
+	     "#2\\s+(?:$::hex in )?main\[^\r\n\]+"] \
+	"check call stack"
+
+    gdb_test "source $::pyfile" "Python script imported" "import python scripts"
+
+    set lineno [gdb_get_line_number "Temporary breakpoint here."]
+    gdb_test "python MyFinishBreakpoint(gdb.selected_frame().older())" \
+	"Temporary breakpoint $::decimal at $::hex: file \[^\r\n\]+/$::srcfile, line $lineno\\." \
+	"create finish breakpoint"
+
+    set saw_stopped_message false
+    set saw_breakpoint_line false
+    set saw_source_line false
+    gdb_test_multiple "continue" "" {
+	-re "^Stopped at MyFinishBreakpoint\r\n" {
+	    set saw_stopped_message true
+	    exp_continue
+	}
+	-re "^Breakpoint $::decimal, main \\(\\) at \[^\r\n\]+/$::srcfile:$lineno\r\n" {
+	    set saw_breakpoint_line true
+	    exp_continue
+	}
+	-re "^$lineno\\s+\[^\r\n\]+\r\n" {
+	    set saw_source_line true
+	    exp_continue
+	}
+	-re "^$::gdb_prompt $" {
+	    gdb_assert { $saw_stopped_message \
+			     && $saw_breakpoint_line \
+			     && $saw_source_line } $gdb_test_name
+	}
+	-re "^\[^\r\n\]*\r\n" {
+	    exp_continue
+	}
+    }
+}
+
+run_test
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
new file mode 100644
index 00000000000..bd10109a3dc
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
@@ -0,0 +1,26 @@
+# 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/>.
+
+
+import gdb
+
+
+class MyFinishBreakpoint(gdb.FinishBreakpoint):
+    def stop(self):
+        print("Stopped at MyFinishBreakpoint")
+        return True
+
+
+print("Python script imported")
-- 
2.25.4


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCHv2 2/4] gdb/python: fix FinishBreakpoint.return_value for tail call functions
  2026-03-05 13:37 ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
  2026-03-05 13:37   ` [PATCHv2 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls Andrew Burgess
@ 2026-03-05 13:37   ` Andrew Burgess
  2026-03-05 13:37   ` [PATCHv2 3/4] gdb/python: don't allow FinishBreakpoints for inline frames Andrew Burgess
                     ` (2 subsequent siblings)
  4 siblings, 0 replies; 14+ messages in thread
From: Andrew Burgess @ 2026-03-05 13:37 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

The FinishBreakpoint.return_value attribute will not be populated
correctly for tail call functions.

In bpfinishpy_init (python/py-finishbreakpoint.c) we use the function
get_frame_pc_if_available to return an address, and then use this
address to lookup a function symbol.

The problem is that, for tail call functions, the address returned by
get_frame_pc_if_available can be outside the bounds of the function,
as a result GDB might find no function symbol at all, or might find
the wrong function symbol, if the tail call function is immediately
adjacent to the next function.

Fix this by using get_frame_address_in_block_if_available instead.
For tail call functions this will return an address within the bounds
of the function, which means that GDB should find the correct function
symbol, and from this the correct return type.

I've extended the existing FinishBreakpoint with tail call test case
to include printing the return value, this test fails without this
patch, but now works.
---
 gdb/python/py-finishbreakpoint.c                          | 6 +++---
 .../gdb.python/py-finish-breakpoint-tailcall.exp          | 8 +++++++-
 gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py | 1 +
 3 files changed, 11 insertions(+), 4 deletions(-)

diff --git a/gdb/python/py-finishbreakpoint.c b/gdb/python/py-finishbreakpoint.c
index 3370bb02580..4af8ec6c75f 100644
--- a/gdb/python/py-finishbreakpoint.c
+++ b/gdb/python/py-finishbreakpoint.c
@@ -174,7 +174,6 @@ bpfinishpy_init (PyObject *self, PyObject *args, PyObject *kwargs)
   struct frame_id frame_id;
   PyObject *internal = NULL;
   int internal_bp = 0;
-  std::optional<CORE_ADDR> pc;
 
   if (!gdb_PyArg_ParseTupleAndKeywords (args, kwargs, "|OO", keywords,
 					&frame_obj, &internal))
@@ -248,9 +247,10 @@ bpfinishpy_init (PyObject *self, PyObject *args, PyObject *kwargs)
 
   try
     {
-      if ((pc = get_frame_pc_if_available (frame)))
+      CORE_ADDR pc;
+      if (get_frame_address_in_block_if_available (frame, &pc))
 	{
-	  struct symbol *function = find_symbol_for_pc (*pc);
+	  struct symbol *function = find_symbol_for_pc (pc);
 	  if (function != nullptr)
 	    {
 	      struct type *ret_type =
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
index e5b5ebc5183..2ae61f389ef 100644
--- a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
@@ -66,11 +66,16 @@ proc run_test {} {
     set saw_stopped_message false
     set saw_breakpoint_line false
     set saw_source_line false
+    set saw_return_value false
     gdb_test_multiple "continue" "" {
 	-re "^Stopped at MyFinishBreakpoint\r\n" {
 	    set saw_stopped_message true
 	    exp_continue
 	}
+	-re "^Return value is 43\r\n" {
+	    set saw_return_value true
+	    exp_continue
+	}
 	-re "^Breakpoint $::decimal, main \\(\\) at \[^\r\n\]+/$::srcfile:$lineno\r\n" {
 	    set saw_breakpoint_line true
 	    exp_continue
@@ -82,7 +87,8 @@ proc run_test {} {
 	-re "^$::gdb_prompt $" {
 	    gdb_assert { $saw_stopped_message \
 			     && $saw_breakpoint_line \
-			     && $saw_source_line } $gdb_test_name
+			     && $saw_source_line \
+			     && $saw_return_value } $gdb_test_name
 	}
 	-re "^\[^\r\n\]*\r\n" {
 	    exp_continue
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
index bd10109a3dc..e493dfa4017 100644
--- a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.py
@@ -20,6 +20,7 @@ import gdb
 class MyFinishBreakpoint(gdb.FinishBreakpoint):
     def stop(self):
         print("Stopped at MyFinishBreakpoint")
+        print("Return value is {}".format(self.return_value))
         return True
 
 
-- 
2.25.4


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCHv2 3/4] gdb/python: don't allow FinishBreakpoints for inline frames
  2026-03-05 13:37 ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
  2026-03-05 13:37   ` [PATCHv2 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls Andrew Burgess
  2026-03-05 13:37   ` [PATCHv2 2/4] gdb/python: fix FinishBreakpoint.return_value for tail call functions Andrew Burgess
@ 2026-03-05 13:37   ` Andrew Burgess
  2026-03-05 13:37   ` [PATCHv2 4/4] gdb/python: fix gdb.FinishBreakpoint returning to a tail call frame Andrew Burgess
  2026-03-05 15:59   ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Tom Tromey
  4 siblings, 0 replies; 14+ messages in thread
From: Andrew Burgess @ 2026-03-05 13:37 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess, Eli Zaretskii

Creating a Python gdb.FinishBreakpoint for an inline frame doesn't
work.

If we look at the 'finish' command, in the finish_command
function (infcmd.c) then we see that GDB handles inline frames very
different to non-inline frames.

For non-inline frames GDB creates a temporary breakpoint and then
resumes the inferior until the breakpoint is hit.

But for inline frames, GDB steps forward until we have left the inline
frame.

When it comes to gdb.FinishBreakpoint we only have the "create a
temporary breakpoint" mechanism; that is, after all, what the
FinishBreakpoint is, it's a temporary breakpoint placed at the
return address in the caller.

Currently, when a FinishBreakpoint is created within an inline frame,
GDB ends up creating the breakpoint at the current $pc.  As a result
the breakpoint will not be hit before the current function
exits (unless there's a loop going on, but that's not the point).

We could imagine what a solution to this problem would look like, GDB
would need to figure out the set of addresses for all possible exit
points from the inline function, and place a breakpoint at each of
these locations.  I don't propose doing that in this commit.

Instead, I plan to update the docs to note that creating a
FinishBreakpoint within an inline frame is not allowed, and I will
catch this case within bpfinishpy_init (python/py-finishbreakpoint.c)
and throw an error.

Though the error is new, all I'm doing is raising an error for a case
that never worked.

There's a new test to cover this case.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
---
 gdb/doc/python.texi                           |   3 +
 gdb/python/py-finishbreakpoint.c              |   8 +-
 .../gdb.python/py-finish-breakpoint-inline.c  |  58 +++++++
 .../py-finish-breakpoint-inline.exp           | 147 ++++++++++++++++++
 .../gdb.python/py-finish-breakpoint-inline.py |  27 ++++
 5 files changed, 242 insertions(+), 1 deletion(-)
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp
 create mode 100644 gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py

diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 009ac7a3f9f..2df3b7c0423 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -7282,6 +7282,9 @@ Finish Breakpoints in Python
 Finish breakpoints are thread specific and must be create with the right
 thread selected.
 
+It is not possible to create a @code{gdb.FinishBreakpoint} for an
+inline frame (@pxref{Inline Functions}).
+
 @defun FinishBreakpoint.__init__ (@r{[}frame@r{]} @r{[}, internal@r{]})
 Create a finish breakpoint at the return address of the @code{gdb.Frame}
 object @var{frame}.  If @var{frame} is not provided, this defaults to the
diff --git a/gdb/python/py-finishbreakpoint.c b/gdb/python/py-finishbreakpoint.c
index 4af8ec6c75f..834f85037c0 100644
--- a/gdb/python/py-finishbreakpoint.c
+++ b/gdb/python/py-finishbreakpoint.c
@@ -192,10 +192,16 @@ bpfinishpy_init (PyObject *self, PyObject *args, PyObject *kwargs)
 	  PyErr_SetString (PyExc_ValueError,
 			   _("Invalid ID for the `frame' object."));
 	}
+      else if (get_frame_type (frame) == INLINE_FRAME)
+	{
+	  PyErr_SetString
+	    (PyExc_ValueError,
+	     _("Unable to create FinishBreakpoint for inline frame."));
+	}
       else
 	{
 	  prev_frame = get_prev_frame (frame);
-	  if (prev_frame == 0)
+	  if (prev_frame == nullptr)
 	    {
 	      PyErr_SetString (PyExc_ValueError,
 			       _("\"FinishBreakpoint\" not "
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c
new file mode 100644
index 00000000000..c955917a873
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.c
@@ -0,0 +1,58 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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/>.  */
+
+/* Global used to create filler code within functions.  */
+volatile int global_var = 1;
+
+static int  __attribute__ ((noinline, noclone))
+baz (int arg)
+{
+  arg += global_var;
+  return arg;
+}
+
+static inline int __attribute__ ((__always_inline__))
+bar (int arg)
+{
+  arg += global_var;
+  arg = baz (arg);		/* Finish location.  */
+  arg -= global_var;
+  return arg;
+}
+
+static inline int __attribute__ ((__always_inline__))
+foo (int arg)
+{
+  arg += global_var;
+  arg = bar (arg);
+  arg -= global_var;
+  return arg;
+}
+
+int
+main (void)
+{
+  int ans;
+
+  ++global_var;
+  ++global_var;
+  ans = foo (42);
+  ++global_var;
+  ++global_var;
+  ans += global_var;
+  return ans;		/* Final breakpoint.  */
+}
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp
new file mode 100644
index 00000000000..d1c3f3de8f0
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.exp
@@ -0,0 +1,147 @@
+# 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/>.
+
+# Check that attempting to create a FinishBreakpoint within an inline
+# function will fail.
+#
+# For inline functions the 'finish' command steps forward until we are
+# outside the inline function.
+#
+# For FinishBreakpoints though we need to pick an address an place a
+# breakpoint there.  Currently GDB doesn't know where to place such a
+# breakpoint for an inline function, so our solution is to prevent
+# creation of FinishBreakpoints for inline frames.
+
+load_lib gdb-python.exp
+
+require allow_python_tests
+
+standard_testfile
+
+if {[prepare_for_testing "failed to prepare" $testfile $srcfile]} {
+    return
+}
+
+if {![runto_main]} {
+    return
+}
+
+# Source the Python script.
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+gdb_test "source $pyfile" "Python script imported" "import python scripts"
+
+# Breakpoint locations needed for this test.
+gdb_breakpoint foo
+gdb_breakpoint bar
+gdb_breakpoint baz
+set final_lineno [gdb_get_line_number "Final breakpoint."]
+gdb_breakpoint $final_lineno
+
+# Depending on how the code is compiled, and exactly where the finish
+# breakpoint is placed, the breakpoint could potentially be reported
+# on either of these lines.
+set lineno_1 [gdb_get_line_number "Finish location."]
+set lineno_2 [expr {$lineno_1 + 1}]
+set lineno_re "(?:$lineno_1|$lineno_2)"
+
+# Run to 'foo', which is an inline function called from a normal
+# function, and try to create a MyFinishBreakpoint.  This should fail.
+gdb_continue_to_breakpoint "breakpoint in foo"
+gdb_test "python MyFinishBreakpoint(gdb.selected_frame())" \
+    "Error occurred in Python: Unable to create FinishBreakpoint for inline frame\\." \
+    "try to create FinishBreakpoint for inline frame, caller is a normal frame"
+
+# Continue to 'bar', which is an inline function called from another
+# inline function, and try to create a MyFinishBreakpoint.  This
+# should fail.
+gdb_continue_to_breakpoint "breakpoint in bar"
+gdb_test "python MyFinishBreakpoint(gdb.selected_frame())" \
+    "Error occurred in Python: Unable to create FinishBreakpoint for inline frame\\." \
+    "try to create FinishBreakpoint for inline frame, caller is an inline frame"
+
+# Continue to 'baz', which is a normal function called from an inline
+# function, and create a MyFinishBreakpoint, which we expect to succeed.
+gdb_continue_to_breakpoint "breakpoint in baz"
+gdb_test "python MyFinishBreakpoint(gdb.selected_frame())" \
+    "Temporary breakpoint $decimal at $hex: file \[^\r\n\]+/$srcfile, line $lineno_re\\." \
+    "create FinishBreakpoint normal function, caller is an inline frame"
+
+# Continue and make sure we hit the MyFinishBreakpoint.
+set saw_finish_breakpoint false
+set saw_return_value false
+set saw_breakpoint_location false
+set saw_source_line false
+gdb_test_multiple "continue" "continue to finish breakpoint" {
+    -re "^Stopped at MyFinishBreakpoint\r\n" {
+	set saw_finish_breakpoint true
+	exp_continue
+    }
+    -re "^Return value is 51\r\n" {
+	set saw_return_value true
+	exp_continue
+    }
+    -re "^Breakpoint $decimal, ($hex in )?bar \\(arg=$decimal\\) at \[^\r\n\]+/$srcfile:$lineno_re\r\n" {
+	set saw_breakpoint_location true
+	exp_continue
+    }
+    -re "^$lineno_re\\s+\[^\r\n\]+\r\n" {
+	set saw_source_line true
+	exp_continue
+    }
+    -re "^$gdb_prompt $" {
+	gdb_assert {$saw_finish_breakpoint \
+			&& $saw_return_value \
+			&& $saw_breakpoint_location \
+			&& $saw_source_line } $gdb_test_name
+    }
+    -re "^\[^\r\n\]*\r\n" {
+	exp_continue
+    }
+}
+
+# Continue to the final breakpoint location.  We don't expect to see
+# any of the MyFinishBreakpoint output here.  If we do then we've hit
+# an unexpected FinishBreakpoint.
+set saw_finish_breakpoint false
+set saw_return_value false
+set saw_breakpoint_location false
+set saw_source_line false
+gdb_test_multiple "continue" "continue to final breakpoint" {
+    -re "^Stopped at MyFinishBreakpoint\r\n" {
+	set saw_finish_breakpoint true
+	exp_continue
+    }
+    -re "^Return value is 51\r\n" {
+	set saw_return_value true
+	exp_continue
+    }
+    -re "^Breakpoint $decimal, main \\(\\) at \[^\r\n\]+/$srcfile:$final_lineno\r\n" {
+	set saw_breakpoint_location true
+	exp_continue
+    }
+    -re "^$final_lineno\\s+\[^\r\n\]+\r\n" {
+	set saw_source_line true
+	exp_continue
+    }
+    -re "^$gdb_prompt $" {
+	gdb_assert {!$saw_finish_breakpoint \
+			&& !$saw_return_value \
+			&& $saw_breakpoint_location \
+			&& $saw_source_line } $gdb_test_name
+    }
+    -re "^\[^\r\n\]*\r\n" {
+	exp_continue
+    }
+}
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py
new file mode 100644
index 00000000000..e493dfa4017
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-inline.py
@@ -0,0 +1,27 @@
+# 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/>.
+
+
+import gdb
+
+
+class MyFinishBreakpoint(gdb.FinishBreakpoint):
+    def stop(self):
+        print("Stopped at MyFinishBreakpoint")
+        print("Return value is {}".format(self.return_value))
+        return True
+
+
+print("Python script imported")
-- 
2.25.4


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCHv2 4/4] gdb/python: fix gdb.FinishBreakpoint returning to a tail call frame
  2026-03-05 13:37 ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
                     ` (2 preceding siblings ...)
  2026-03-05 13:37   ` [PATCHv2 3/4] gdb/python: don't allow FinishBreakpoints for inline frames Andrew Burgess
@ 2026-03-05 13:37   ` Andrew Burgess
  2026-03-05 15:59   ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Tom Tromey
  4 siblings, 0 replies; 14+ messages in thread
From: Andrew Burgess @ 2026-03-05 13:37 UTC (permalink / raw)
  To: gdb-patches; +Cc: Andrew Burgess

I noticed that gdb.FinishBreakpoint doesn't work if the parent
function is a tail call function.  In bpfinishpy_init we use
get_frame_pc to find the address at which the finish breakpoint should
be placed within the previous frame.

However, if the previous frame is a tail call frame, then get_frame_pc
will return an address outside of the tail call function, an address
which will not be reached on the return path.

Unlike other recent tail call fixes I've made, we cannot switch to
using something like get_frame_address_in_block here as in the tail
call case this will return an address within the function, but not an
address that will be executed when we return.

What we need to do in the tail call case is create the finish
breakpoint in the frame that called the tail call function.  Or if
that frame is itself a tail call, then we should walk back up the call
stack until we find a non-tail call function.

This can be achieved by adding a call to skip_tailcall_frames into
bpfinishpy_init after our existing call to get_prev_frame.

I've extended the existing test case to cover this additional
situation.
---
 gdb/python/py-finishbreakpoint.c              |  3 ++
 .../py-finish-breakpoint-tailcall.exp         | 29 +++++++++++++++----
 2 files changed, 27 insertions(+), 5 deletions(-)

diff --git a/gdb/python/py-finishbreakpoint.c b/gdb/python/py-finishbreakpoint.c
index 834f85037c0..fbbb705a864 100644
--- a/gdb/python/py-finishbreakpoint.c
+++ b/gdb/python/py-finishbreakpoint.c
@@ -201,6 +201,9 @@ bpfinishpy_init (PyObject *self, PyObject *args, PyObject *kwargs)
       else
 	{
 	  prev_frame = get_prev_frame (frame);
+	  if (prev_frame != nullptr)
+	    prev_frame = skip_tailcall_frames (prev_frame);
+
 	  if (prev_frame == nullptr)
 	    {
 	      PyErr_SetString (PyExc_ValueError,
diff --git a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
index 2ae61f389ef..6a266abd790 100644
--- a/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
+++ b/gdb/testsuite/gdb.python/py-finish-breakpoint-tailcall.exp
@@ -40,9 +40,17 @@ if {[build_executable "failed to build" $testfile $srcfile \
 # For remote host testing.
 set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
 
-# Run to 'normal_function' and then try to create a FinishBreakpoint
-# in the parent frame.
-proc run_test {} {
+# Run to 'normal_function' and then try to create a FinishBreakpoint.
+#
+# When USE_PARENT_FRAME_P is true we use the parent frame, which is
+# for tailcall_function, to create the FinishBreakpoint.
+#
+# When USE_PARENT_FRAME_P is false we use the frame of normal_function
+# to create the FinishBreakpoint.
+#
+# In both cases the finish breakpoint should be placed back in main,
+# which is where the inferior should stop when resumed.
+proc run_test { use_parent_frame_p } {
     clean_restart $::testfile
 
     if {![runto normal_function]} {
@@ -59,7 +67,16 @@ proc run_test {} {
     gdb_test "source $::pyfile" "Python script imported" "import python scripts"
 
     set lineno [gdb_get_line_number "Temporary breakpoint here."]
-    gdb_test "python MyFinishBreakpoint(gdb.selected_frame().older())" \
+
+    if { $use_parent_frame_p } {
+	gdb_test_no_output "python frame=gdb.selected_frame().older()" \
+	    "store a reference to the parent frame"
+    } else {
+	gdb_test_no_output "python frame=gdb.selected_frame()" \
+	    "store a reference to the current frame"
+    }
+
+    gdb_test "python MyFinishBreakpoint(frame)" \
 	"Temporary breakpoint $::decimal at $::hex: file \[^\r\n\]+/$::srcfile, line $lineno\\." \
 	"create finish breakpoint"
 
@@ -96,4 +113,6 @@ proc run_test {} {
     }
 }
 
-run_test
+foreach_with_prefix parent_frame { true false } {
+    run_test $parent_frame
+}
-- 
2.25.4


^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints
  2026-03-05 13:37 ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
                     ` (3 preceding siblings ...)
  2026-03-05 13:37   ` [PATCHv2 4/4] gdb/python: fix gdb.FinishBreakpoint returning to a tail call frame Andrew Burgess
@ 2026-03-05 15:59   ` Tom Tromey
  2026-03-05 17:49     ` Andrew Burgess
  4 siblings, 1 reply; 14+ messages in thread
From: Tom Tromey @ 2026-03-05 15:59 UTC (permalink / raw)
  To: Andrew Burgess; +Cc: gdb-patches

>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:

Andrew> In v2:
Andrew>   - Rebased to current upstream HEAD.

Andrew>   - Retested.

Andrew> I've been looking at places where we use get_frame_pc when we should
Andrew> be using get_frame_address_in_block to handle tail calls correctly.
Andrew> This got me looking at gdb.FinishBreakpoint, and I found a bunch of
Andrew> things that needed fixing.

Thanks.  This all looks good to me.

Approved-By: Tom Tromey <tom@tromey.com>

Tom

^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints
  2026-03-05 15:59   ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Tom Tromey
@ 2026-03-05 17:49     ` Andrew Burgess
  0 siblings, 0 replies; 14+ messages in thread
From: Andrew Burgess @ 2026-03-05 17:49 UTC (permalink / raw)
  To: Tom Tromey; +Cc: gdb-patches

Tom Tromey <tom@tromey.com> writes:

>>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
>
> Andrew> In v2:
> Andrew>   - Rebased to current upstream HEAD.
>
> Andrew>   - Retested.
>
> Andrew> I've been looking at places where we use get_frame_pc when we should
> Andrew> be using get_frame_address_in_block to handle tail calls correctly.
> Andrew> This got me looking at gdb.FinishBreakpoint, and I found a bunch of
> Andrew> things that needed fixing.
>
> Thanks.  This all looks good to me.
>
> Approved-By: Tom Tromey <tom@tromey.com>

Pushed.

Thanks,
Andrew


^ permalink raw reply	[flat|nested] 14+ messages in thread

end of thread, other threads:[~2026-03-05 17:49 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-01-24 11:29 [PATCH 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
2026-01-24 11:29 ` [PATCH 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls Andrew Burgess
2026-01-27 16:37   ` Andrew Burgess
2026-01-24 11:29 ` [PATCH 2/4] gdb/python: fix FinishBreakpoint.return_value for tail call functions Andrew Burgess
2026-01-24 11:29 ` [PATCH 3/4] gdb/python: don't allow FinishBreakpoints for inline frames Andrew Burgess
2026-01-24 12:23   ` Eli Zaretskii
2026-01-24 11:29 ` [PATCH 4/4] gdb/python: fix gdb.FinishBreakpoint returning to a tail call frame Andrew Burgess
2026-03-05 13:37 ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Andrew Burgess
2026-03-05 13:37   ` [PATCHv2 1/4] gdb: fix frame_unwind_caller_WHAT functions for inline and tail calls Andrew Burgess
2026-03-05 13:37   ` [PATCHv2 2/4] gdb/python: fix FinishBreakpoint.return_value for tail call functions Andrew Burgess
2026-03-05 13:37   ` [PATCHv2 3/4] gdb/python: don't allow FinishBreakpoints for inline frames Andrew Burgess
2026-03-05 13:37   ` [PATCHv2 4/4] gdb/python: fix gdb.FinishBreakpoint returning to a tail call frame Andrew Burgess
2026-03-05 15:59   ` [PATCHv2 0/4] Fixes for tail call, until, and FinishBreakpoints Tom Tromey
2026-03-05 17:49     ` Andrew Burgess

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox