[PATCHv3,4/5] gdb: improve GDB's ability to auto-load the exec for a core file

Message ID 91e04b778f6e3d79238e102ac950b7ade0b5eb1b.1730205615.git.aburgess@redhat.com
State New
Headers
Series Better executable auto-loading when opening a core file |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gdb_check--master-arm success Test passed
linaro-tcwg-bot/tcwg_gdb_check--master-aarch64 success Test passed

Commit Message

Andrew Burgess Oct. 29, 2024, 2:08 p.m. UTC
  GDB already has a limited mechanism for auto-loading the executable
corresponding to a core file, this can be found in the function
locate_exec_from_corefile_build_id in corelow.c.

However, this approach uses the build-id of the core file to look in
either the debug directory (for a symlink back to the executable) or
by asking debuginfod.  This is great, and works fine if the core file
is a "system" binary, but often, when I'm debugging a core file, it's
part of my development cycle, so there's no build-id symlink in the
debug directory, and debuginfod doesn't know about the binary either,
so GDB can't auto load the executable....

... but the executable is right there!

This commit builds on the earlier commits in this series to make GDB
smarter.

On GNU/Linux, when we parse the execution context from the core
file (see linux-tdep.c), we already grab the command pointed to by
AT_EXECFN.  If this is an absolute path then GDB can use this to
locate the executable, a build-id check ensures we've found the
correct file.  With this small change GDB suddenly becomes a lot
better at auto-loading the executable for a core file.

But we can do better!  Often the AT_EXECFN is not an absolute path.

If it is a relative path then we check for this path relative to the
core file.  This helps if a user does something like:

  $ ./build/bin/some_prog
  Aborted (core dumped)
  $ gdb -c corefile

In this case the core file in the current directory will have an
AT_EXECFN value of './build/bin/some_prog', so if we look for that
path relative to the location of the core file this might result in a
hit, again, a build-id check ensures we found the right file.

But we can do better still!  What if the user moves the core file?  Or
the user is using some tool to manage core files (e.g. the systemd
core file management tool), and the user downloads the core file to a
location from which the relative path no longer works?

Well in this case we can make use of the core file's mapped file
information (the NT_FILE note).  The executable will be included in
the mapped file list, and the path within the mapped file list will be
an absolute path.  We can search for mapped file information based on
an address within the mapped file, and the auxv vector happens to
include an AT_ENTRY value, which is the entry address in the main
executable.  If we look up the mapped file containing this address
we'll have the absolute path to the main executable, a build-id check
ensures this really is the file we're looking for.

It might be tempting to jump straight to the third approach, however,
there is one small downside to the third approach: if the executable
is a symlink then the AT_EXECFN string will be the name of the
symlink, that is, the thing the user asked to run.  The mapped file
entry will be the name of the actual file, i.e. the symlink target.
When we auto-load the executable based on the third approach, the file
loaded might have a different name to that which the user expects,
though the build-id check (almost) guarantees that we've loaded the
correct binary.

But there's one more thing we can check for!

If the user has placed the core file and the executable into a
directory together, for example, as might happen with a bug report,
then neither the absolute path check, nor the relative patch check
will find the executable.  So GDB will also look for a file with the
right name in the same directory as the core file.  Again, a build-id
check is performed to ensure we find the correct file.

Of course, it's still possible that GDB is unable to find the
executable using any of these approaches.  In this case, nothing
changes, GDB will check in the debug info directory for a build-id
based link back to the executable, and if that fails, GDB will ask
debuginfod for the executable.  If this all fails, then, as usual, the
user is able to load the correct executable with the 'file' command,
but hopefully, this should be needed far less from now on.
---
 gdb/arch-utils.h                              |  25 +-
 gdb/corelow.c                                 | 141 ++++++++--
 gdb/linux-tdep.c                              |  22 ++
 gdb/testsuite/gdb.base/corefile-find-exec.c   |  25 ++
 gdb/testsuite/gdb.base/corefile-find-exec.exp | 242 ++++++++++++++++++
 5 files changed, 438 insertions(+), 17 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/corefile-find-exec.c
 create mode 100644 gdb/testsuite/gdb.base/corefile-find-exec.exp
  

Patch

