[v5,13/13] gdb, mi: Add -shadow-stack-list-frames command

Message ID 20260924083311.1961530-14-christina.joos@intel.com
State New
Headers
Series Add new command to print the shadow stack backtrace |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 fail Patch failed to apply
linaro-tcwg-bot/tcwg_gdb_check--master-arm fail Patch failed to apply

Commit Message

Joos, Christina Sept. 24, 2026, 8:33 a.m. UTC
  Add the mi command for the command "backtrace -shadow".
Similar to the mi interface for the ordinary backtrace command,
support low-frame and high-frame as command line parameters.

Example print of a full shadow stack backtrace:
~~~
(gdb)
-shadow-stack-list-frames
^done,shadow-stack=[
shadow-stack-frame={level="0",addr="0x00007ffff7c3fe70",
  func="__libc_start_call_main",file="../sysdeps/nptl/libc_start_call_main.h",
  fullname="/usr/[...]/sysdeps/nptl/libc_start_call_main.h",
  line="58",arch="i386:x86-64"},
shadow-stack-frame={level="1",addr="0x00007ffff7c3ff20",
  func="__libc_start_main_impl",file="../csu/libc-start.c",
  fullname="/usr/[...]/csu/libc-start.c",
  line="128",arch="i386:x86-64"},
shadow-stack-frame={level="2",addr="0x0000000000401075",
  func="_start",arch="i386:x86-64"}]
~~~

Example print of a shadow stack backtrace using low- and high-frame:
~~~
(gdb)
-shadow-stack-list-frames 0 1
^done,shadow-stack=[
shadow-stack-frame={level="0",addr="0x00007ffff7c3fe70",
  func="__libc_start_call_main",file="../sysdeps/nptl/libc_start_call_main.h",
  fullname="/usr/[...]/sysdeps/nptl/libc_start_call_main.h",
  line="58",arch="i386:x86-64"},
shadow-stack-frame={level="1",addr="0x00007ffff7c3ff20",
  func="__libc_start_main_impl",file="../csu/libc-start.c",
  fullname="/usr/[...]/csu/libc-start.c",
  line="128",arch="i386:x86-64"}]
~~~
---
 gdb/NEWS                                      |   8 ++
 gdb/doc/gdb.texinfo                           |  51 +++++++
 gdb/mi/mi-cmd-stack.c                         |  95 +++++++++++++
 gdb/mi/mi-cmds.c                              |   2 +
 gdb/mi/mi-cmds.h                              |   1 +
 gdb/shadow-stack.c                            | 127 +++++++++++++-----
 gdb/shadow-stack.h                            |  43 ++++++
 .../gdb.mi/mi-shadow-stack-signal.exp         |  69 ++++++++++
 gdb/testsuite/gdb.mi/mi-shadow-stack.exp      |  93 +++++++++++++
 9 files changed, 453 insertions(+), 36 deletions(-)
 create mode 100644 gdb/testsuite/gdb.mi/mi-shadow-stack-signal.exp
 create mode 100644 gdb/testsuite/gdb.mi/mi-shadow-stack.exp
  

Comments

