diff --git a/gdb/aarch64-linux-tdep.c b/gdb/aarch64-linux-tdep.c
index 63c6f6b76b92..2257b33d1e5f 100644
--- a/gdb/aarch64-linux-tdep.c
+++ b/gdb/aarch64-linux-tdep.c
@@ -32,6 +32,7 @@
 #include "symtab.h"
 #include "tramp-frame.h"
 #include "trad-frame.h"
+#include "dwarf2/frame.h"
 #include "target.h"
 #include "target/target.h"
 #include "expop.h"
@@ -2561,6 +2562,54 @@ aarch64_linux_get_shadow_stack_pointer (gdbarch *gdbarch, regcache *regcache,
   return gcspr;
 }
 
+/* Implement Guarded Control Stack Pointer Register unwinding.  For each
+   previous GCS pointer check if its address is still in the GCS memory
+   range.  If it's outside the range set the returned value to unavailable,
+   otherwise return a value containing the new GCS pointer.  */
+
+static value *
+aarch64_linux_dwarf2_prev_gcspr (const frame_info_ptr &this_frame,
+				 void **this_cache, int regnum)
+{
+  value *v = frame_unwind_got_register (this_frame, regnum, regnum);
+  gdb_assert (v != nullptr);
+
+  gdbarch *gdbarch = get_frame_arch (this_frame);
+
+  if (v->entirely_available () && !v->optimized_out ())
+    {
+      int size = register_size (gdbarch, regnum);
+      bfd_endian byte_order = gdbarch_byte_order (gdbarch);
+      CORE_ADDR gcspr = extract_unsigned_integer (v->contents_all ().data (),
+						size, byte_order);
+
+      /* Starting with v6.13, the Linux kernel supports Guarded Control
+	 Stack.  Using /proc/PID/smaps we can only check if the current
+	 GCSPR points to GCS memory.  Only if this is the case a valid
+	 previous GCS pointer can be calculated.  */
+      std::pair<CORE_ADDR, CORE_ADDR> range;
+      if (linux_address_in_shadow_stack_mem_range (gcspr, &range))
+	{
+	  /* The GCS grows downwards.  To compute the previous GCS pointer,
+	     we need to increment the GCSPR.  */
+	  CORE_ADDR new_gcspr = gcspr + 8;
+
+	  /* If NEW_GCSPR points to the end of or before (<=) the current
+	     GCS memory range we consider NEW_GCSPR as valid.  */
+	  if (new_gcspr <= range.second)
+	    return frame_unwind_got_address (this_frame, regnum, new_gcspr);
+	}
+    }
+
+  /* Return a value which is marked as unavailable in case we could not
+     calculate a valid previous GCS pointer.  */
+  value *retval
+    = value::allocate_register (get_next_frame_sentinel_okay (this_frame),
+				regnum, register_type (gdbarch, regnum));
+  retval->mark_bytes_unavailable (0, retval->type ()->length ());
+  return retval;
+}
+
 /* AArch64 Linux implementation of the report_signal_info gdbarch
    hook.  Displays information about possible memory tag violations.  */
 
@@ -3134,8 +3183,11 @@ aarch64_linux_init_abi (struct gdbarch_info info, struct gdbarch *gdbarch)
 			    aarch64_use_target_description_from_corefile_notes);
 
   if (tdep->has_gcs_linux ())
-    set_gdbarch_get_shadow_stack_pointer (gdbarch,
+    {
+      set_gdbarch_get_shadow_stack_pointer (gdbarch,
 					aarch64_linux_get_shadow_stack_pointer);
+      tdep->fn_prev_gcspr = aarch64_linux_dwarf2_prev_gcspr;
+    }
 }
 
 #if GDB_SELF_TEST
diff --git a/gdb/aarch64-tdep.c b/gdb/aarch64-tdep.c
index e21c4a8e9ae1..c479a0b70323 100644
--- a/gdb/aarch64-tdep.c
+++ b/gdb/aarch64-tdep.c
@@ -1408,6 +1408,12 @@ aarch64_dwarf2_frame_init_reg (struct gdbarch *gdbarch, int regnum,
 	  return;
 	}
     }
+  if (tdep->has_gcs () && tdep->fn_prev_gcspr != nullptr
+      && regnum == tdep->gcs_reg_base)
+    {
+      reg->how = DWARF2_FRAME_REG_FN;
+      reg->loc.fn = tdep->fn_prev_gcspr;
+    }
 }
 
 /* Implement the execute_dwarf_cfa_vendor_op method.  */
