diff --git a/gdb/testsuite/gdb.base/check-errno.c b/gdb/testsuite/gdb.base/check-errno.c
new file mode 100644
index 00000000000..c6835a866e8
--- /dev/null
+++ b/gdb/testsuite/gdb.base/check-errno.c
@@ -0,0 +1,37 @@
+/* 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/>.  */
+
+#include <errno.h>
+
+static void shadow_errno ();
+
+int main ()
+{
+  errno = 42;
+
+  shadow_errno ();	/* main-breakpoint */
+  return 0;
+}
+
+#undef errno
+static void
+shadow_errno ()
+{
+  int errno = 36;
+
+  return;		/* shadow_errno-breakpoint */
+}
diff --git a/gdb/testsuite/gdb.base/check-errno.exp b/gdb/testsuite/gdb.base/check-errno.exp
new file mode 100644
index 00000000000..41c9da7e635
--- /dev/null
+++ b/gdb/testsuite/gdb.base/check-errno.exp
@@ -0,0 +1,237 @@
+# 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.
+
+# Check that errno can be accessed by GDB under a variety of
+# circumstances.
+#
+# The challenge with GDB accessing errno is that, on modern systems,
+# errno is a variable in thread-local storage.  So, if GDB's access to
+# thread local storage is broken, some of these tests could fail.
+#
+# Another possibility (which is tested by the "macro" tests below) is
+# that errno is a macro and the debuginfo includes a macro expression
+# for errno.  In the past, this often helped with being able to print
+# errno, but at the time that this test was written, it seems to not
+# always work correctly.
+#
+# When the binary is compiled statically, GDB's access to thread local
+# storage may be compromised, perhaps leading to failures with those
+# tests.
+#
+# To complicate things even more, access to errno might not work when
+# examining a core file.  E.g. if it's necessary to call a function,
+# perhaps named '__errno_location', this won't be possible without
+# a running inferior.
+#
+# It's also possible for a program to declare errno in an inner scope
+# causing the thread-local errno to be shadowed.  GDB should still
+# correctly print the masking errno for this case.
+
+standard_testfile
+
+proc do_tests {{do_xfail_cast 0} {do_xfail 0} {do_xfail_core_test 0}} {
+    clean_restart $::binfile
+    runto_main
+
+    gdb_breakpoint [gdb_get_line_number "main-breakpoint"]
+    gdb_continue_to_breakpoint "main-breakpoint"
+
+    # Whether or not "print errno" will work often depends on the
+    # debuginfo available.  We can make some inferences about whether
+    # some of the tests should have xfail set-up by looking at the
+    # output of "ptype errno".  We pass these ptype tests, even for
+    # less than ideal outputs, because the point is to set up the
+    # xfail.  A failure can still occur for output which doesn't match
+    # any of those listed.
+    set test "ptype errno"
+    gdb_test_multiple $test $test {
+	-re "type = int\r\n$::gdb_prompt $" {
+	    pass $test
+	}
+	-re "type = .*no debug info.*$::gdb_prompt $" {
+	    pass $test
+	    set do_xfail 1
+	    set do_xfail_core_test 1
+	}
+	-re ".*Cannot find thread-local variables on this target.*$::gdb_prompt $" {
+	    pass $test
+	    set do_xfail 1
+	    set do_xfail_core_test 1
+	}
+	-re ".*has unknown return type; cast the call to its declared return type.*$::gdb_prompt $" {
+	    pass $test
+	    set do_xfail 1
+	    set do_xfail_core_test 1
+
+	    # On Linux, using -g3, which causes macro information to
+	    # be included in the debuginfo, errno might be defined as
+	    # follows:
+	    #
+	    #   #define errno (*__errno_location ())
+	    #
+	    # So, when we do "ptype errno", due to macro expansion,
+	    # this ends up being "ptype (*__errno_location ())".  So
+	    # the call to __errno_location (or something similar on
+	    # other OSes) is the call mentioned in the error message.
+	    #
+	    # For the test "print (int) errno", we're casting the
+	    # result of the expression, which includes both the call
+	    # along with a dereferencing operation.
+	    #
+	    # This will sometimes produce the right answer, but it's
+	    # also just as likely to fail.  E.g.  on x86_64, if the
+	    # address being returned as a 32-bit int is the same as
+	    # that which would have been returned as a 64-bit pointer,
+	    # then the test might pass.  Otherwise, it will almost
+	    # certainly fail, which is why we XFAIL it here.  But do
+	    # expect to see the occasional XPASS for this case.
+	    set do_xfail_cast 1
+	}
+    }
+
+    # If errno is defined as a macro that contains an obvious function
+    # call, it's likely that it won't work when debugging a core file.
+    set test "info macro errno"
+    gdb_test_multiple $test $test {
+	-re "Defined at.*\[\r\n\]#define.*\\\(\\\).*$::gdb_prompt $" {
+	    set do_xfail_core_test 1
+	    pass $test
+	}
+	-re "Defined at.*\[\r\n\]#define.*$::gdb_prompt $" {
+	    pass $test
+	}
+	-re "The symbol .errno. has no definition.*$::gdb_prompt $" {
+	    pass $test
+	}
+    }
+
+    if $do_xfail {
+	setup_xfail *-*-*
+    }
+    gdb_test "print errno" ".* = 42"
+
+    if $do_xfail_cast {
+	setup_xfail *-*-*
+    }
+    gdb_test "print (int) errno" ".* = 42"
+
+    set corefile ${::binfile}.core
+    set core_supported [gdb_gcore_cmd $corefile "save corefile"]
+    # Normally, we'd check core_supported here and return if it's
+    # not, but we'll defer that until after the shadow test.
+
+    gdb_breakpoint [gdb_get_line_number "shadow_errno-breakpoint"]
+    gdb_continue_to_breakpoint "shadow_errno-breakpoint"
+
+    # This test demonstrates why a simple hack to GDB for printing
+    # errno is a bad idea.  (The hack was to intercept the string
+    # "errno" in process_print_command_args() and replace it with
+    # "*(*(int *(*)(void)) __errno_location) ()".)
+    gdb_test "print errno" ".* = 36" "print masking errno"
+
+    # Finish test early if no core file was made.
+    if !$core_supported {
+	return
+    }
+
+    clean_restart $::binfile
+
+    set core_loaded [gdb_core_cmd $corefile "load corefile"]
+    if { $core_loaded == -1 } {
+	return
+    }
+    if $do_xfail_core_test {
+	setup_xfail *-*-*
+    }
+    gdb_test "print errno" ".* = 42" "check errno value from corefile"
+}
+
+set binprefix $binfile
+
+with_test_prefix "default" {
+    set binfile $binprefix-default
+    if  { [gdb_compile "${srcdir}/${subdir}/${srcfile}" "${binfile}" executable {debug}] != "" } {
+	untested "failed to compile"
+    } else {
+	do_tests
+    }
+}
+
+with_test_prefix "macros" {
+    set binfile $binprefix-macros
+    if  { [gdb_compile "${srcdir}/${subdir}/${srcfile}" "${binfile}" executable {debug macros}] != "" } {
+	untested "failed to compile"
+    } else {
+	do_tests
+    }
+}
+
+with_test_prefix "static" {
+    set binfile $binprefix-static
+    if  { [gdb_compile "${srcdir}/${subdir}/${srcfile}" "${binfile}" executable {debug "additional_flags=-static"}] != "" } {
+	untested "failed to compile"
+    } else {
+	set xfail_cast_test 0
+	if {[istarget *-linux*]} {
+	    set xfail_cast_test 1
+	}
+	do_tests $xfail_cast_test
+    }
+}
+
+with_test_prefix "static-macros" {
+    set binfile $binprefix-static-macros
+    if  { [gdb_compile "${srcdir}/${subdir}/${srcfile}" "${binfile}" executable {debug macros "additional_flags=-static"}] != "" } {
+	untested "failed to compile"
+    } else {
+	do_tests
+    }
+}
+
+with_test_prefix "pthreads" {
+    set binfile $binprefix-pthreads
+    if  { [gdb_compile_pthreads "${srcdir}/${subdir}/${srcfile}" "${binfile}" executable {debug}] != "" } {
+	untested "failed to compile"
+    } else {
+	do_tests
+    }
+}
+
+with_test_prefix "pthreads-macros" {
+    set binfile $binprefix-pthreads-macros
+    if  { [gdb_compile_pthreads "${srcdir}/${subdir}/${srcfile}" "${binfile}" executable {debug macros}] != "" } {
+	untested "failed to compile"
+    } else {
+	do_tests
+    }
+}
+
+with_test_prefix "pthreads-static" {
+    set binfile $binprefix-pthreads
+    if  { [gdb_compile_pthreads "${srcdir}/${subdir}/${srcfile}" "${binfile}" executable {debug "additional_flags=-static"}] != "" } {
+	untested "failed to compile"
+    } else {
+	set xfail_cast_test 0
+	if {[istarget *-linux*]} {
+	    set xfail_cast_test 1
+	}
+	do_tests $xfail_cast_test
+    }
+}
+
+with_test_prefix "pthreads-static-macros" {
+    set binfile $binprefix-pthreads-macros
+    if  { [gdb_compile_pthreads "${srcdir}/${subdir}/${srcfile}" "${binfile}" executable {debug macros "additional_flags=-static"}] != "" } {
+	untested "failed to compile"
+    } else {
+	do_tests
+    }
+}