Eli Zaretskii Sept. 24, 2026, 9:39 a.m. UTC | #1
> From: Christina Joos <christina.joos@intel.com>
> Cc: thiago.bauermann@linaro.org, tom@tromey.com, luis.machado.foss@gmail.com
> Date: Thu, 24 Sep 2026 10:33:10 +0200
> 
> Add the mi command for the command "backtrace -shadow".
> Similar to the mi interface for the ordinary backtrace command,
> support low-frame and high-frame as command line parameters.
> 
> Example print of a full shadow stack backtrace:
> ~~~
> (gdb)
> -shadow-stack-list-frames
> ^done,shadow-stack=[
> shadow-stack-frame={level="0",addr="0x00007ffff7c3fe70",
>   func="__libc_start_call_main",file="../sysdeps/nptl/libc_start_call_main.h",
>   fullname="/usr/[...]/sysdeps/nptl/libc_start_call_main.h",
>   line="58",arch="i386:x86-64"},
> shadow-stack-frame={level="1",addr="0x00007ffff7c3ff20",
>   func="__libc_start_main_impl",file="../csu/libc-start.c",
>   fullname="/usr/[...]/csu/libc-start.c",
>   line="128",arch="i386:x86-64"},
> shadow-stack-frame={level="2",addr="0x0000000000401075",
>   func="_start",arch="i386:x86-64"}]
> ~~~
> 
> Example print of a shadow stack backtrace using low- and high-frame:
> ~~~
> (gdb)
> -shadow-stack-list-frames 0 1
> ^done,shadow-stack=[
> shadow-stack-frame={level="0",addr="0x00007ffff7c3fe70",
>   func="__libc_start_call_main",file="../sysdeps/nptl/libc_start_call_main.h",
>   fullname="/usr/[...]/sysdeps/nptl/libc_start_call_main.h",
>   line="58",arch="i386:x86-64"},
> shadow-stack-frame={level="1",addr="0x00007ffff7c3ff20",
>   func="__libc_start_main_impl",file="../csu/libc-start.c",
>   fullname="/usr/[...]/csu/libc-start.c",
>   line="128",arch="i386:x86-64"}]
> ~~~
> ---
>  gdb/NEWS                                      |   8 ++
>  gdb/doc/gdb.texinfo                           |  51 +++++++
>  gdb/mi/mi-cmd-stack.c                         |  95 +++++++++++++
>  gdb/mi/mi-cmds.c                              |   2 +
>  gdb/mi/mi-cmds.h                              |   1 +
>  gdb/shadow-stack.c                            | 127 +++++++++++++-----
>  gdb/shadow-stack.h                            |  43 ++++++
>  .../gdb.mi/mi-shadow-stack-signal.exp         |  69 ++++++++++
>  gdb/testsuite/gdb.mi/mi-shadow-stack.exp      |  93 +++++++++++++
>  9 files changed, 453 insertions(+), 36 deletions(-)
>  create mode 100644 gdb/testsuite/gdb.mi/mi-shadow-stack-signal.exp
>  create mode 100644 gdb/testsuite/gdb.mi/mi-shadow-stack.exp

Thanks, the documentation parts are okay.

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

Patch

diff --git a/gdb/NEWS b/gdb/NEWS
index 707f3b5327e..52edd6de239 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -391,6 +391,14 @@  qExecAndArgs
   AIX version is now AIX 7.2 TL5.  GDB will only support
   DWARF debugging in AIX, going forward.
 
+* New MI commands
+
+-shadow-stack-list-frames
+  Added new MI command which is equivalent to the CLI command
+  'backtrace -shadow' but supports 'low-frame' and 'high-frame' as
+  command line parameters.  The parameters are used to print shadow
+  stack frames between certain levels on the shadow stack only.
+
 *** Changes in GDB 17
 
 * Debugging Linux programs that use x86-64 or x86-64 with 32-bit pointer
diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index c2f9474b4de..4558a4a523c 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -35585,6 +35585,57 @@  Show a single frame:
 (gdb)
 @end smallexample
 
+@anchor{-shadow-stack-list-frames}
+@findex -shadow-stack-list-frames
+@subheading The @code{-shadow-stack-list-frames} Command
+
+@subsubheading Synopsis
+
+@smallexample
+ -shadow-stack-list-frames [ @var{low-frame} @var{high-frame} ]
+@end smallexample
+
+List the shadow stack frames currently on the shadow stack.  In case the
+element on the shadow stack is a return address, @value{GDBN} prints the
+same fields as in @code{-stack-list-frames} with the return address on
+the shadow stack as @var{addr}.
+If the element on the shadow stack is not a return address, @value{GDBN}
+only prints @var{level}, @var{addr}, @var{func} and @var{arch}.
+
+If invoked without arguments, this command prints a backtrace for the
+whole shadow stack.  Like the @code{-stack-list-frames} command, if given
+two integer arguments, it shows the frames whose levels are between the
+two arguments (inclusive).
+
+@subsubheading @value{GDBN} Command
+
+The corresponding @value{GDBN} command is @samp{backtrace -shadow}.
+
+@subsubheading Example
+
+Show shadow stack frames between @var{low-frame} and @var{high-frame}:
+
+@smallexample
+(gdb)
+-shadow-stack-list-frames 0 2
+^done,shadow-stack=[
+shadow-stack-frame=@{level="0",addr="0x00007ffff7c45330",
+  func="<signal handler called>",arch="i386:x86-64"@},
+shadow-stack-frame=@{level="1",addr="0x80007ffff7bfffd8",
+  func="<sigframe token>",arch="i386:x86-64"@},
+shadow-stack-frame=@{level="2",addr="0x00007ffff7c4527e",
+  func="__GI_raise",file="[...]/raise.c",
+  fullname="./[...]/posix/raise.c",
+  line="26",arch="i386:x86-64"@}]
+(gdb)
+@end smallexample
+
+Note that for frame 0 and 1, @value{GDBN} printed only @var{level},
+@var{addr}, @var{func} and @var{arch}.  For frame 0, @var{func} is
+@code{"<signal handler called>"} similar to the @code{-stack-list-frames}
+command.  For frame 1 @value{GDBN} prints @code{"<sigframe token>"} for
+@var{func}.  This is due to an element on the shadow stack which is not a
+return address.
 
 @findex -stack-list-locals
 @anchor{-stack-list-locals}
