bp_manipulation_endian<sizeof (BREAK_INSN_LITTLE), \
BREAK_INSN_LITTLE, BREAK_INSN_BIG>
+/* Structure returned from gdbarch core_parse_exec_context method. Wraps
+ the execfn string and a vector containing the inferior argument. If a
+ gdbarch is unable to parse this information then an empty structure is
+ returned, check the execfn as an indication, if this is nullptr then no
+ other fields should be considered valid. */
+
+struct core_file_exec_context
+{
+ /* Constructor, just move everything into place. The EXEC_NAME should
+ 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. */
+ core_file_exec_context (gdb::unique_xmalloc_ptr<char> exec_name,
+ std::vector<gdb::unique_xmalloc_ptr<char>> argv)
+ : m_exec_name (std::move (exec_name)),
+ m_arguments (std::move (argv))
+ {
+ gdb_assert (m_exec_name != nullptr);
+ }
+
+ /* Create a default context object. In its default state a context
+ object holds no useful information, and will return false from its
+ valid() method. */
+ core_file_exec_context () = default;
+
+ /* Return true if this object contains valid context information. */
+ bool valid () const
+ { return m_exec_name != nullptr; }
+
+ /* Return the execfn string (executable name) as extracted from the core
+ file. Will always return non-nullptr if valid() returns true. */
+ const char *execfn () const
+ { return m_exec_name.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. */
+ const std::vector<gdb::unique_xmalloc_ptr<char>> &args () const
+ { return m_arguments; }
+
+private:
+
+ /* The executable filename as reported in the core file. Can be nullptr
+ if no executable name is found. */
+ gdb::unique_xmalloc_ptr<char> m_exec_name;
+
+ /* 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;
+};
+
/* Default implementation of gdbarch_displaced_hw_singlestep. */
extern bool default_displaced_step_hw_singlestep (struct gdbarch *);
read_core_file_mappings_pre_loop_ftype pre_loop_cb,
read_core_file_mappings_loop_ftype loop_cb);
+/* Default implementation of gdbarch_core_parse_exec_context. Returns
+ an empty core_file_exec_context. */
+extern core_file_exec_context default_core_parse_exec_context
+ (struct gdbarch *gdbarch, bfd *cbfd);
+
/* Default implementation of gdbarch
use_target_description_from_corefile_notes. */
extern bool default_use_target_description_from_corefile_notes
#include "cli/cli-utils.h"
#include "gdbarch.h"
#include "interps.h"
+#include "arch-utils.h"
void
reopen_exec_file (void)
}
}
+/* See arch-utils.h. */
+
+core_file_exec_context
+default_core_parse_exec_context (struct gdbarch *gdbarch, bfd *cbfd)
+{
+ return {};
+}
+\f
+
std::string
memory_error_message (enum target_xfer_status err,
struct gdbarch *gdbarch, CORE_ADDR memaddr)
static void
core_target_open (const char *arg, int from_tty)
{
- const char *p;
int siggy;
int scratch_chan;
int flags;
exception_print (gdb_stderr, except);
}
- p = bfd_core_file_failing_command (current_program_space->core_bfd ());
- if (p)
- gdb_printf (_("Core was generated by `%s'.\n"), p);
+ /* 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;
+ for (const auto &a : ctx.args ())
+ {
+ args += ' ';
+ args += a.get ();
+ }
+
+ gdb_printf (_("Core was generated by `%ps%s'.\n"),
+ styled_string (file_name_style.style (),
+ ctx.execfn ()),
+ args.c_str ());
+
+ /* Copy the arguments into the inferior. */
+ std::vector<char *> argv;
+ for (const auto &a : ctx.args ())
+ argv.push_back (a.get ());
+ gdb::array_view<char * const> view (argv.data (), argv.size ());
+ current_inferior ()->set_args (view);
+ }
+ else
+ {
+ gdb::unique_xmalloc_ptr<char> failing_command = make_unique_xstrdup
+ (bfd_core_file_failing_command (current_program_space->core_bfd ()));
+ if (failing_command != nullptr)
+ gdb_printf (_("Core was generated by `%s'.\n"),
+ failing_command.get ());
+ }
/* Clearing any previous state of convenience variables. */
clear_exit_convenience_vars ();
gdbarch_get_pc_address_flags_ftype *get_pc_address_flags = default_get_pc_address_flags;
gdbarch_read_core_file_mappings_ftype *read_core_file_mappings = default_read_core_file_mappings;
gdbarch_use_target_description_from_corefile_notes_ftype *use_target_description_from_corefile_notes = default_use_target_description_from_corefile_notes;
+ gdbarch_core_parse_exec_context_ftype *core_parse_exec_context = default_core_parse_exec_context;
};
/* Create a new ``struct gdbarch'' based on information provided by
/* Skip verify of get_pc_address_flags, invalid_p == 0. */
/* Skip verify of read_core_file_mappings, invalid_p == 0. */
/* Skip verify of use_target_description_from_corefile_notes, invalid_p == 0. */
+ /* Skip verify of core_parse_exec_context, invalid_p == 0. */
if (!log.empty ())
internal_error (_("verify_gdbarch: the following are invalid ...%s"),
log.c_str ());
gdb_printf (file,
"gdbarch_dump: use_target_description_from_corefile_notes = <%s>\n",
host_address_to_string (gdbarch->use_target_description_from_corefile_notes));
+ gdb_printf (file,
+ "gdbarch_dump: core_parse_exec_context = <%s>\n",
+ host_address_to_string (gdbarch->core_parse_exec_context));
if (gdbarch->dump_tdep != NULL)
gdbarch->dump_tdep (gdbarch, file);
}
{
gdbarch->use_target_description_from_corefile_notes = use_target_description_from_corefile_notes;
}
+
+core_file_exec_context
+gdbarch_core_parse_exec_context (struct gdbarch *gdbarch, bfd *cbfd)
+{
+ gdb_assert (gdbarch != NULL);
+ gdb_assert (gdbarch->core_parse_exec_context != NULL);
+ if (gdbarch_debug >= 2)
+ gdb_printf (gdb_stdlog, "gdbarch_core_parse_exec_context called\n");
+ return gdbarch->core_parse_exec_context (gdbarch, cbfd);
+}
+
+void
+set_gdbarch_core_parse_exec_context (struct gdbarch *gdbarch,
+ gdbarch_core_parse_exec_context_ftype core_parse_exec_context)
+{
+ gdbarch->core_parse_exec_context = core_parse_exec_context;
+}
typedef bool (gdbarch_use_target_description_from_corefile_notes_ftype) (struct gdbarch *gdbarch, struct bfd *corefile_bfd);
extern bool gdbarch_use_target_description_from_corefile_notes (struct gdbarch *gdbarch, struct bfd *corefile_bfd);
extern void set_gdbarch_use_target_description_from_corefile_notes (struct gdbarch *gdbarch, gdbarch_use_target_description_from_corefile_notes_ftype *use_target_description_from_corefile_notes);
+
+/* Examine the core file bfd object CBFD and try to extract the name of
+ the current executable and the argument list, which are return in a
+ core_file_exec_context object.
+
+ If for any reason the details can't be extracted from CBFD then an
+ empty context is returned.
+
+ It is required that the current inferior be the one associated with
+ CBFD, strings are read from the current inferior using target methods
+ which all assume current_inferior() is the one to read from. */
+
+typedef core_file_exec_context (gdbarch_core_parse_exec_context_ftype) (struct gdbarch *gdbarch, bfd *cbfd);
+extern core_file_exec_context gdbarch_core_parse_exec_context (struct gdbarch *gdbarch, bfd *cbfd);
+extern void set_gdbarch_core_parse_exec_context (struct gdbarch *gdbarch, gdbarch_core_parse_exec_context_ftype *core_parse_exec_context);
struct inferior;
struct x86_xsave_layout;
struct solib_ops;
+struct core_file_exec_context;
#include "regcache.h"
predefault="default_use_target_description_from_corefile_notes",
invalid=False,
)
+
+Method(
+ comment="""
+Examine the core file bfd object CBFD and try to extract the name of
+the current executable and the argument list, which are return in a
+core_file_exec_context object.
+
+If for any reason the details can't be extracted from CBFD then an
+empty context is returned.
+
+It is required that the current inferior be the one associated with
+CBFD, strings are read from the current inferior using target methods
+which all assume current_inferior() is the one to read from.
+""",
+ type="core_file_exec_context",
+ name="core_parse_exec_context",
+ params=[("bfd *", "cbfd")],
+ predefault="default_core_parse_exec_context",
+ invalid=False,
+)
}
}
+/* Try to extract the inferior arguments, environment, and executable name
+ from core file CBFD. */
+
+static core_file_exec_context
+linux_corefile_parse_exec_context_1 (struct gdbarch *gdbarch, bfd *cbfd)
+{
+ gdb_assert (gdbarch != nullptr);
+
+ /* If there's no core file loaded then we're done. */
+ if (cbfd == nullptr)
+ return {};
+
+ /* This function (currently) assumes the stack grows down. If this is
+ not the case then this function isn't going to help. */
+ if (!gdbarch_stack_grows_down (gdbarch))
+ return {};
+
+ int ptr_bytes = gdbarch_ptr_bit (gdbarch) / TARGET_CHAR_BIT;
+
+ /* Find the .auxv section in the core file. The BFD library creates this
+ for us from the AUXV note when the BFD is opened. If the section
+ can't be found then there's nothing more we can do. */
+ struct bfd_section * section = bfd_get_section_by_name (cbfd, ".auxv");
+ if (section == nullptr)
+ return {};
+
+ /* Grab the contents of the .auxv section. If we can't get the contents
+ then there's nothing more we can do. */
+ bfd_size_type size = bfd_section_size (section);
+ if (bfd_section_size_insane (cbfd, section))
+ return {};
+ gdb::byte_vector contents (size);
+ if (!bfd_get_section_contents (cbfd, section, contents.data (), 0, size))
+ return {};
+
+ /* Parse the .auxv section looking for the AT_EXECFN attribute. The
+ value of this attribute is a pointer to a string, the string is the
+ executable command. Additionally, this string is placed at the top of
+ the program stack, and so will be in the same PT_LOAD segment as the
+ argv and envp arrays. We can use this to try and locate these arrays.
+ If we can't find the AT_EXECFN attribute then we're not going to be
+ able to do anything else here. */
+ CORE_ADDR execfn_string_addr;
+ if (target_auxv_search (contents, current_inferior ()->top_target (),
+ gdbarch, AT_EXECFN, &execfn_string_addr) != 1)
+ return {};
+
+ /* Read in the program headers from CBFD. If we can't do this for any
+ reason then just give up. */
+ long phdrs_size = bfd_get_elf_phdr_upper_bound (cbfd);
+ if (phdrs_size == -1)
+ return {};
+ gdb::unique_xmalloc_ptr<Elf_Internal_Phdr>
+ phdrs ((Elf_Internal_Phdr *) xmalloc (phdrs_size));
+ int num_phdrs = bfd_get_elf_phdrs (cbfd, phdrs.get ());
+ if (num_phdrs == -1)
+ return {};
+
+ /* Now scan through the headers looking for the one which contains the
+ address held in EXECFN_STRING_ADDR, this is the address of the
+ executable command pointed too by the AT_EXECFN auxv entry. */
+ Elf_Internal_Phdr *hdr = nullptr;
+ for (int i = 0; i < num_phdrs; i++)
+ {
+ /* The program header that contains the address EXECFN_STRING_ADDR
+ should be one where all content is contained within CBFD, hence
+ the check that the file size matches the memory size. */
+ if (phdrs.get ()[i].p_type == PT_LOAD
+ && phdrs.get ()[i].p_vaddr <= execfn_string_addr
+ && (phdrs.get ()[i].p_vaddr
+ + phdrs.get ()[i].p_memsz) > execfn_string_addr
+ && phdrs.get ()[i].p_memsz == phdrs.get ()[i].p_filesz)
+ {
+ hdr = &phdrs.get ()[i];
+ break;
+ }
+ }
+
+ /* If we failed to find a suitable program header then give up. */
+ if (hdr == nullptr)
+ return {};
+
+ /* As we assume the stack grows down (see early check in this function)
+ we know that the information we are looking for sits somewhere between
+ EXECFN_STRING_ADDR and the segments virtual address. These define
+ the HIGH and LOW addresses between which we are going to search. */
+ CORE_ADDR low = hdr->p_vaddr;
+ CORE_ADDR high = execfn_string_addr;
+
+ /* This PTR is going to be the address we are currently accessing. */
+ CORE_ADDR ptr = align_down (high, ptr_bytes);
+
+ /* Setup DEREF a helper function which loads a value from an address.
+ The returned value is always placed into a uint64_t, even if we only
+ load 4-bytes, this allows the code below to be pretty generic. All
+ the values we're dealing with are unsigned, so this should be OK. */
+ enum bfd_endian byte_order = gdbarch_byte_order (gdbarch);
+ const auto deref = [=] (CORE_ADDR p) -> uint64_t
+ {
+ ULONGEST value = read_memory_unsigned_integer (p, ptr_bytes, byte_order);
+ return (uint64_t) value;
+ };
+
+ /* Now search down through memory looking for a PTR_BYTES sized object
+ which contains the value EXECFN_STRING_ADDR. The hope is that this
+ will be the AT_EXECFN entry in the auxv table. There is no guarantee
+ that we'll find the auxv table this way, but we will do our best to
+ validate that what we find is the auxv table, see below. */
+ while (ptr > low)
+ {
+ if (deref (ptr) == execfn_string_addr
+ && (ptr - ptr_bytes) > low
+ && deref (ptr - ptr_bytes) == AT_EXECFN)
+ break;
+
+ ptr -= ptr_bytes;
+ }
+
+ /* If we reached the lower bound then we failed -- bail out. */
+ if (ptr <= low)
+ return {};
+
+ /* Assuming that we are looking at a value field in the auxv table, move
+ forward PTR_BYTES bytes so we are now looking at the next key field in
+ the auxv table, then scan forward until we find the null entry which
+ will be the last entry in the auxv table. */
+ ptr += ptr_bytes;
+ while ((ptr + (2 * ptr_bytes)) < high
+ && (deref (ptr) != 0 || deref (ptr + ptr_bytes) != 0))
+ ptr += (2 * ptr_bytes);
+
+ /* PTR now points to the null entry in the auxv table, or we think it
+ does. Now we want to find the start of the auxv table. There's no
+ in-memory pattern we can search for at the start of the table, but
+ we can find the start based on the size of the .auxv section within
+ the core file CBFD object. In the actual core file the auxv is held
+ in a note, but the bfd library makes this into a section for us.
+
+ The addition of (2 * PTR_BYTES) here is because PTR is pointing at the
+ null entry, but the null entry is also included in CONTENTS. */
+ ptr = ptr + (2 * ptr_bytes) - contents.size ();
+
+ /* If we reached the lower bound then we failed -- bail out. */
+ if (ptr <= low)
+ return {};
+
+ /* PTR should now be pointing to the start of the auxv table mapped into
+ the inferior memory. As we got here using a heuristic then lets
+ compare an auxv table sized block of inferior memory, if this matches
+ then it's not a guarantee that we are in the right place, but it does
+ make it more likely. */
+ gdb::byte_vector target_contents (size);
+ if (target_read_memory (ptr, target_contents.data (), size) != 0)
+ memory_error (TARGET_XFER_E_IO, ptr);
+ if (memcmp (contents.data (), target_contents.data (), size) != 0)
+ return {};
+
+ /* We have reasonable confidence that PTR points to the start of the auxv
+ table. Below this should be the null terminated list of pointers to
+ environment strings, and below that the null terminated list of
+ pointers to arguments strings. After that we should find the
+ argument count. First, check for the null at the end of the
+ environment list. */
+ if (deref (ptr - ptr_bytes) != 0)
+ return {};
+
+ ptr -= (2 * ptr_bytes);
+ while (ptr > low && deref (ptr) != 0)
+ ptr -= ptr_bytes;
+
+ /* If we reached the lower bound then we failed -- bail out. */
+ if (ptr <= low)
+ return {};
+
+ /* PTR is now pointing to the null entry at the end of the argument
+ string pointer list. We now want to scan backward to find the entire
+ argument list. There's no handy null marker that we can look for
+ here, instead, as we scan backward we look for the argument count
+ (argc) value which appears immediately before the argument list.
+
+ Technically, we could have zero arguments, so the argument count would
+ be zero, however, we don't support this case. If we find a null entry
+ in the argument list before we find the argument count then we just
+ bail out.
+
+ Start by moving to the last argument string pointer, we expect this
+ to be non-null. */
+ ptr -= ptr_bytes;
+ uint64_t argc = 0;
+ while (ptr > low)
+ {
+ uint64_t val = deref (ptr);
+ if (val == 0)
+ return {};
+
+ if (val == argc)
+ break;
+
+ /* For GNU/Linux on ARM, glibc removes argc from the stack and
+ replaces it with the "stack-limit". This actually means a pointer
+ to the first argument string. This is unfortunate, but we can
+ still detect this case. */
+ if (val == (ptr + ptr_bytes))
+ break;
+
+ argc++;
+ ptr -= ptr_bytes;
+ }
+
+ /* If we reached the lower bound then we failed -- bail out. */
+ if (ptr <= low)
+ return {};
+
+ /* PTR is now pointing at the argument count value (or where the argument
+ count should be, see notes on ARM above). Move it forward so we're
+ pointing at the first actual argument string pointer. */
+ ptr += ptr_bytes;
+
+ /* We can now parse all of the argument strings. */
+ std::vector<gdb::unique_xmalloc_ptr<char>> arguments;
+
+ /* Skip the first argument. This is the executable command, but we'll
+ load that separately later. */
+ ptr += ptr_bytes;
+
+ uint64_t v;
+ while ((v = deref (ptr)) != 0)
+ {
+ gdb::unique_xmalloc_ptr<char> str = target_read_string (v, INT_MAX);
+ if (str == nullptr)
+ return {};
+ arguments.emplace_back (std::move (str));
+ ptr += ptr_bytes;
+ }
+
+ /* Skip the null-pointer at the end of the argument list. We will now
+ be pointing at the first environment string. */
+ ptr += ptr_bytes;
+
+ /* Parse the environment strings. Nothing is done with this yet, but
+ will be in a later commit. */
+ std::vector<gdb::unique_xmalloc_ptr<char>> environment;
+ while ((v = deref (ptr)) != 0)
+ {
+ gdb::unique_xmalloc_ptr<char> str = target_read_string (v, INT_MAX);
+ if (str == nullptr)
+ return {};
+ environment.emplace_back (std::move (str));
+ ptr += ptr_bytes;
+ }
+
+ gdb::unique_xmalloc_ptr<char> execfn
+ = target_read_string (execfn_string_addr, INT_MAX);
+ if (execfn == nullptr)
+ return {};
+
+ return core_file_exec_context (std::move (execfn),
+ std::move (arguments));
+}
+
+/* Parse and return execution context details from core file CBFD. */
+
+static core_file_exec_context
+linux_corefile_parse_exec_context (struct gdbarch *gdbarch, bfd *cbfd)
+{
+ /* Catch and discard memory errors.
+
+ If the core file format is not as we expect then we can easily trigger
+ a memory error while parsing the core file. We don't want this to
+ prevent the user from opening the core file; the information provided
+ by this function is helpful, but not critical, debugging can continue
+ without it. Instead just give a warning and return an empty context
+ object. */
+ try
+ {
+ return linux_corefile_parse_exec_context_1 (gdbarch, cbfd);
+ }
+ catch (const gdb_exception_error &ex)
+ {
+ if (ex.error == MEMORY_ERROR)
+ {
+ warning
+ (_("failed to parse execution context from corefile: %s"),
+ ex.message->c_str ());
+ return {};
+ }
+ else
+ throw;
+ }
+}
+
/* Fill the PRPSINFO structure with information about the process being
debugged. Returns 1 in case of success, 0 for failures. Please note that
even if the structure cannot be entirely filled (e.g., GDB was unable to
set_gdbarch_infcall_mmap (gdbarch, linux_infcall_mmap);
set_gdbarch_infcall_munmap (gdbarch, linux_infcall_munmap);
set_gdbarch_get_siginfo_type (gdbarch, linux_get_siginfo_type);
+ set_gdbarch_core_parse_exec_context (gdbarch,
+ linux_corefile_parse_exec_context);
}
void _initialize_linux_tdep ();
--- /dev/null
+/* 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;
+}
--- /dev/null
+# 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 can handle reading the full executable name and argument
+# list from a 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] == -1} {
+ untested "failed to compile"
+ return -1
+}
+
+# Linux core files can encore upto 80 characters for the command and
+# arguments in the psinfo. If BINFILE is less than 80 characters in
+# length then lets try to make it longer.
+set binfile_len [string length $binfile]
+if { $binfile_len <= 80 } {
+ set extra_len [expr 80 - $binfile_len + 1]
+ set extra_str [string repeat "x" $extra_len]
+ set new_binfile $binfile$extra_str
+ remote_exec build "mv $binfile $new_binfile"
+ set binfile $new_binfile
+}
+
+# Generate a core file, this time the inferior has no additional
+# arguments.
+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"
+
+# Load the core file and confirm that the full executable name is
+# seen.
+clean_restart $binfile
+set saw_generated_line false
+gdb_test_multiple "core-file $corefile_1" "load core file no args" {
+ -re "^Core was generated by `[string_to_regexp $binfile]'\\.\r\n" {
+ set saw_generated_line true
+ exp_continue
+ }
+
+ -re "^$gdb_prompt $" {
+ gdb_assert { $saw_generated_line } $gdb_test_name
+ }
+
+ -re "^\[^\r\n\]*\r\n" {
+ exp_continue
+ }
+}
+
+# Generate a core file, this time pass some arguments to the inferior.
+set args "aaaaa bbbbb ccccc ddddd eeeee"
+set corefile [core_find $binfile {} $args]
+if {$corefile == ""} {
+ untested "unable to create corefile"
+ return 0
+}
+set corefile_2 "$binfile.2.core"
+remote_exec build "mv $corefile $corefile_2"
+
+# Load the core file and confirm that the full executable name and
+# argument list are seen.
+clean_restart $binfile
+set saw_generated_line false
+gdb_test_multiple "core-file $corefile_2" "load core file with args" {
+ -re "^Core was generated by `[string_to_regexp $binfile] $args'\\.\r\n" {
+ set saw_generated_line true
+ exp_continue
+ }
+
+ -re "^$gdb_prompt $" {
+ gdb_assert { $saw_generated_line } $gdb_test_name
+ }
+
+ -re "^\[^\r\n\]*\r\n" {
+ exp_continue
+ }
+}
+
+# Also, the argument list should be available through 'show args'.
+gdb_test "show args" \
+ "Argument list to give program being debugged when it is started is \"$args\"\\."