diff --git a/gdb/frame.c b/gdb/frame.c
index fae89cbbc0a..9ed4515d4d5 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2419,6 +2419,38 @@ get_prev_frame_always_1 (frame_info_ptr this_frame)
 	}
     }
 
+  /* Ensure we can unwind the program counter of THIS_FRAME.  */
+  try
+    {
+      /* Calling frame_unwind_pc for the sentinel frame relies on the
+	 current_frame being set, which at this point it might not be if we
+	 are in the process of setting the current_frame after a stop (see
+	 get_current_frame).
+
+	 The point of this check is to ensure that the unwinder for
+	 THIS_FRAME can actually unwind the $pc, which we assume the
+	 sentinel frame unwinder can always do (it's just a read from the
+	 machine state), so we only call frame_unwind_pc for frames other
+	 than the sentinel (level -1) frame.
+
+	 Additionally, we don't actually care about the value of the
+	 unwound $pc, just that the call completed successfully.  */
+      if (this_frame->level >= 0)
+	frame_unwind_pc (this_frame);
+    }
+  catch (const gdb_exception_error &ex)
+    {
+      if (ex.error == NOT_AVAILABLE_ERROR || ex.error == OPTIMIZED_OUT_ERROR)
+	{
+	  frame_debug_printf ("  -> nullptr // no saved PC");
+	  this_frame->stop_reason = UNWIND_NO_SAVED_PC;
+	  this_frame->prev = nullptr;
+	  return nullptr;
+	}
+
+      throw;
+    }
+
   return get_prev_frame_maybe_check_cycle (this_frame);
 }
 