diff --git a/gdb/mi/mi-cmd-stack.c b/gdb/mi/mi-cmd-stack.c
index a3b577032ce..c127fc9d239 100644
--- a/gdb/mi/mi-cmd-stack.c
+++ b/gdb/mi/mi-cmd-stack.c
@@ -34,6 +34,9 @@ 
 #include "inferior.h"
 #include "source.h"
 #include "gdbsupport/unordered_map.h"
+#include "shadow-stack.h"
+#include "gdbarch.h"
+#include "arch-utils.h"
 
 enum what_to_list { locals, arguments, all };
 
@@ -830,3 +833,95 @@  mi_cmd_stack_info_frame (const char *command, const char *const *argv,
   print_frame_info (user_frame_print_options,
 		    get_selected_frame (), 1, LOC_AND_ADDRESS, 0, 1);
 }
+
+/* Parse arguments of -shadow-stack-list-frames command and set FRAME_LOW
+   and FRAME_HIGH accordingly.  Throw an error in case the arguments are
+   invalid.  */
+
+static void
+mi_cmd_shadow_stack_list_frames_parse_args (const char *const *argv,
+					    int argc, int &frame_low,
+					    int &frame_high)
+{
+  /* There should either be low - high range, or no arguments.  */
+  if ((argc != 0) && (argc != 2))
+    error (_("-shadow-stack-list-frames: Usage: [FRAME_LOW FRAME_HIGH]"));
+
+  /* If there is a range, set it.  */
+  if (argc == 2)
+    {
+      frame_low = atoi (argv[0]);
+      frame_high = atoi (argv[1]);
+      if (frame_low < 0 && frame_high < 0)
+	error (_("-shadow-stack-list-frames: Invalid option " \
+		 "``%d'' and ``%d''."),
+	       frame_low, frame_high);
+      else if (frame_low < 0)
+	error (_("-shadow-stack-list-frames: Invalid option ``%d''."),
+	       frame_low);
+      else if (frame_high < 0)
+	error (_("-shadow-stack-list-frames: Invalid option ``%d''."),
+	       frame_high);
+    }
+  else
+    {
+      /* No arguments, print the whole shadow stack backtrace.  */
+      frame_low = -1;
+      frame_high = -1;
+    }
+}
+
+/* Print a list of the shadow stack frames.  Args can be none, in which
+   case we want to print the whole shadow stack backtrace, or a pair of
+   numbers specifying the frame numbers at which to start and stop the
+   display.  If the two numbers are equal, a single frame will be
+   displayed.  */
+
+void
+mi_cmd_shadow_stack_list_frames (const char *command,
+				 const char *const *argv,
+				 int argc)
+{
+  int frame_low;
+  int frame_high;
+
+  mi_cmd_shadow_stack_list_frames_parse_args (argv, argc, frame_low,
+					      frame_high);
+
+  gdbarch *gdbarch = get_current_arch ();
+  const CORE_ADDR start_ssp
+    = get_validated_shadow_stack_pointer (gdbarch);
+
+  ui_out_emit_list list_emitter (current_uiout, "shadow-stack");
+
+  std::optional<std::pair<CORE_ADDR, CORE_ADDR>> range
+    = get_non_empty_shadow_stack_range (gdbarch, start_ssp);
+  if (!range.has_value ())
+    return;
+
+  /* Extract the first shadow stack frame info (level 0).  */
+  std::optional<shadow_stack_frame_info> curr
+    = get_shadow_stack_frame_info (gdbarch, start_ssp, 0);
+
+  /* Let's position curr on the shadow stack frame at which to start the
+     display.  This could be the innermost frame if the whole shadow stack
+     needs displaying, or if frame_low is 0.  */
+  int frame_num = 0;
+  for (; curr.has_value () && frame_num < frame_low; frame_num++)
+    curr = curr->unwind_prev_shadow_stack_frame_info (*range);
+
+  if (!curr.has_value ())
+    error (_("-shadow-stack-list-frames: Not enough frames on the shadow "
+	     "stack."));
+
+  /* Now let's print the shadow stack frames up to frame_high, or until
+     the bottom of the shadow stack.  */
+  for (; curr.has_value () && (frame_num <= frame_high || frame_high == -1);
+       frame_num++)
+    {
+      QUIT;
+      print_shadow_stack_frame_info (user_frame_print_options, *curr,
+				     LOCATION);
+      curr = curr->unwind_prev_shadow_stack_frame_info (*range);
+    }
+}
diff --git a/gdb/mi/mi-cmds.c b/gdb/mi/mi-cmds.c
index 48d85e8785c..51f173f66ee 100644
--- a/gdb/mi/mi-cmds.c
+++ b/gdb/mi/mi-cmds.c
@@ -303,6 +303,8 @@  add_builtin_mi_commands ()
   add_mi_cmd_mi ("stack-info-frame", mi_cmd_stack_info_frame);
   add_mi_cmd_mi ("stack-list-arguments", mi_cmd_stack_list_args);
   add_mi_cmd_mi ("stack-list-frames", mi_cmd_stack_list_frames);
