Re: [RFA, 3 of 3] save/restore process record, part 3 (save/restore)

Michael Snyder
2009-10-19T18:00:55Z

[-- Attachment #1: Type: text/plain, Size: 792 bytes --]

Hui Zhu wrote:
> Hi Michael,
>
> + do_cleanups (old_cleanups);
>
> This line will remove the record file that we just save.
>
> I change some code:
> static void
> record_save_cleanups (void *data)
> {
> bfd *obfd = data;
> //char *pathname = xstrdup (bfd_get_filename (obfd));
> bfd_close (obfd);
> //unlink (pathname);
> //xfree (pathname);
> }
>
> I think you want unlink the gdb_record when save get some error. It
> maybe need "discard_cleanups" the old_cleanups and bfd_close (obfd);
>
> After change the code, everything is OK.

Yes. Thanks. Like this:
+ if (record_list->prev)
+ record_list = record_list->prev;
+ }
+
+ do_cleanups (set_cleanups);
+ bfd_close (obfd);
+ discard_cleanups (old_cleanups);
+
+ /* Succeeded. */

New diff attached.

[-- Attachment #2: tmp12b.txt --]
[-- Type: text/plain, Size: 23203 bytes --]

--- record.2b.c 2009-10-18 10:17:40.000000000 -0700
+++ record.12b.c 2009-10-19 10:52:28.000000000 -0700
@@ -23,10 +23,15 @@
#include "gdbthread.h"
#include "event-top.h"
#include "exceptions.h"
+#include "completer.h"
+#include "arch-utils.h"
#include "gdbcore.h"
#include "exec.h"
#include "record.h"
+#include "elf-bfd.h"
+#include "gcore.h"

+#include <byteswap.h>
#include <signal.h>

#define DEFAULT_RECORD_INSN_MAX_NUM 200000
@@ -34,6 +39,8 @@
#define RECORD_IS_REPLAY \
(record_list->next || execution_direction == EXEC_REVERSE)

+#define RECORD_FILE_MAGIC netorder32(0x20091016)
+
/* These are the core structs of the process record functionality.

A record_entry is a record of the value change of a register
@@ -482,24 +489,24 @@ record_check_insn_num (int set_terminal)
if (q)
record_stop_at_limit = 0;
else
- error (_("Process record: inferior program stopped."));
+ error (_("Process record: stopped by user."));
}
}
}
}

+static void
+record_arch_list_cleanups (void *ignore)
+{
+ record_list_release (record_arch_list_tail);
+}
+
/* Before inferior step (when GDB record the running message, inferior
only can step), GDB will call this function to record the values to
record_list. This function will call gdbarch_process_record to
record the running message of inferior and set them to
record_arch_list, and add it to record_list. */

-static void
-record_message_cleanups (void *ignore)
-{
- record_list_release (record_arch_list_tail);
-}
-
struct record_message_args {
struct regcache *regcache;
enum target_signal signal;
@@ -511,7 +518,7 @@ record_message (void *args)
int ret;
struct record_message_args *myargs = args;
struct gdbarch *gdbarch = get_regcache_arch (myargs->regcache);
- struct cleanup *old_cleanups = make_cleanup (record_message_cleanups, 0);
+ struct cleanup *old_cleanups = make_cleanup (record_arch_list_cleanups, 0);

record_arch_list_head = NULL;
record_arch_list_tail = NULL;
@@ -651,8 +658,8 @@ record_exec_insn (struct regcache *regca
{
entry->u.mem.mem_entry_not_accessible = 1;
if (record_debug)
- warning (_("Process record: error reading memory at "
- "addr = %s len = %d."),
+ warning ("Process record: error reading memory at "
+ "addr = %s len = %d.",
paddress (gdbarch, entry->u.mem.addr),
entry->u.mem.len);
}
@@ -664,8 +671,8 @@ record_exec_insn (struct regcache *regca
{
entry->u.mem.mem_entry_not_accessible = 1;
if (record_debug)
- warning (_("Process record: error writing memory at "
- "addr = %s len = %d."),
+ warning ("Process record: error writing memory at "
+ "addr = %s len = %d.",
paddress (gdbarch, entry->u.mem.addr),
entry->u.mem.len);
}
@@ -678,6 +685,217 @@ record_exec_insn (struct regcache *regca
}
}

+/* bfdcore_read -- read bytes from a core file section. */
+
+static inline void
+bfdcore_read (bfd *obfd, asection *osec, void *buf, int len, int *offset)
+{
+ int ret = bfd_get_section_contents (obfd, osec, buf, *offset, len);
+
+ if (ret)
+ *offset += len;
+ else
+ error (_("Failed to read %d bytes from core file %s ('%s').\n"),
+ len, bfd_get_filename (obfd),
+ bfd_errmsg (bfd_get_error ()));
+}
+
+static inline uint64_t
+netorder64 (uint64_t fromfile)
+{
+ return (BYTE_ORDER == LITTLE_ENDIAN)
+ ? bswap_64 (fromfile)
+ : fromfile;
+}
+
+static inline uint32_t
+netorder32 (uint32_t fromfile)
+{
+ return (BYTE_ORDER == LITTLE_ENDIAN)
+ ? bswap_32 (fromfile)
+ : fromfile;
+}
+
+static inline uint16_t
+netorder16 (uint16_t fromfile)
+{
+ return (BYTE_ORDER == LITTLE_ENDIAN)
+ ? bswap_16 (fromfile)
+ : fromfile;
+}
+
+/* Restore the execution log from a core_bfd file. */
+
+static void
+record_restore (void)
+{
+ uint32_t magic;
+ struct cleanup *old_cleanups;
+ struct record_entry *rec;
+ asection *osec;
+ uint32_t osec_size;
+ int bfd_offset = 0;
+ struct regcache *regcache;
+
+ /* We restore the execution log from the open core bfd,
+ if there is one. */
+ if (core_bfd == NULL)
+ return;
+
+ /* "record_restore" can only be called when record list is empty. */
+ gdb_assert (record_first.next == NULL);
+
+ if (record_debug)
+ printf_filtered ("Restoring recording from core file.\n");
+
+ /* Now need to find our special note section. */
+ osec = bfd_get_section_by_name (core_bfd, "null0");
+ osec_size = bfd_section_size (core_bfd, osec);
+ if (record_debug)
+ printf_filtered ("Find precord section %s.\n",
+ osec ? "succeeded" : "failed");
+ if (!osec)
+ return;
+ if (record_debug)
+ printf_filtered ("%s", bfd_section_name (core_bfd, osec));
+
+ /* Check the magic code. */
+ bfdcore_read (core_bfd, osec, &magic, sizeof (magic), &bfd_offset);
+ if (magic != RECORD_FILE_MAGIC)
+ error (_("Version mis-match or file format error in core file %s."),
+ bfd_get_filename (core_bfd));
+ if (record_debug)
+ printf_filtered ("\
+ Reading 4-byte magic cookie RECORD_FILE_MAGIC (0x%08x)\n",
+ netorder32 (magic));
+
+ /* Restore the entries in recfd into record_arch_list_head and
+ record_arch_list_tail. */
+ record_arch_list_head = NULL;
+ record_arch_list_tail = NULL;
+ record_insn_num = 0;
+ old_cleanups = make_cleanup (record_arch_list_cleanups, 0);
+ regcache = get_current_regcache ();
+
+ while (1)
+ {
+ int ret;
+ uint8_t tmpu8;
+ uint32_t regnum, len, signal, count;
+ uint64_t addr;
+
+ /* We are finished when offset reaches osec_size. */
+ if (bfd_offset >= osec_size)
+ break;
+ bfdcore_read (core_bfd, osec, &tmpu8, sizeof (tmpu8), &bfd_offset);
+
+ switch (tmpu8)
+ {
+ case record_reg: /* reg */
+ /* Get register number to regnum. */
+ bfdcore_read (core_bfd, osec, &regnum,
+ sizeof (regnum), &bfd_offset);
+ regnum = netorder32 (regnum);
+
+ rec = record_reg_alloc (regcache, regnum);
+
+ /* Get val. */
+ bfdcore_read (core_bfd, osec, record_get_loc (rec),
+ rec->u.reg.len, &bfd_offset);
+
+ if (record_debug)
+ printf_filtered ("\
+ Reading register %d (1 plus %d plus %d bytes)\n",
+ rec->u.reg.num,
+ sizeof (regnum),
+ rec->u.reg.len);
+ break;
+
+ case record_mem: /* mem */
+ /* Get len. */
+ bfdcore_read (core_bfd, osec, &len,
+ sizeof (len), &bfd_offset);
+ len = netorder32 (len);
+
+ /* Get addr. */
+ bfdcore_read (core_bfd, osec, &addr,
+ sizeof (addr), &bfd_offset);
+ addr = netorder64 (addr);
+
+ rec = record_mem_alloc (addr, len);
+
+ /* Get val. */
+ bfdcore_read (core_bfd, osec, record_get_loc (rec),
+ rec->u.mem.len, &bfd_offset);
+
+ if (record_debug)
+ printf_filtered ("\
+ Reading memory %s (1 plus %d plus %d plus %d bytes)\n",
+ paddress (get_current_arch (),
+ rec->u.mem.addr),
+ sizeof (addr),
+ sizeof (len),
+ rec->u.mem.len);
+ break;
+
+ case record_end: /* end */
+ rec = record_end_alloc ();
+ record_insn_num ++;
+
+ /* Get signal value. */
+ bfdcore_read (core_bfd, osec, &signal,
+ sizeof (signal), &bfd_offset);
+ signal = netorder32 (signal);
+ rec->u.end.sigval = signal;
+
+ /* Get insn count. */
+ bfdcore_read (core_bfd, osec, &count,
+ sizeof (count), &bfd_offset);
+ count = netorder32 (count);
+ rec->u.end.insn_num = count;
+ record_insn_count = count + 1;
+ if (record_debug)
+ printf_filtered ("\
+ Reading record_end (1 + %d + %d bytes), offset == %s\n",
+ sizeof (signal),
+ sizeof (count),
+ paddress (get_current_arch (),
+ bfd_offset));
+ break;
+
+ default:
+ error (_("Bad entry type in core file %s."),
+ bfd_get_filename (core_bfd));
+ break;
+ }
+
+ /* Add rec to record arch list. */
+ record_arch_list_add (rec);
+ }
+
+ discard_cleanups (old_cleanups);
+
+ /* Add record_arch_list_head to the end of record list. */
+ record_first.next = record_arch_list_head;
+ record_arch_list_head->prev = &record_first;
+ record_arch_list_tail->next = NULL;
+ record_list = &record_first;
+
+ /* Update record_insn_max_num. */
+ if (record_insn_num > record_insn_max_num)
+ {
+ record_insn_max_num = record_insn_num;
+ warning (_("Auto increase record/replay buffer limit to %d."),
+ record_insn_max_num);
+ }
+
+ /* Succeeded. */
+ printf_filtered (_("Restored records from core file %s.\n"),
+ bfd_get_filename (core_bfd));
+
+ print_stack_frame (get_selected_frame (NULL), 1, SRC_AND_LOC);
+}
+
static struct target_ops *tmp_to_resume_ops;
static void (*tmp_to_resume) (struct target_ops *, ptid_t, int,
enum target_signal);
@@ -728,6 +946,7 @@ record_core_open_1 (char *name, int from
}

push_target (&record_core_ops);
+ record_restore ();
}

static void
@@ -735,9 +954,6 @@ record_open_1 (char *name, int from_tty)
{
struct target_ops *t;

- if (record_debug)
- fprintf_unfiltered (gdb_stdlog, "Process record: record_open\n");
-
/* check exec */
if (!target_has_execution)
error (_("Process record: the program is not being run."));
@@ -779,6 +995,12 @@ record_open (char *name, int from_tty)
error (_("Process record target already running. Use \"record stop\" to "
"stop record target first."));

+ /* Reset */
+ record_insn_num = 0;
+ record_insn_count = 0;
+ record_list = &record_first;
+ record_list->next = NULL;
+
/* Reset the tmp beneath pointers. */
tmp_to_resume_ops = NULL;
tmp_to_resume = NULL;
@@ -822,17 +1044,6 @@ record_open (char *name, int from_tty)
if (!tmp_to_xfer_partial)
error (_("Could not find 'to_xfer_partial' method on the target stack."));

- if (current_target.to_stratum == core_stratum)
- record_core_open_1 (name, from_tty);
- else
- record_open_1 (name, from_tty);
-
- /* Reset */
- record_insn_num = 0;
- record_insn_count = 0;
- record_list = &record_first;
- record_list->next = NULL;
-
/* Set the tmp beneath pointers to beneath pointers. */
record_beneath_to_resume_ops = tmp_to_resume_ops;
record_beneath_to_resume = tmp_to_resume;
@@ -844,6 +1055,11 @@ record_open (char *name, int from_tty)
record_beneath_to_xfer_partial = tmp_to_xfer_partial;
record_beneath_to_insert_breakpoint = tmp_to_insert_breakpoint;
record_beneath_to_remove_breakpoint = tmp_to_remove_breakpoint;
+
+ if (current_target.to_stratum == core_stratum)
+ record_core_open_1 (name, from_tty);
+ else
+ record_open_1 (name, from_tty);
}

static void
@@ -1114,8 +1330,7 @@ record_wait (struct target_ops *ops,
{
if (record_debug)
fprintf_unfiltered (gdb_stdlog,
- "Process record: break "
- "at %s.\n",
+ "Process record: break at %s.\n",
paddress (gdbarch, tmp_pc));
if (gdbarch_decr_pc_after_break (gdbarch)
&& execution_direction == EXEC_FORWARD
@@ -1190,8 +1405,7 @@ static void
record_mourn_inferior (struct target_ops *ops)
{
if (record_debug)
- fprintf_unfiltered (gdb_stdlog, "Process record: "
- "record_mourn_inferior\n");
+ fprintf_unfiltered (gdb_stdlog, "Process record: record_mourn_inferior\n");

unpush_target (&record_ops);
target_mourn_inferior ();
@@ -1345,8 +1559,8 @@ record_xfer_partial (struct target_ops *
record_list_release (record_arch_list_tail);
if (record_debug)
fprintf_unfiltered (gdb_stdlog,
- _("Process record: failed to record "
- "execution log."));
+ "Process record: failed to record "
+ "execution log.");
return -1;
}
if (record_arch_list_add_end ())
@@ -1354,8 +1568,8 @@ record_xfer_partial (struct target_ops *
record_list_release (record_arch_list_tail);
if (record_debug)
fprintf_unfiltered (gdb_stdlog,
- _("Process record: failed to record "
- "execution log."));
+ "Process record: failed to record "
+ "execution log.");
return -1;
}
record_list->next = record_arch_list_head;
@@ -1642,6 +1856,298 @@ cmd_record_start (char *args, int from_t
execute_command ("target record", from_tty);
}

+/* Record log save-file format
+ Version 1 (never released)
+
+ Header:
+ 4 bytes: magic number htonl(0x20090829).
+ NOTE: be sure to change whenever this file format changes!
+
+ Records:
+ record_end:
+ 1 byte: record type (record_end, see enum record_type).
+ record_reg:
+ 1 byte: record type (record_reg, see enum record_type).
+ 8 bytes: register id (network byte order).
+ MAX_REGISTER_SIZE bytes: register value.
+ record_mem:
+ 1 byte: record type (record_mem, see enum record_type).
+ 8 bytes: memory length (network byte order).
+ 8 bytes: memory address (network byte order).
+ n bytes: memory value (n == memory length).
+
+ Version 2
+ 4 bytes: magic number netorder32(0x20091016).
+ NOTE: be sure to change whenever this file format changes!
+
+ Records:
+ record_end:
+ 1 byte: record type (record_end, see enum record_type).
+ 4 bytes: signal
+ 4 bytes: instruction count
+ record_reg:
+ 1 byte: record type (record_reg, see enum record_type).
+ 4 bytes: register id (network byte order).
+ n bytes: register value (n == actual register size).
+ (eg. 4 bytes for x86 general registers).
+ record_mem:
+ 1 byte: record type (record_mem, see enum record_type).
+ 4 bytes: memory length (network byte order).
+ 8 bytes: memory address (network byte order).
+ n bytes: memory value (n == memory length).
+
+*/
+
+/* bfdcore_write -- write bytes into a core file section. */
+
+static inline void
+bfdcore_write (bfd *obfd, asection *osec, void *buf, int len, int *offset)
+{
+ int ret = bfd_set_section_contents (obfd, osec, buf, *offset, len);
+
+ if (ret)
+ *offset += len;
+ else
+ error (_("Failed to write %d bytes to core file %s ('%s').\n"),
+ len, bfd_get_filename (obfd),
+ bfd_errmsg (bfd_get_error ()));
+}
+
+/* Restore the execution log from a file. We use a modified elf
+ corefile format, with an extra section for our data. */
+
+static void
+cmd_record_restore (char *args, int from_tty)
+{
+ core_file_command (args, from_tty);
+ record_open (args, from_tty);
+}
+
+static void
+record_save_cleanups (void *data)
+{
+ bfd *obfd = data;
+ char *pathname = xstrdup (bfd_get_filename (obfd));
+ bfd_close (obfd);
+ unlink (pathname);
+ xfree (pathname);
+}
+
+/* Save the execution log to a file. We use a modified elf corefile
+ format, with an extra section for our data. */
+
+static void
+cmd_record_save (char *args, int from_tty)
+{
+ char *recfilename, recfilename_buffer[40];
+ int recfd;
+ struct record_entry *cur_record_list;
+ uint32_t magic;
+ struct regcache *regcache;
+ struct gdbarch *gdbarch;
+ struct cleanup *old_cleanups;
+ struct cleanup *set_cleanups;
+ bfd *obfd;
+ int save_size = 0;
+ asection *osec = NULL;
+ int bfd_offset = 0;
+
+ if (strcmp (current_target.to_shortname, "record") != 0)
+ error (_("This command can only be used with target 'record'.\n"
+ "Use 'target record' first.\n"));
+
+ if (args && *args)
+ recfilename = args;
+ else
+ {
+ /* Default recfile name is "gdb_record.PID". */
+ snprintf (recfilename_buffer, sizeof (recfilename_buffer),
+ "gdb_record.%d", PIDGET (inferior_ptid));
+ recfilename = recfilename_buffer;
+ }
+
+ /* Open the save file. */
+ if (record_debug)
+ printf_filtered ("Saving execution log to core file '%s'\n", recfilename);
+
+ /* Open the output file. */
+ obfd = create_gcore_bfd (recfilename);
+ old_cleanups = make_cleanup (record_save_cleanups, obfd);
+
+ /* Save the current record entry to "cur_record_list". */
+ cur_record_list = record_list;
+
+ /* Get the values of regcache and gdbarch. */
+ regcache = get_current_regcache ();
+ gdbarch = get_regcache_arch (regcache);
+
+ /* Disable the GDB operation record. */
+ set_cleanups = record_gdb_operation_disable_set ();
+
+ /* Reverse execute to the begin of record list. */
+ while (1)
+ {
+ /* Check for beginning and end of log. */
+ if (record_list == &record_first)
+ break;
+
+ record_exec_insn (regcache, gdbarch, record_list);
+
+ if (record_list->prev)
+ record_list = record_list->prev;
+ }
+
+ /* Compute the size needed for the extra bfd section. */
+ save_size = 4; /* magic cookie */
+ for (record_list = record_first.next; record_list;
+ record_list = record_list->next)
+ switch (record_list->type)
+ {
+ case record_end:
+ save_size += 1 + 4 + 4;
+ break;
+ case record_reg:
+ save_size += 1 + 4 + record_list->u.reg.len;
+ break;
+ case record_mem:
+ save_size += 1 + 4 + 8 + record_list->u.mem.len;
+ break;
+ }
+
+ /* Make the new bfd section. */
+ osec = bfd_make_section_anyway_with_flags (obfd, "precord",
+ SEC_HAS_CONTENTS
+ | SEC_READONLY);
+ if (osec == NULL)
+ error (_("Failed to create 'precord' section for corefile %s: %s"),
+ recfilename,
+ bfd_errmsg (bfd_get_error ()));
+ bfd_set_section_size (obfd, osec, save_size);
+ bfd_set_section_vma (obfd, osec, 0);
+ bfd_set_section_alignment (obfd, osec, 0);
+ bfd_section_lma (obfd, osec) = 0;
+
+ /* Save corefile state. */
+ write_gcore_file (obfd);
+
+ /* Write out the record log. */
+ /* Write the magic code. */
+ magic = RECORD_FILE_MAGIC;
+ if (record_debug)
+ printf_filtered ("\
+ Writing 4-byte magic cookie RECORD_FILE_MAGIC (0x%08x)\n",
+ magic);
+ bfdcore_write (obfd, osec, &magic, sizeof (magic), &bfd_offset);
+
+ /* Save the entries to recfd and forward execute to the end of
+ record list. */
+ record_list = &record_first;
+ while (1)
+ {
+ /* Save entry. */
+ if (record_list != &record_first)
+ {
+ uint8_t type;
+ uint32_t regnum, len, signal, count;
+ uint64_t addr;
+
+ type = record_list->type;
+ bfdcore_write (obfd, osec, &type, sizeof (type), &bfd_offset);
+
+ switch (record_list->type)
+ {
+ case record_reg: /* reg */
+ if (record_debug)
+ printf_filtered ("\
+ Writing register %d (1 plus %d plus %d bytes)\n",
+ record_list->u.reg.num,
+ sizeof (regnum),
+ record_list->u.reg.len);
+
+ /* Write regnum. */
+ regnum = netorder32 (record_list->u.reg.num);
+ bfdcore_write (obfd, osec, &regnum,
+ sizeof (regnum), &bfd_offset);
+
+ /* Write regval. */
+ bfdcore_write (obfd, osec, record_get_loc (record_list),
+ record_list->u.reg.len, &bfd_offset);
+ break;
+
+ case record_mem: /* mem */
+ if (record_debug)
+ printf_filtered ("\
+ Writing memory %s (1 plus %d plus %d plus %d bytes)\n",
+ paddress (gdbarch,
+ record_list->u.mem.addr),
+ sizeof (addr),
+ sizeof (len),
+ record_list->u.mem.len);
+
+ /* Write memlen. */
+ len = netorder32 (record_list->u.mem.len);
+ bfdcore_write (obfd, osec, &len, sizeof (len), &bfd_offset);
+
+ /* Write memaddr. */
+ addr = netorder64 (record_list->u.mem.addr);
+ bfdcore_write (obfd, osec, &addr,
+ sizeof (addr), &bfd_offset);
+
+ /* Write memval. */
+ bfdcore_write (obfd, osec, record_get_loc (record_list),
+ record_list->u.mem.len, &bfd_offset);
+ break;
+
+ case record_end:
+ if (record_debug)
+ printf_filtered ("\
+ Writing record_end (1 + %d + %d bytes)\n",
+ sizeof (signal),
+ sizeof (count));
+ /* Write signal value. */
+ signal = netorder32 (record_list->u.end.sigval);
+ bfdcore_write (obfd, osec, &signal,
+ sizeof (signal), &bfd_offset);
+
+ /* Write insn count. */
+ count = netorder32 (record_list->u.end.insn_num);
+ bfdcore_write (obfd, osec, &count,
+ sizeof (count), &bfd_offset);
+ break;
+ }
+ }
+
+ /* Execute entry. */
+ record_exec_insn (regcache, gdbarch, record_list);
+
+ if (record_list->next)
+ record_list = record_list->next;
+ else
+ break;
+ }
+
+ /* Reverse execute to cur_record_list. */
+ while (1)
+ {
+ /* Check for beginning and end of log. */
+ if (record_list == cur_record_list)
+ break;
+
+ record_exec_insn (regcache, gdbarch, record_list);
+
+ if (record_list->prev)
+ record_list = record_list->prev;
+ }
+
+ do_cleanups (set_cleanups);
+ bfd_close (obfd);
+ discard_cleanups (old_cleanups);
+
+ /* Succeeded. */
+ printf_filtered (_("Saved core file %s with execution log.\n"),
+ recfilename);
+}
+
/* Truncate the record log from the present point
of replay until the end. */

@@ -1749,6 +2255,8 @@ info_record_command (char *args, int fro
void
_initialize_record (void)
{
+ struct cmd_list_element *c;
+
/* Init record_first. */
record_first.prev = NULL;
record_first.next = NULL;
@@ -1767,10 +2275,12 @@ _initialize_record (void)
NULL, show_record_debug, &setdebuglist,
&showdebuglist);

- add_prefix_cmd ("record", class_obscure, cmd_record_start,
+ c = add_prefix_cmd ("record", class_obscure, cmd_record_start,
_("Abbreviated form of \"target record\" command."),
&record_cmdlist, "record ", 0, &cmdlist);
+ set_cmd_completer (c, filename_completer);
add_com_alias ("rec", "record", class_obscure, 1);
+
add_prefix_cmd ("record", class_support, set_record_command,
_("Set record options"), &set_record_cmdlist,
"set record ", 0, &setlist);
@@ -1784,6 +2294,17 @@ _initialize_record (void)
"info record ", 0, &infolist);
add_alias_cmd ("rec", "record", class_obscure, 1, &infolist);

+ c = add_cmd ("save", class_obscure, cmd_record_save,
+ _("Save the execution log to a file.\n\
+Argument is optional filename.\n\
+Default filename is 'gdb_record.<process_id>'."),
+ &record_cmdlist);
+
+ c = add_cmd ("restore", class_obscure, cmd_record_restore,
+ _("Restore the execution log from a file.\n\
+Argument is filename. File must be created with 'record save'."),
+ &record_cmdlist);
+ set_cmd_completer (c, filename_completer);

add_cmd ("delete", class_obscure, cmd_record_delete,
_("Delete the rest of execution log and start recording it anew."),