diff --git a/gdb/frame.c b/gdb/frame.c
index 6a6d968b9a97..e6e58a76668f 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -188,6 +188,10 @@ struct frame_info
   /* A frame specific string describing the STOP_REASON in more detail.
      Only valid when PREV_P is set, but even then may still be NULL.  */
   const char *stop_string;
+
+  /* True if this frame was created from addresses given by the user (see
+     create_new_frame) rather than through unwinding.  */
+  bool is_user_created;
 };
 
 /* See frame.h.  */
@@ -1669,6 +1673,11 @@ get_current_frame (void)
    never 0 and SELECTED_FRAME_ID is never the ID of the innermost
    frame.
 
+   An exception to that is if the selected frame is a used-created one
+   (created and selected through the "select-frame view" command).  That
+   is encoded by having SELECTED_FRAME_LEVEL == 0 and SELECTED_FRAME_ID
+   the frame id derived from the user-provided addresses.
+
    If SELECTED_FRAME_ID / SELECTED_FRAME_LEVEL are null_frame_id / -1,
    and the target has no stack or is executing, then there's no
    selected frame.  */
@@ -1695,10 +1704,6 @@ void
 restore_selected_frame (frame_id frame_id, int frame_level)
   noexcept
 {
-  /* save_selected_frame never returns level == 0, so we shouldn't see
-     it here either.  */
-  gdb_assert (frame_level != 0);
-
   /* FRAME_ID can be null_frame_id only IFF frame_level is -1.  */
   gdb_assert ((frame_level == -1 && !frame_id_p (frame_id))
 	      || (frame_level != -1 && frame_id_p (frame_id)));
@@ -1731,6 +1736,15 @@ lookup_selected_frame (struct frame_id a_frame_id, int frame_level)
       return;
     }
 
+  /* This means the selected frame was a user-created one.  Create a new one
+     using the user-provided addresses, which happen to be in the frame id.  */
+  if (frame_level == 0)
+    {
+      select_frame (create_new_frame (a_frame_id.stack_addr,
+				      a_frame_id.code_addr));
+      return;
+    }
+
   /* select_frame never saves 0 in SELECTED_FRAME_LEVEL, so we
      shouldn't see it here.  */
   gdb_assert (frame_level > 0);
@@ -1855,7 +1869,10 @@ select_frame (frame_info_ptr fi)
 
   selected_frame = fi;
   selected_frame_level = frame_relative_level (fi);
-  if (selected_frame_level == 0)
+
+  /* If the frame is a user-created one, save its level and frame id just like
+     any other non-level-0 frame.  */
+  if (selected_frame_level == 0 && !fi->is_user_created)
     {
       /* Treat the current frame especially -- we want to always
 	 save/restore it without warning, even if the frame ID changes
@@ -1934,6 +1951,7 @@ create_new_frame (CORE_ADDR addr, CORE_ADDR pc)
 
   fi = FRAME_OBSTACK_ZALLOC (struct frame_info);
 
+  fi->is_user_created = true;
   fi->next = create_sentinel_frame (current_program_space,
 				    get_current_regcache ());
 
@@ -2841,6 +2859,14 @@ get_frame_type (frame_info_ptr frame)
   return frame->unwind->type;
 }
 
+/* See frame.h.  */
+
+bool
+frame_is_user_created (const frame_info *frame)
+{
+  return frame->is_user_created;
+}
+
 struct program_space *
 get_frame_program_space (frame_info_ptr frame)
 {
diff --git a/gdb/frame.h b/gdb/frame.h
index cf8bbc6a52bd..12cb27573f4e 100644
--- a/gdb/frame.h
+++ b/gdb/frame.h
@@ -419,6 +419,10 @@ extern int frame_relative_level (frame_info_ptr fi);
 
 extern enum frame_type get_frame_type (frame_info_ptr);
 
+/* Return whether FRAME is user created.  */
+
+extern bool frame_is_user_created (const frame_info *frame);
+
 /* Return the frame's program space.  */
 extern struct program_space *get_frame_program_space (frame_info_ptr);
 
diff --git a/gdb/testsuite/gdb.base/frame-view.c b/gdb/testsuite/gdb.base/frame-view.c
new file mode 100644
index 000000000000..7b1cbd6abd42
--- /dev/null
+++ b/gdb/testsuite/gdb.base/frame-view.c
@@ -0,0 +1,74 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 2022 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 <pthread.h>
+#include <assert.h>
+
+struct type_1
+{
+  int m;
+};
+
+struct type_2
+{
+  int n;
+};
+
+static int
+baz (struct type_1 z1, struct type_2 z2)
+{
+  return z1.m + z2.n;
+}
+
+static int
+bar (struct type_1 y1, struct type_2 y2)
+{
+  return baz (y1, y2);
+}
+
+static int
+foo (struct type_1 x1, struct type_2 x2)
+{
+  return bar (x1, x2);
+}
+
+static void *
+thread_func (void *p)
+{
+  struct type_1 t1;
+  struct type_2 t2;
+  t1.m = 11;
+  t2.n = 11;
+  foo (t1, t2);
+
+  return NULL;
+}
+
+int
+main (void)
+{
+  pthread_t thread;
+  int res;
+
+  res = pthread_create (&thread, NULL, thread_func, NULL);
+  assert (res == 0);
+
+  res = pthread_join (thread, NULL);
+  assert (res == 0);
+
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/frame-view.exp b/gdb/testsuite/gdb.base/frame-view.exp
new file mode 100644
index 000000000000..45f0dcf08904
--- /dev/null
+++ b/gdb/testsuite/gdb.base/frame-view.exp
@@ -0,0 +1,68 @@
+# Copyright 2022 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 "frame view" family of commands.
+
+standard_testfile
+
+if { [build_executable "failed to prepare" \
+	${testfile} ${srcfile}] } {
+    return
+}
+
+proc test_select_frame_view {} {
+    clean_restart $::binfile
+
+    if { ![runto_main] } {
+	return
+    }
+
+    # Stop thread 2 at a baz.
+    gdb_test "break baz"
+    gdb_test "continue" "Thread 2.*hit Breakpoint $::decimal, baz .*"
+
+    # Grab the stack pointer and pc of thread 2's frame.
+    set frame_sp ""
+    set frame_pc ""
+
+    gdb_test_multiple "info frame" "" {
+	-re -wrap ".*frame at ($::hex):.*" {
+	    set frame_sp $expect_out(1,string)
+	    pass $gdb_test_name
+	}
+    }
+
+    gdb_test_multiple "print/x \$pc" "" {
+	-re -wrap " = ($::hex)" {
+	    set frame_pc $expect_out(1,string)
+	    pass $gdb_test_name
+	}
+    }
+
+    if { $frame_sp == "" || $frame_pc == "" } {
+	# Something must have failed and logged a failure above.
+	return
+    }
+
+    # Select thread 2's frame in thread 1.
+    gdb_test "thread 1" "Switching to thread 1 .*"
+    gdb_test_no_output "select-frame view $frame_sp $frame_pc"
+
+    # Verify that the "frame" command does not change the selected frame.
+    gdb_test "frame" "#0  baz \\(z1=.*, z2=.*\\).*" "frame"
+    gdb_test "frame" "#0  baz \\(z1=.*, z2=.*\\).*" "frame again"
+}
+
+test_select_frame_view