diff --git a/gdb/testsuite/gdb.base/pc-not-saved.c b/gdb/testsuite/gdb.base/pc-not-saved.c
new file mode 100644
index 00000000000..bc6632a97d7
--- /dev/null
+++ b/gdb/testsuite/gdb.base/pc-not-saved.c
@@ -0,0 +1,48 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2024 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/>.  */
+
+volatile int global_var = 0;
+
+void
+other_func (void)
+{
+  /* Nothing.  */
+}
+
+void
+break_bt_here (void)
+{
+  /* This is all nonsense; just filler so this function has a body.  */
+  if (global_var != 99)
+    global_var++;
+  if (global_var != 98)
+    global_var++;
+  if (global_var != 97)
+    global_var++;
+  if (global_var != 96)
+    global_var++;
+  other_func ();
+  if (global_var != 95)
+    global_var++;
+}
+
+int
+main (void)
+{
+  break_bt_here ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/pc-not-saved.exp b/gdb/testsuite/gdb.base/pc-not-saved.exp
new file mode 100644
index 00000000000..f267a269f1a
--- /dev/null
+++ b/gdb/testsuite/gdb.base/pc-not-saved.exp
@@ -0,0 +1,113 @@
+# Copyright 2024 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test how GDB handles a frame in which the previous-pc value is not
+# available.  Specifically, check that the backtrace correctly reports
+# why the backtrace is truncated, and ensure that 'display' directives
+# still work when 'stepi'-ing through the frame.
+#
+# We do this by registering a Python unwinder which doesn't provide
+# any previous register values.
+
+require allow_python_tests
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } {
+    return
+}
+
+set remote_python_file \
+    [gdb_remote_download host "${srcdir}/${subdir}/${testfile}.py"]
+
+if { ![runto "break_bt_here"] } {
+    return
+}
+
+# Figuring out the correct frame-id from a Python unwinder is hard.
+# We need to know the function's start address (not too hard), and the
+# stack address on entry to the function, which is much harder to
+# figure out in a cross-target way.
+#
+# So instead we run without any Python unwinder in place and use
+# 'maint print frame-id' to record the frame-id.  We then restart GDB,
+# load the Python unwinder, and tell it to use the frame-id we
+# recorded here.
+set pc unknown
+set cfa unknown
+gdb_test_multiple "maintenance print frame-id" "store break_bt_here frame-id" {
+    -re -wrap "frame-id for frame #0: \\{stack=($hex),code=($hex),\[^\}\]+\\}" {
+	set cfa $expect_out(1,string)
+	set pc $expect_out(1,string)
+    }
+}
+gdb_assert { ![string equal $cfa unknown] } \
+    "check we read the frame's CFA"
+
+gdb_assert { ![string equal $pc unknown] } \
+    "check we read the frame's PC"
+
+# Restart and load the Python unwinder script.
+clean_restart $binfile
+gdb_test_no_output "source ${remote_python_file}" "load python file"
+
+# Tell the Python unwinder to use the frame-id we cached above.
+gdb_test_no_output "python set_break_bt_here_frame_id($pc, $cfa)"
+
+# Run up to the function which the unwinder will claim.
+if { ![runto "break_bt_here"] } {
+    return
+}
+
+# Print the backtrace.  Check that the reason for stopping the
+# backtrace is that the previous $pc is not available.
+gdb_test "bt" \
+    [multi_line \
+	 "^#0  break_bt_here \\(\\) at \[^\r\n\]+" \
+	 "Backtrace stopped: frame did not save the PC"] \
+    "backtrace from break_bt_here function"
+
+# Ensure we can stepi.
+gdb_test "stepi" \
+    "(:?$hex\\s+)?$decimal\\s+\[^\r\n\]+" \
+    "stepi without a display in place"
+
+# Setup a 'display' directive.
+gdb_test "display/i \$pc" \
+    [multi_line \
+	 "^1: x/i \\\$pc" \
+	 "=> $hex <break_bt_here(:?\\+$decimal)?>:\\s+\[^\r\n\]+"]
+
+# Step again, check the 'display' directive is shown.
+gdb_test "stepi" \
+    [multi_line \
+	 "(:?$hex\\s+)?$decimal\\s+\[^\r\n\]+" \
+	 "1: x/i \\\$pc" \
+	 "=> $hex <break_bt_here(:?\\+$decimal)?>:\\s+\[^\r\n\]+"] \
+    "stepi with a display in place"
+
+# Continue to a function that is called from within break_bt_here.
+# The Python unwinder will then be claiming frame #1.
+gdb_breakpoint other_func
+gdb_continue_to_breakpoint "continue to other_func"
+
+# Print the backtrace and check that the reason for stopping the
+# backtrace is that the previous $pc is not available.
+gdb_test "bt" \
+    [multi_line \
+	 "#0  other_func \\(\\) at \[^\r\n\]+" \
+	 "#1  (:?$hex in )?break_bt_here \\(\\) at \[^\r\n\]+" \
+	 "Backtrace stopped: frame did not save the PC"] \
+    "backtrace from other_func function"
diff --git a/gdb/testsuite/gdb.base/pc-not-saved.py b/gdb/testsuite/gdb.base/pc-not-saved.py
new file mode 100644
index 00000000000..65a8e764885
--- /dev/null
+++ b/gdb/testsuite/gdb.base/pc-not-saved.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2024 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
+from gdb.unwinder import Unwinder, FrameId
+
+# Cached FrameId.  See set_break_bt_here_frame_id for details.
+break_bt_here_frame_id = None
+
+
+def set_break_bt_here_frame_id(pc, cfa):
+    """Call this to pre-calculate the FrameId for the frame our unwinder
+    is going to claim, this avoids us having to actually figure out a
+    frame-id within the unwinder, something which is going to be hard
+    to do in a cross-target way.
+
+    Instead we first run the test without the Python unwinder in
+    place, use 'maint print frame-id' to record the frame-id, then,
+    after loading this Python script, we all this function to record
+    the frame-id that the unwinder should use."""
+    global break_bt_here_frame_id
+    break_bt_here_frame_id = FrameId(cfa, pc)
+
+
+class break_unwinding(Unwinder):
+
+    """An unwinder for the function 'break_bt_here'.  This unwinder will
+    claim any frame for the function in question, but doesn't provide
+    any unwound register values.  Importantly, we don't provide a
+    previous $pc value, this means that if we are stopped in
+    'break_bt_here' then we should fail to unwind beyond frame #0."""
+
+    def __init__(self):
+        Unwinder.__init__(self, "break unwinding")
+
+    def __call__(self, pending_frame):
+        pc_desc = pending_frame.architecture().registers().find("pc")
+        pc = pending_frame.read_register(pc_desc)
+
+        if pc.is_optimized_out:
+            return None
+
+        block = gdb.block_for_pc(pc)
+        if block == None:
+            return None
+        func = block.function
+        if func == None:
+            return None
+        if str(func) != "break_bt_here":
+            return None
+
+        global break_bt_here_frame_id
+        if break_bt_here_frame_id is None:
+            return None
+
+        return pending_frame.create_unwind_info(break_bt_here_frame_id)
+
+
+gdb.unwinder.register_unwinder(None, break_unwinding(), True)