diff --git a/gdb/arch-utils.h b/gdb/arch-utils.h
index 1c33bfb4704..fb4a3ef9c5b 100644
--- a/gdb/arch-utils.h
+++ b/gdb/arch-utils.h
@@ -22,6 +22,7 @@ 
 
 #include "gdbarch.h"
 #include "gdbsupport/environ.h"
+#include "filenames.h"
 
 class frame_info_ptr;
 struct minimal_symbol;
@@ -87,15 +88,23 @@  struct core_file_exec_context
      never be nullptr.  Only call this constructor if all the arguments
      have been collected successfully, i.e. if the EXEC_NAME could be
      found but not ARGV then use the no-argument constructor to create an
-     empty context object.  */
+     empty context object.
+
+     The EXEC_FILENAME must be the absolute filename of the executable
+     that generated this core file, or nullptr if the absolute filename
+     is not known.  */
   core_file_exec_context (gdb::unique_xmalloc_ptr<char> exec_name,
+			  gdb::unique_xmalloc_ptr<char> exec_filename,
 			  std::vector<gdb::unique_xmalloc_ptr<char>> argv,
 			  std::vector<gdb::unique_xmalloc_ptr<char>> envp)
     : m_exec_name (std::move (exec_name)),
+      m_exec_filename (std::move (exec_filename)),
       m_arguments (std::move (argv)),
       m_environment (std::move (envp))
   {
     gdb_assert (m_exec_name != nullptr);
+    gdb_assert (exec_filename == nullptr
+		|| IS_ABSOLUTE_PATH (exec_filename.get ()));
   }
 
   /* Create a default context object.  In its default state a context
@@ -112,6 +121,13 @@  struct core_file_exec_context
   const char *execfn () const
   { return m_exec_name.get (); }
 
+  /* Return the absolute path to the executable if known.  This might
+     return nullptr even when execfn() returns a non-nullptr value.
+     Additionally, the file referenced here might have a different name
+     than the file returned by execfn if execfn is a symbolic link.  */
+  const char *exec_filename () const
+  { return m_exec_filename.get (); }
+
   /* Return the vector of inferior arguments as extracted from the core
      file.  This does not include argv[0] (the executable name) for that
      see the execfn() function.  */
@@ -127,6 +143,13 @@  struct core_file_exec_context
      if no executable name is found.  */
   gdb::unique_xmalloc_ptr<char> m_exec_name;
 
+  /* Full filename to the executable that was actually executed.  The name
+     within EXEC_FILENAME might not match what the user typed, e.g. if the
+     user typed ./symlinked_name which is a symlink to /tmp/real_name then
+     this is going to contain '/tmp/realname' while EXEC_NAME above will
+     contain './symlinkedname'.  */
+  gdb::unique_xmalloc_ptr<char> m_exec_filename;
+
   /* List of arguments.  Doesn't include argv[0] which is the executable
      name, for this look at m_exec_name field.  */
   std::vector<gdb::unique_xmalloc_ptr<char>> m_arguments;
diff --git a/gdb/corelow.c b/gdb/corelow.c
index a0129f84b1c..272b86b6f33 100644
--- a/gdb/corelow.c
+++ b/gdb/corelow.c
@@ -828,18 +828,117 @@  rename_vmcore_idle_reg_sections (bfd *abfd, inferior *inf)
 	     replacement_lwpid_str.c_str ());
 }
 