diff --git a/gdb/aarch64-tdep.h b/gdb/aarch64-tdep.h
index 54ca641a35a3..99e7d26ce4ab 100644
--- a/gdb/aarch64-tdep.h
+++ b/gdb/aarch64-tdep.h
@@ -23,6 +23,7 @@
 #define GDB_AARCH64_TDEP_H
 
 #include "arch/aarch64.h"
+#include "dwarf2/frame.h"
 #include "displaced-stepping.h"
 #include "infrun.h"
 #include "gdbarch.h"
@@ -190,6 +191,9 @@ struct aarch64_gdbarch_tdep : gdbarch_tdep_base
      available.  */
   int gcs_linux_reg_base = -1;
 
+  /* Function to unwind the GCSPR from the given frame.  */
+  fn_prev_register fn_prev_gcspr = nullptr;
+
   /* Returns true if the target supports GCS.  */
   bool
   has_gcs () const
diff --git a/gdb/testsuite/gdb.arch/aarch64-gcs-return.c b/gdb/testsuite/gdb.arch/aarch64-gcs-return.c
new file mode 100644
index 000000000000..c6ade8af7e9f
--- /dev/null
+++ b/gdb/testsuite/gdb.arch/aarch64-gcs-return.c
@@ -0,0 +1,105 @@
+/* This test program is part of GDB, the GNU debugger.
+
+   Copyright 2025 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/auxv.h>
+#include <sys/syscall.h>
+#include <linux/prctl.h>
+
+/* Feature check for Guarded Control Stack.  */
+#ifndef HWCAP_GCS
+#define HWCAP_GCS (1UL << 32)
+#endif
+
+#ifndef PR_GET_SHADOW_STACK_STATUS
+#define PR_GET_SHADOW_STACK_STATUS 74
+#define PR_SET_SHADOW_STACK_STATUS 75
+#define PR_SHADOW_STACK_ENABLE (1UL << 0)
+#endif
+
+/* We need to use a macro to call prctl because after GCS is enabled, it's not
+   possible to return from the function which enabled it.  This is because the
+   return address of the calling function isn't on the GCS.  */
+#define my_syscall2(num, arg1, arg2)					\
+  ({									\
+    register long _num __asm__("x8") = (num);				\
+    register long _arg1 __asm__("x0") = (long)(arg1);			\
+    register long _arg2 __asm__("x1") = (long)(arg2);			\
+    register long _arg3 __asm__("x2") = 0;				\
+    register long _arg4 __asm__("x3") = 0;				\
+    register long _arg5 __asm__("x4") = 0;				\
+									\
+    __asm__ volatile("svc #0\n"						\
+		     : "=r"(_arg1)					\
+		     : "r"(_arg1), "r"(_arg2), "r"(_arg3), "r"(_arg4),	\
+		       "r"(_arg5), "r"(_num)				\
+		     : "memory", "cc");					\
+    _arg1;								\
+  })
+
+static int __attribute__ ((noinline))
+call2 ()
+{
+  return 42; /* Break call2.  */
+}
+
+static int __attribute__ ((noinline))
+call1 ()
+{
+  return call2 (); /* Break call1.  */
+}
+
+int
+main ()
+{
+  if (!(getauxval (AT_HWCAP) & HWCAP_GCS))
+    {
+      fprintf (stderr, "GCS support not found in AT_HWCAP\n");
+      return EXIT_FAILURE;
+    }
+
+  /* Force shadow stacks on, our tests *should* be fine with or
+     without libc support and with or without this having ended
+     up tagged for GCS and enabled by the dynamic linker.  We
+     can't use the libc prctl() function since we can't return
+     from enabling the stack.  Also lock GCS if not already
+     locked so we can test behaviour when it's locked.  */
+  unsigned long gcs_mode;
+  int ret = my_syscall2 (__NR_prctl, PR_GET_SHADOW_STACK_STATUS, &gcs_mode);
+  if (ret)
+    {
+      fprintf (stderr, "Failed to read GCS state: %d\n", ret);
+      return EXIT_FAILURE;
+    }
+
+  if (!(gcs_mode & PR_SHADOW_STACK_ENABLE))
+    {
+      gcs_mode = PR_SHADOW_STACK_ENABLE;
+      ret = my_syscall2 (__NR_prctl, PR_SET_SHADOW_STACK_STATUS, gcs_mode);
+      if (ret)
+	{
+	  fprintf (stderr, "Failed to configure GCS: %d\n", ret);
+	  return EXIT_FAILURE;
+	}
+    }
+
+  call1 (); /* Break main.  */
+
+  /* Avoid returning, in case libc doesn't understand GCS.  */
+  exit (EXIT_SUCCESS);
+}
diff --git a/gdb/testsuite/gdb.arch/aarch64-gcs-return.exp b/gdb/testsuite/gdb.arch/aarch64-gcs-return.exp
new file mode 100644
index 000000000000..717cc305c25c
--- /dev/null
+++ b/gdb/testsuite/gdb.arch/aarch64-gcs-return.exp
@@ -0,0 +1,132 @@
+# Copyright 2025 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test the GDB return command in a program that uses a Guarded Control Stack.
+# Based on the return tests in gdb.arch/amd64-shadow-stack-cmds.exp.
+
+require allow_aarch64_gcs_tests
+
+standard_testfile
+
+if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } {
+    return
+}
+
+set main_line [gdb_get_line_number "Break main"]
+set call1_line [gdb_get_line_number "Break call1"]
+set call2_line [gdb_get_line_number "Break call2"]
+
+if { ![runto ${main_line}] } {
+    return
+}
+
+proc restart_and_run_infcall_call2 {} {
+    global binfile call2_line
+    clean_restart ${binfile}
+    if { ![runto_main] } {
+	return
+    }
+    set inside_infcall_str "The program being debugged stopped while in a function called from GDB"
+    gdb_breakpoint ${call2_line}
+    gdb_continue_to_breakpoint "Break call2" ".*Break call2.*"
+    gdb_test "call (int) call2()" \
+	"Breakpoint \[0-9\]*, call2.*$inside_infcall_str.*"
+}
+
+with_test_prefix "test inferior call and continue" {
+    gdb_breakpoint ${call1_line}
+    gdb_continue_to_breakpoint "Break call1" ".*Break call1.*"
+
+    gdb_test "call (int) call2()" "= 42"
+
+    gdb_continue_to_end
+}
+
+with_test_prefix "test return inside an inferior call" {
+    restart_and_run_infcall_call2
+
+    gdb_test "return" "\#0.*call2.*" \
+	"Test GCS return inside an inferior call" \
+	"Make.*return now\\? \\(y or n\\) " "y"
+
+    gdb_continue_to_end
+}
+
+with_test_prefix "test return 'above' an inferior call" {
+    restart_and_run_infcall_call2
+
+    gdb_test "frame 2" "call2 ().*" "move to frame 'above' inferior call"
+
+    gdb_test "return" "\#0.*call1.*" \
+	"Test GCS return 'above' an inferior call" \
+	"Make.*return now\\? \\(y or n\\) " "y"
+
+    gdb_continue_to_end
+}
+
+clean_restart ${binfile}
+if { ![runto ${main_line}] } {
+    return
+}
+
+# Extract GCS pointer inside main, call1 and call2 function.
+gdb_breakpoint ${call1_line}
+gdb_breakpoint ${call2_line}
+set gcspr_main [get_valueof /x "\$gcspr" 0 "get value of gcspr in main"]
+gdb_continue_to_breakpoint "Break call1" ".*Break call1.*"
+set gcspr_call1 [get_valueof /x "\$gcspr" 0 "get value of gcspr in call1"]
+gdb_continue_to_breakpoint "Break call2" ".*Break call2.*"
+set gcspr_call2 [get_valueof /x "\$gcspr" 0 "get value of gcspr in call2"]
+
+with_test_prefix "test frame level update" {
+    gdb_test "up" "call1.*" "move to frame 1"
+    gdb_test "print /x \$gcspr" "= $gcspr_call1" "check gcspr of frame 1"
+    gdb_test "up" "main.*" "move to frame 2"
+    gdb_test "print /x \$gcspr" "= $gcspr_main" "check gcspr of frame 2"
+    gdb_test "frame 0" "call2.*" "move to frame 0"
+    gdb_test "print /x \$gcspr" "= $gcspr_call2" "check gcspr of frame 0"
+}
+
+with_test_prefix "test return from current frame" {
+    gdb_test "return (int) 1" "#0.*call1.*" \
+	"Test GCS return from current frame" \
+	"Make.*return now\\? \\(y or n\\) " "y"
+
+    # Potential GCS violations often only occur after resuming normal
+    # execution.  Therefore, it is important to test normal program
+    # continuation after testing the return command.
+    gdb_continue_to_end
+}
+
+clean_restart ${binfile}
+if { ![runto_main] } {
+    return
+}
+
+with_test_prefix "test return from past frame" {
+    gdb_breakpoint ${call2_line}
+    gdb_continue_to_breakpoint "Break call2" ".*Break call2.*"
+
+    gdb_test "frame 1" ".*in call1.*"
+
+    gdb_test "return (int) 1" "#0.*main.*" \
+	"Test GCS return from past frame" \
+	"Make.*return now\\? \\(y or n\\) " "y"
+
+    # Potential GCS violations often only occur after resuming normal
+    # execution.  Therefore, it is important to test normal program
+    # continuation after testing the return command.
+    gdb_continue_to_end
+}