+  add_mi_cmd_mi ("shadow-stack-list-frames",
+		 mi_cmd_shadow_stack_list_frames);
   add_mi_cmd_mi ("stack-list-locals", mi_cmd_stack_list_locals);
   add_mi_cmd_mi ("stack-list-variables", mi_cmd_stack_list_variables);
   add_mi_cmd_mi ("stack-select-frame", mi_cmd_stack_select_frame,
diff --git a/gdb/mi/mi-cmds.h b/gdb/mi/mi-cmds.h
index 108ff645107..9e5d18dbeb4 100644
--- a/gdb/mi/mi-cmds.h
+++ b/gdb/mi/mi-cmds.h
@@ -100,6 +100,7 @@  extern mi_cmd_argv_ftype mi_cmd_stack_list_frames;
 extern mi_cmd_argv_ftype mi_cmd_stack_list_locals;
 extern mi_cmd_argv_ftype mi_cmd_stack_list_variables;
 extern mi_cmd_argv_ftype mi_cmd_stack_select_frame;
+extern mi_cmd_argv_ftype mi_cmd_shadow_stack_list_frames;
 extern mi_cmd_argv_ftype mi_cmd_symbol_list_lines;
 extern mi_cmd_argv_ftype mi_cmd_symbol_info_functions;
 extern mi_cmd_argv_ftype mi_cmd_symbol_info_module_functions;
diff --git a/gdb/shadow-stack.c b/gdb/shadow-stack.c
index 86633cbcb49..a9cbde4ad78 100644
--- a/gdb/shadow-stack.c
+++ b/gdb/shadow-stack.c
@@ -276,8 +276,21 @@  do_print_shadow_stack_frame_info
       uiout->text ("#");
       uiout->field_fmt_signed (2, ui_left, "level", frame.level);
 
+      if (uiout->is_mi_like_p ())
+	{
+	  uiout->field_string
+	    ("addr", hex_string_custom (frame.value, element_size * 2),
+	     address_style.style ());
+	}
+
       uiout->field_string ("func", str, metadata_style.style ());
 
+      if (uiout->is_mi_like_p ())
+	{
+	  uiout->field_string
+	    ("arch", gdbarch_bfd_arch_info (frame.arch)->printable_name);
+	}
+
       uiout->text ("\n");
       gdb_flush (gdb_stdout);
       return;
@@ -323,6 +336,12 @@  do_print_shadow_stack_frame_info
 	    print_lib (uiout, lib);
 	}
 
+      if (uiout->is_mi_like_p ())
+	{
+	  uiout->field_string
+	    ("arch", gdbarch_bfd_arch_info (frame.arch)->printable_name);
+	}
+
       uiout->text ("\n");
     }
 
@@ -342,10 +361,9 @@  do_print_shadow_stack_frame_info
   gdb_flush (gdb_stdout);
 }
 
-/* Redirect output to a temporary buffer for the duration of
-   do_print_shadow_stack_frame_info.  */
+/* See shadow-stack.h.  */
 
-static void
+void
 print_shadow_stack_frame_info
   (const frame_print_options &fp_opts,
    const shadow_stack_frame_info &frame,
@@ -372,11 +390,9 @@  ssp_unwind_stop_reason_to_err_string (ssp_unwind_stop_reason reason)
   gdb_assert_not_reached ("invalid unwind stop reason.");
 }
 