+/* Use CTX to try and find (and open) the executable file for the core file
+   CBFD.  BUILD_ID is the build-id for CBFD which was already extracted by
+   our caller.
+
+   Will return the opened executable or nullptr if the executable couldn't
+   be found.  */
+
+static gdb_bfd_ref_ptr
+locate_exec_from_corefile_exec_context (bfd *cbfd,
+					const bfd_build_id *build_id,
+					const core_file_exec_context &ctx)
+{
+  /* CTX must be valid, and a valid context has an execfn() string.  */
+  gdb_assert (ctx.valid ());
+  gdb_assert (ctx.execfn () != nullptr);
+
+  /* EXEC_NAME will be the command used to start the inferior.  This might
+     not be an absolute path (but could be).  */
+  const char *exec_name = ctx.execfn ();
+
+  /* Function to open FILENAME and check if its build-id matches BUILD_ID
+     from this enclosing scope.  Returns the open BFD for filename if the
+     FILENAME has a matching build-id, otherwise, returns nullptr.  */
+  const auto open_and_check_build_id
+    = [&build_id] (const char *filename) -> gdb_bfd_ref_ptr
+  {
+    /* Try to open a file.  If this succeeds then we still need to perform
+       a build-id check.  */
+    gdb_bfd_ref_ptr execbfd = gdb_bfd_open (filename, gnutarget);
+
+    /* We managed to open a file, but if it's build-id doesn't match
+       BUILD_ID then we just cannot trust it's the right file.  */
+    if (execbfd != nullptr)
+      {
+	const bfd_build_id *other_build_id = build_id_bfd_get (execbfd.get ());
+
+	if (other_build_id == nullptr
+	    || !build_id_equal (other_build_id, build_id))
+	  execbfd = nullptr;
+      }
+
+    return execbfd;
+  };
+
+  gdb_bfd_ref_ptr execbfd;
+
+  /* If EXEC_NAME is absolute then try to open it now.  Otherwise, see if
+     EXEC_NAME is a relative path from the location of the core file.  This
+     is just a guess, the executable might not be here, but we still rely
+     on a build-id match in order to accept any executable we find; we
+     don't accept something just because it happens to be in the right
+     location.  */
+  if (IS_ABSOLUTE_PATH (exec_name))
+    execbfd = open_and_check_build_id (exec_name);
+  else
+    {
+      std::string p = (ldirname (bfd_get_filename (cbfd))
+		       + '/'
+		       + exec_name);
+      execbfd = open_and_check_build_id (p.c_str ());
+    }
+
+  /* If we haven't found the executable yet, then try checking to see if
+     the executable is in the same directory as the core file.  Again,
+     there's no reason why this should be the case, but it's worth a try,
+     and the build-id check should ensure we don't use an invalid file if
+     we happen to find one.  */
+  if (execbfd == nullptr)
+    {
+      const char *base_name = lbasename (exec_name);
+      std::string p = (ldirname (bfd_get_filename (cbfd))
+		       + '/'
+		       + base_name);
+      execbfd = open_and_check_build_id (p.c_str ());
+    }
+
+  /* If the above didn't provide EXECBFD then try the exec_filename from
+     the context.  This will be an absolute filename which the gdbarch code
+     figured out from the core file.  In some cases the gdbarch code might
+     not be able to figure out a suitable absolute filename though.  */
+  if (execbfd == nullptr && ctx.exec_filename () != nullptr)
+    {
+      gdb_assert (IS_ABSOLUTE_PATH (ctx.exec_filename ()));
+
+      /* Try to open a file.  If this succeeds then we still need to
+	 perform a build-id check.  */
+      execbfd = open_and_check_build_id (ctx.exec_filename ());
+    }
+
+  return execbfd;
+}
+
 /* Locate (and load) an executable file (and symbols) given the core file
    BFD ABFD.  */
 
 static void
-locate_exec_from_corefile_build_id (bfd *abfd, int from_tty)
+locate_exec_from_corefile_build_id (bfd *abfd,
+				    const core_file_exec_context &ctx,
+				    int from_tty)
 {
   const bfd_build_id *build_id = build_id_bfd_get (abfd);
   if (build_id == nullptr)
     return;
 
-  gdb_bfd_ref_ptr execbfd
-    = find_objfile_by_build_id (build_id, abfd->filename);
+  gdb_bfd_ref_ptr execbfd;
+
+  if (ctx.valid ())
+    execbfd = locate_exec_from_corefile_exec_context (abfd, build_id, ctx);
+
+  if (execbfd == nullptr)
+    execbfd = find_objfile_by_build_id (build_id, abfd->filename);
 
   if (execbfd != nullptr)
     {
@@ -908,13 +1007,6 @@  core_target_open (const char *arg, int from_tty)
 
   validate_files ();
 
-  /* If we have no exec file, try to set the architecture from the
-     core file.  We don't do this unconditionally since an exec file
-     typically contains more information that helps us determine the
-     architecture than a core file.  */
-  if (!current_program_space->exec_bfd ())
-    set_gdbarch_from_file (current_program_space->core_bfd ());
-
   current_inferior ()->push_target (std::move (target_holder));
 
   switch_to_no_thread ();
@@ -969,9 +1061,31 @@  core_target_open (const char *arg, int from_tty)
       switch_to_thread (thread);
     }
 
+  /* In order to parse the exec context from the core file the current
+     inferior needs to have a suitable gdbarch set.  If an exec file is
+     loaded then the gdbarch will have been set based on the exec file, but
+     if not, ensure we have a suitable gdbarch in place now.  */
+  if (current_program_space->exec_bfd () == nullptr)
+      current_inferior ()->set_arch (target->core_gdbarch ());
+
+  /* See if the gdbarch can find the executable name and argument list from
+     the core file.  */
+  core_file_exec_context ctx
+    = gdbarch_core_parse_exec_context (target->core_gdbarch (),
+				       current_program_space->core_bfd ());
+
+  /* If we don't have an executable loaded then see if we can locate one
+     based on the core file.  */
   if (current_program_space->exec_bfd () == nullptr)
     locate_exec_from_corefile_build_id (current_program_space->core_bfd (),
-					from_tty);
+					ctx, from_tty);
+
+  /* If we have no exec file, try to set the architecture from the
+     core file.  We don't do this unconditionally since an exec file
+     typically contains more information that helps us determine the
+     architecture than a core file.  */
+  if (current_program_space->exec_bfd () == nullptr)
+    set_gdbarch_from_file (current_program_space->core_bfd ());
 
   post_create_inferior (from_tty);
 
@@ -989,11 +1103,6 @@  core_target_open (const char *arg, int from_tty)
       exception_print (gdb_stderr, except);
     }
 
-  /* See if the gdbarch can find the executable name and argument list from
-     the core file.  */
-  core_file_exec_context ctx
-    = gdbarch_core_parse_exec_context (target->core_gdbarch (),
-				       current_program_space->core_bfd ());
   if (ctx.valid ())
     {
       std::string args;
diff --git a/gdb/linux-tdep.c b/gdb/linux-tdep.c
index e89bda9af13..354efe8f37b 100644
--- a/gdb/linux-tdep.c
+++ b/gdb/linux-tdep.c
@@ -2090,7 +2090,29 @@  linux_corefile_parse_exec_context_1 (struct gdbarch *gdbarch, bfd *cbfd)
   if (execfn == nullptr)
     return {};
 
+  /* When the core-file was loaded GDB processed the file backed mappings
+     (from the NT_FILE note).  One of these should have been for the
+     executable.  The AT_EXECFN string might not be an absolute path, but
+     the path in NT_FILE will be absolute, though if AT_EXECFN is a
+     symlink, then the NT_FILE entry will point to the actual file, not the
+     symlink.
+
+     Use the AT_ENTRY address to look for the NT_FILE entry which contains
+     that address, this should be the executable.  */
+  gdb::unique_xmalloc_ptr<char> exec_filename;
+  CORE_ADDR exec_entry_addr;
+  if (target_auxv_search (contents, current_inferior ()->top_target (),
+			  gdbarch, AT_ENTRY, &exec_entry_addr) == 1)
+    {
+      std::optional<core_target_mapped_file_info> info
+	= core_target_find_mapped_file (nullptr, exec_entry_addr);
+      if (info.has_value () && !info->filename ().empty ()
+	  && IS_ABSOLUTE_PATH (info->filename ().c_str ()))
+	exec_filename = make_unique_xstrdup (info->filename ().c_str ());
+    }
+
   return core_file_exec_context (std::move (execfn),
+				 std::move (exec_filename),
 				 std::move (arguments),
 				 std::move (environment));
 }