-/* Read the memory at shadow stack pointer SSP and assign it to
-   RETURN_VALUE.  In case of success, return true.  Otherwise,
-   return false.  */
+/* See shadow-stack.h.  */
 
-static bool
+bool
 read_shadow_stack_memory (gdbarch *gdbarch, CORE_ADDR ssp,
 			  CORE_ADDR &return_value)
 {
@@ -435,12 +451,9 @@  get_trailing_outermost_shadow_stack_frame_info
   return trailing;
 }
 
-/* If possible, get shadow stack frame info for the shadow stack pointer
-   SSP and its current frame LEVEL.  Pass FALLBACK_ARCH which can be used
-   as fallback gdbarch in case the gdbarch cannot be extracted from the
-   SAL.  Usually this is the gdbarch of the previous frame.  */
+/* See shadow-stack.h.  */
 
-static std::optional<shadow_stack_frame_info>
+std::optional<shadow_stack_frame_info>
 get_shadow_stack_frame_info
   (gdbarch *fallback_arch, const CORE_ADDR ssp, unsigned long level)
 {
@@ -596,22 +609,35 @@  shadow_stack_frame_info::inside_main_func () const
   return false;
 }
 
-/* Print all elements on the shadow stack or just the innermost COUNT_EXP
-   frames.  */
+/* See shadow-stack.h.  */
 
-void
-backtrace_shadow_command (const frame_print_options &fp_opts,
-			  const char *count_exp, int from_tty)
+CORE_ADDR
+get_validated_shadow_stack_pointer (gdbarch *gdbarch)
 {
   if (!target_has_stack ())
-    error (_("No shadow stack."));
+    {
+      if (!current_uiout->is_mi_like_p ())
+	error (_("No shadow stack."));
+      else
+	error (_("-shadow-stack-list-frames: No shadow stack."));
+    }
 
-  gdbarch *gdbarch = get_current_arch ();
   if (!gdbarch_address_in_shadow_stack_memory_range_p (gdbarch)
       || !gdbarch_top_addr_empty_shadow_stack_p (gdbarch)
       || gdbarch_ssp_regnum (gdbarch) == -1)
-    error (_("Printing of the shadow stack backtrace is not supported for"
-	     " the current target."));
+    {
+      if (!current_uiout->is_mi_like_p ())
+	{
+	  error (_("Printing of the shadow stack backtrace is not " \
+		   "supported for the current target."));
+	}
+      else
+	{
+	  error (_("-shadow-stack-list-frames: " \
+		   "Printing of the shadow stack backtrace is not " \
+		   "supported for the current target."));
+	}
+    }
 
   regcache *regcache = get_thread_regcache (inferior_thread ());
   bool shadow_stack_enabled = false;
@@ -621,10 +647,26 @@  backtrace_shadow_command (const frame_print_options &fp_opts,
 					shadow_stack_enabled);
 
   if (!start_ssp.has_value () || !shadow_stack_enabled)
-    error (_("Shadow stack is not enabled for the current thread."));
+    {
+      if (!current_uiout->is_mi_like_p ())
+	error (_("Shadow stack is not enabled for the current thread."));
+      else
+	{
+	  error (_("-shadow-stack-list-frames: " \
+		   "Shadow stack is not enabled for the current thread."));
+	}
+    }
+
+  return *start_ssp;
+}
 