diff --git a/gdb/testsuite/gdb.base/corefile-find-exec.c b/gdb/testsuite/gdb.base/corefile-find-exec.c
new file mode 100644
index 00000000000..ed4df606a2d
--- /dev/null
+++ b/gdb/testsuite/gdb.base/corefile-find-exec.c
@@ -0,0 +1,25 @@ 
+/* 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 <stdlib.h>
+
+int
+main (int argc, char **argv)
+{
+  abort ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/corefile-find-exec.exp b/gdb/testsuite/gdb.base/corefile-find-exec.exp
new file mode 100644
index 00000000000..40324c1f01c
--- /dev/null
+++ b/gdb/testsuite/gdb.base/corefile-find-exec.exp
@@ -0,0 +1,242 @@ 
+# 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/>.
+
+# Check GDB's ability to auto-load the executable based on the file
+# names extracted from the core file.
+#
+# Currently, only Linux supports reading full executable and arguments
+# from a core file.
+require {istarget *-linux*}
+
+standard_testfile
+
+if {[build_executable $testfile.exp $testfile $srcfile {debug build-id}] == -1} {
+    untested "failed to compile"
+    return -1
+}
+
+# Load the COREFILE and confirm that GDB auto-loads the executable.
+# The symbols should be read from SYMBOL_FILE and the core file should
+# be reported as generated by GEN_FROM_FILE.
+proc test_load { corefile symbol_file gen_from_file } {
+    clean_restart
+    set saw_generated_line false
+    set saw_reading_symbols false
+
+    gdb_test_multiple "core-file $corefile" "load core file" {
+
+	-re "^Reading symbols from [string_to_regexp $symbol_file]\\.\\.\\.\r\n" {
+	    set saw_reading_symbols true
+	    exp_continue
+	}
+
+	-re "^Core was generated by `[string_to_regexp $gen_from_file]'\\.\r\n" {
+	    set saw_generated_line true
+	    exp_continue
+	}
+
+	-re "^$::gdb_prompt $" {
+	    gdb_assert { $saw_generated_line && $saw_reading_symbols} \
+		$gdb_test_name
+	}
+
+	-re "^\[^\r\n\]*\r\n" {
+	    exp_continue
+	}
+    }
+}
+
+with_test_prefix "absolute path" {
+    # Generate a core file, this uses an absolute path to the
+    # executable.
+    with_test_prefix "to file" {
+	set corefile [core_find $binfile]
+	if {$corefile == ""} {
+	    untested "unable to create corefile"
+	    return 0
+	}
+	set corefile_1 "$binfile.1.core"
+	remote_exec build "mv $corefile $corefile_1"
+
+	test_load $corefile_1 $binfile $binfile
+    }
+
+    # And create a symlink, and repeat the test using an absolute path
+    # to the symlink.
+    with_test_prefix "to symlink" {
+	set symlink_name "symlink_1"
+	set symlink [standard_output_file $symlink_name]
+
+	with_cwd [standard_output_file ""] {
+	    remote_exec build "ln -s ${testfile} $symlink_name"
+	}
+
+	set corefile [core_find $symlink]
+	if {$corefile == ""} {
+	    untested "unable to create corefile"
+	    return 0
+	}
+	set corefile_2 "$binfile.2.core"
+	remote_exec build "mv $corefile $corefile_2"
+
+	test_load $corefile_2 $symlink $symlink
+    }
+
+    # Like the previous test, except this time, delete the symlink
+    # after generating the core file.  GDB should be smart enough to
+    # figure out that we can use the underlying TESTFILE binary.
+    with_test_prefix "to deleted symlink" {
+	set symlink_name "symlink_2"
+	set symlink [standard_output_file $symlink_name]
+
+	with_cwd [standard_output_file ""] {
+	    remote_exec build "ln -s ${testfile} $symlink_name"
+	}
+
+	set corefile [core_find $symlink]
+	if {$corefile == ""} {
+	    untested "unable to create corefile"
+	    return 0
+	}
+	set corefile_3 "$binfile.3.core"
+	remote_exec build "mv $corefile $corefile_3"
+
+	remote_exec build "rm -f $symlink"
+
+	test_load $corefile_3 $binfile $symlink
+    }
+
+    # Generate the core file with an absolute path to the executable,
+    # but move the core file and executable into a single directory
+    # together so GDB can't use the absolute path to find the
+    # executable.
+    #
+    # GDB should still find the executable though, but looking in the
+    # same directory as the core file.
+    with_test_prefix "in side directory" {
+	set binfile_2 [standard_output_file ${testfile}_2]
+	remote_exec build "cp $binfile $binfile_2"
+
+	set corefile [core_find $binfile_2]
+	if {$corefile == ""} {
+	    untested "unable to create corefile"
+	    return 0
+	}
+	set corefile_4 "$binfile.4.core"
+	remote_exec build "mv $corefile $corefile_4"
+
+	set side_dir [standard_output_file side_dir]
+	remote_exec build "mkdir -p $side_dir"
+	remote_exec build "mv $binfile_2 $side_dir"
+	remote_exec build "mv $corefile_4 $side_dir"
+
+	set relocated_corefile_4 [file join $side_dir [file tail $corefile_4]]
+	set relocated_binfile_2 [file join $side_dir [file tail $binfile_2]]
+	test_load $relocated_corefile_4 $relocated_binfile_2 $binfile_2
+    }
+}
+
+with_test_prefix "relative path" {
+    # Generate a core file using relative a path.  We ned to work
+    # around the core_find proc a little here.  The core_find proc
+    # creates a sub-directory using standard_output_file and runs the
+    # test binary from inside that directory.
+    #
+    # Usually core_find is passed an absolute path, so thre's no
+    # problem, but we want to pass a relative path.
+    #
+    # So setup a directory structure like this:
+    #
+    # corefile-find-exec/
+    #    reldir/
+    #      <copy of $binfile here>
+    #    workdir/
+    #
+    # Place a copy of BINFILE in 'reldir/' and switch to workdir, use
+    # core_find which will create a sibling directory of workdir, and
+    # run the relative path from there.  We then move the generated
+    # core file back into 'workdir/', this leaves a tree like:
+    #
+    # corefile-find-exec/
+    #    reldir/
+    #      <copy of $binfile here>
+    #    workdir/
+    #      <core file here>
+    #
+    # Now we can ask GDB to open the core file, if all goes well GDB
+    # should make use of the relative path encoded in the core file to
+    # locate the executable in 'reldir/'.
+    #
+    # We also setup a symlink in 'reldir' that points to the
+    # executable and repeat the test, but this time executing the
+    # symlink.
+    set reldir_name "reldir"
+    set reldir [standard_output_file $reldir_name]
+    remote_exec build "mkdir -p $reldir"
+
+    set alt_testfile "alt_${testfile}"
+    set binfile_3 "$reldir/${alt_testfile}"
+    remote_exec build "cp $binfile $binfile_3"
+
+    set symlink_2 "symlink_2"
+    with_cwd $reldir {
+	remote_exec build "ln -s ${alt_testfile} ${symlink_2}"
+    }
+
+    set work_dir [standard_output_file "workdir"]
+    remote_exec build "mkdir -p $work_dir"
+
+    set rel_path_to_file "../${reldir_name}/${alt_testfile}"
+    set rel_path_to_symlink_2 "../${reldir_name}/${symlink_2}"
+
+    with_cwd $work_dir {
+	with_test_prefix "to file" {
+	    set corefile [core_find $rel_path_to_file]
+	    if {$corefile == ""} {
+		untested "unable to create corefile"
+		return 0
+	    }
+	    set corefile_5 "${work_dir}/${testfile}.5.core"
+	    remote_exec build "mv $corefile $corefile_5"
+
+	    test_load $corefile_5 \
+		[file join $work_dir $rel_path_to_file] \
+		$rel_path_to_file
+	}
+
+	with_test_prefix "to symlink" {
+	    set corefile [core_find $rel_path_to_symlink_2]
+	    if {$corefile == ""} {
+		untested "unable to create corefile"
+		return 0
+	    }
+	    set corefile_6 "${work_dir}/${testfile}.6.core"
+	    remote_exec build "mv $corefile $corefile_6"
+
+	    test_load $corefile_6 \
+		[file join $work_dir $rel_path_to_symlink_2] \
+		$rel_path_to_symlink_2
+	}
+
+	# Move the core file.  Now the relative path doesn't work so
+	# we instead rely on GDB to use information about the mapped
+	# files to help locate the executable.
+	with_test_prefix "with moved corefile" {
+	    set corefile_7 [standard_output_file "${testfile}.7.core"]
+	    remote_exec build "cp $corefile_6 $corefile_7"
+	    test_load $corefile_7 $binfile_3 $rel_path_to_symlink_2
+	}
+    }
+}