-  /* Check if START_SSP points to a shadow stack memory range and use
-     the returned range to determine when to stop unwinding.
+/* See shadow-stack.h.  */
+
+std::optional<std::pair<CORE_ADDR, CORE_ADDR>>
+get_non_empty_shadow_stack_range (gdbarch *gdbarch, const CORE_ADDR ssp)
+{
+  /* Check if SSP points to a shadow stack memory range and use the
+     returned range to determine when to stop unwinding.
      Note that a shadow stack memory range can change, due to shadow stack
      switches for instance on x86 for an inter-privilege far call or when
      calling an interrupt/exception handler at a higher privilege level.
@@ -632,26 +674,39 @@  backtrace_shadow_command (const frame_print_options &fp_opts,
      Linux kernel v6.6.  However, shadow stack switches are not supported
      due to missing kernel space support.  We therefore implement this
      command without support for shadow stack switches for now.  */
-  bool is_top_addr_empty_shadow_stack = false;
   std::pair<CORE_ADDR, CORE_ADDR> range;
-  if (!gdbarch_address_in_shadow_stack_memory_range (gdbarch, *start_ssp,
-						     &range))
+  if (!gdbarch_address_in_shadow_stack_memory_range (gdbarch, ssp, &range))
     {
       /* For x86, if the current shadow stack pointer does not point to
 	 shadow stack memory but is valid, the shadow stack is empty.  */
-      is_top_addr_empty_shadow_stack = true;
+      return {};
     }
 
-  if (!is_top_addr_empty_shadow_stack)
+  if (gdbarch_top_addr_empty_shadow_stack_p (gdbarch)
+      && gdbarch_top_addr_empty_shadow_stack (gdbarch, ssp, range))
     {
       /* For ARM's Guarded Control Stack, the shadow stack can be empty
 	 even though START_SSP points to shadow stack memory range.  */
-      is_top_addr_empty_shadow_stack
-	= gdbarch_top_addr_empty_shadow_stack_p (gdbarch)
-	  && gdbarch_top_addr_empty_shadow_stack (gdbarch, *start_ssp, range);
+      return {};
     }
 
-  if (is_top_addr_empty_shadow_stack)
+  return {range};
+}
+
+/* Print all elements on the shadow stack or just the innermost COUNT_EXP
+   frames.  */
+
+void
+backtrace_shadow_command (const frame_print_options &fp_opts,
+			  const char *count_exp, int from_tty)
+{
+  gdbarch *gdbarch = get_current_arch ();
+  const CORE_ADDR start_ssp
+    = get_validated_shadow_stack_pointer (gdbarch);
+
+  std::optional<std::pair<CORE_ADDR, CORE_ADDR>> range
+    = get_non_empty_shadow_stack_range (gdbarch, start_ssp);
+  if (!range.has_value ())
     {
       gdb_printf (_("The shadow stack is empty.\n"));
       return;
@@ -660,7 +715,7 @@  backtrace_shadow_command (const frame_print_options &fp_opts,
   /* Extract the first shadow stack frame info (level 0).  */
   ssp_unwind_stop_reason reason = ssp_unwind_stop_reason::no_error;
   std::optional<shadow_stack_frame_info> current
-    = get_shadow_stack_frame_info (gdbarch, *start_ssp, 0);
+    = get_shadow_stack_frame_info (gdbarch, start_ssp, 0);
 
   if (!current.has_value ())
     reason = ssp_unwind_stop_reason::memory_read_error;
@@ -676,7 +731,7 @@  backtrace_shadow_command (const frame_print_options &fp_opts,
       if (count < 0)
 	{
 	  trailing = get_trailing_outermost_shadow_stack_frame_info
-		       (range, std::abs (count), *current);
+		       (*range, std::abs (count), *current);
 
 	  if (!trailing.has_value ())
 	    reason = current->unwind_stop_reason;
@@ -698,7 +753,7 @@  backtrace_shadow_command (const frame_print_options &fp_opts,
       print_shadow_stack_frame_info (fp_opts, *current, LOCATION);
 
       trailing = current;
-      current = current->unwind_prev_shadow_stack_frame_info (range);
+      current = current->unwind_prev_shadow_stack_frame_info (*range);
     }
 
   /* If we've stopped before the end, mention that.  */
diff --git a/gdb/shadow-stack.h b/gdb/shadow-stack.h
index d4b0d53acf3..8cdc389846a 100644
--- a/gdb/shadow-stack.h
+++ b/gdb/shadow-stack.h
@@ -139,4 +139,47 @@  class shadow_stack_frame_info
   bool inside_main_func () const;
 };
 
+/* Read the memory at shadow stack pointer SSP and assign it to
+   RETURN_VALUE.  In case of success, return true.  Otherwise, return
+   false.  */
+
+bool read_shadow_stack_memory (gdbarch *gdbarch, CORE_ADDR ssp,
+			       CORE_ADDR &return_value);
+
+/* Print information of shadow stack frame info FRAME.  The output is
+   formatted according to PRINT_WHAT.  For the meaning of PRINT_WHAT, see
+   enum print_what comments in frame.h.  Note that PRINT_WHAT is
+   overridden, if PRINT_OPTIONS.print_frame_info != print_frame_info_auto.
+   Redirect output to a temporary buffer for the duration of
+   do_print_shadow_stack_frame_info.  */
+
+void print_shadow_stack_frame_info
+  (const frame_print_options &fp_opts,
+   const shadow_stack_frame_info &frame,
+   print_what print_what);
+
+/* Check if shadow stacks are supported and enabled.  If it is, return the
+   validated shadow stack pointer.  If not, throw an appropriate error
+   message.
+   This helper is specific to printing the shadow stack backtrace and must
+   only be called from the CLI command "bt -shadow" or its MI equivalent
+   "-shadow-stack-list-frames" since the error messages are specific to
+   these commands.  */
+
+CORE_ADDR get_validated_shadow_stack_pointer (gdbarch *gdbarch);
+
+/* If the shadow stack pointed to by SSP is empty, return an empty optional.
+   Otherwise return the shadow stack memory range.  */
+
+std::optional<std::pair<CORE_ADDR, CORE_ADDR>>
+  get_non_empty_shadow_stack_range (gdbarch *gdbarch, const CORE_ADDR ssp);
+
+/* If possible, get shadow stack frame info for the shadow stack pointer
+   SSP and its current frame LEVEL.  Pass FALLBACK_ARCH which can be used
+   as fallback gdbarch in case the gdbarch cannot be extracted from the
+   SAL.  Usually this is the gdbarch of the previous frame.  */
+
+std::optional<shadow_stack_frame_info> get_shadow_stack_frame_info
+  (gdbarch *fallback_arch, const CORE_ADDR ssp, unsigned long level);
+
 #endif /* GDB_SHADOW_STACK_H */
diff --git a/gdb/testsuite/gdb.mi/mi-shadow-stack-signal.exp b/gdb/testsuite/gdb.mi/mi-shadow-stack-signal.exp
new file mode 100644
index 00000000000..c9e9c80a76b
--- /dev/null
+++ b/gdb/testsuite/gdb.mi/mi-shadow-stack-signal.exp
@@ -0,0 +1,69 @@ 
+# Copyright 2024-2026 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test the mi command -shadow-stack-list-frames for signal handling on linux.
+
+load_lib mi-support.exp
+set MIFLAGS "-i=mi"
+
+require allow_ssp_tests {istarget "*-*-linux*"}
+
+save_vars { ::env(GLIBC_TUNABLES) } {
+    append_environment GLIBC_TUNABLES "glibc.cpu.hwcaps" "SHSTK"
+
+    set srcfile "${srcdir}/gdb.arch/amd64-shadow-stack-signal.c"
+    set testfile mi-shadow-stack-signal
+
+    # Test shadow-stack-list-frames for shadow stack element which is no
+    # return address.
+    if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile} \
+	{debug additional_flags="-fcf-protection=return"}] } {
+	return
+    }
+
+    if { [mi_clean_restart $testfile] } {
+	return
+    }
+
+    mi_runto_main
+    mi_send_resuming_command "exec-continue" "continue till signal"
+
+    set r_signal "reason=\"signal-received\",signal-name=\"SIGUSR1\",signal-meaning=\"User defined signal 1\""
+    gdb_expect {
+	-re ".*stopped,${r_signal}.*$mi_gdb_prompt" {
+	    pass "Wait for user interrupt"
+	}
+	timeout {
+	    fail "Wait for user interrupt (timeout)"
+	    return
+	}
+    }
+
+    mi_gdb_test "break handler" \
+	{(&.*)*.*~"Breakpoint 2 at.*\\n".*=breakpoint-created,bkpt=\{number="2",type="breakpoint".*\}.*\n\^done}
+
+    mi_execute_to "exec-continue" "breakpoint-hit" "handler" ".*" ".*" ".*" \
+	{"" "disp=\"keep\""} "continue to handler"
+
+    # We only test the frame belonging to the shadow stack element which
+    # is not a return address.  This frame is triggered by the signal
+    # exception.
+    set any "\[^\"\]+"
+    mi_gdb_test "231-shadow-stack-list-frames 1 1" \
+	"231\\^done,shadow-stack=\\\[shadow-stack-frame=\{level=\"1\",addr=\"$hex\",func=\"<sigframe token>\",arch=\"$any\"\}\\\]" \
+	"test shadow-stack-list-frames"
+
+    mi_gdb_exit
+}
diff --git a/gdb/testsuite/gdb.mi/mi-shadow-stack.exp b/gdb/testsuite/gdb.mi/mi-shadow-stack.exp
new file mode 100644
index 00000000000..ac045d6f3a2
--- /dev/null
+++ b/gdb/testsuite/gdb.mi/mi-shadow-stack.exp
@@ -0,0 +1,93 @@ 
+# Copyright 2024-2026 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test the mi command -shadow-stack-list-frames.
+
+load_lib mi-support.exp
+set MIFLAGS "-i=mi"
+
+require allow_ssp_tests
+
+save_vars { ::env(GLIBC_TUNABLES) } {
+    append_environment GLIBC_TUNABLES "glibc.cpu.hwcaps" "SHSTK"
+
+    set srcfile "${srcdir}/gdb.arch/amd64-shadow-stack.c"
+    set testfile mi-shadow-stack
+
+    if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile} \
+	  {debug additional_flags="-fcf-protection=return"}] } {
+	return
+    }
+
+    if { [mi_clean_restart $testfile] } {
+	return
+    }
+
+    mi_runto_main
+
+    mi_gdb_test "break call2" \
+	{(&.*)*.*~"Breakpoint 2 at.*\\n".*=breakpoint-created,bkpt=\{number="2",type="breakpoint".*\}.*\n\^done}
+
+    mi_execute_to "exec-continue" "breakpoint-hit" "call2" ".*" ".*" ".*" \
+	{"" "disp=\"keep\""} "continue to call2"
+
+    set any "\[^\"\]+"
+    set any_remaining_frame_attr "\[^\r\n]+"
+
+    # It's enough to test the first 3 frames.  For frame 3 we just test that it
+    # exists as other attributes might depend on the environment.
+    set frame_start "shadow-stack-frame=\{level="
+    set frame1 "$frame_start\"0\",addr=\"$hex\",func=\"call1\",file=\"$any\",fullname=\"$any\",line=\"$decimal\",arch=\"$any\"\}"
+    set frame2 "$frame_start\"1\",addr=\"$hex\",func=\"main\",file=\"$any\",fullname=\"$any\",line=\"$decimal\",arch=\"$any\"\}"
+    set frame3 "$frame_start\"2\",addr=\"$hex\"$any_remaining_frame_attr\}"
+    mi_gdb_test "231-shadow-stack-list-frames" \
+	"231\\^done,shadow-stack=\\\[$frame1.*$frame2\\\]" \
+	"test shadow-stack-list-frames"
+
+    mi_gdb_test "set backtrace past-main on" \
+	".*=cmd-param-changed,param=\"backtrace past-main\",value=\"on\".*\\^done"
+    mi_gdb_test "231-shadow-stack-list-frames" \
+	"231\\^done,shadow-stack=\\\[$frame1.*$frame2.*$frame3.*\\\]" \
+	"test shadow-stack-list-frames past main"
+
+    # Test low-frame/high-frame
+    mi_gdb_test "231-shadow-stack-list-frames 0 1" \
+	"231\\^done,shadow-stack=\\\[$frame1.*$frame2\\\]" \
+	"test shadow-stack-list-frames low/high-frames"
+
+    # Test inferior function calls.
+    set inside_infcall_str "The program being debugged stopped while in a function called from GDB"
+    mi_gdb_test "call (int) call2()" \
+	".*Breakpoint \[0-9\]*, call2.*$inside_infcall_str.*" \
+	"call (int) call2()"
+
+    set dummy_frame1 "$frame_start\"0\",addr=\"$hex\",func=\"<function called from gdb>\",arch=\"$any\"\}"
+    set frame1 "$frame_start\"1\",addr=\"$hex\",func=\"call1\",file=\"$any\",fullname=\"$any\",line=\"$decimal\",arch=\"$any\"\}"
+    mi_gdb_test "231-shadow-stack-list-frames" \
+	"231\\^done,shadow-stack=\\\[$dummy_frame1.*$frame1.*" \
+	"test shadow-stack-list-frames inside inferior call"
+
+    mi_gdb_test "call (int) call2()" \
+	".*Breakpoint \[0-9\]*, call2.*$inside_infcall_str.*" \
+	"Execute an inferior call inside an inferior call."
+
+    set dummy_frame2 "$frame_start\"1\",addr=\"$hex\",func=\"<function called from gdb>\",arch=\"$any\"\}"
+    set frame2 "$frame_start\"2\",addr=\"$hex\",func=\"call1\",file=\"$any\",fullname=\"$any\",line=\"$decimal\",arch=\"$any\"\}"
+    mi_gdb_test "231-shadow-stack-list-frames" \
+	"231\\^done,shadow-stack=\\\[$dummy_frame1.*$dummy_frame2.*$frame2.*" \
+	"test shadow-stack-list-frames inside nested inferior call"
+
+    mi_gdb_exit
+}