2725 lines
78 KiB
C
2725 lines
78 KiB
C
/* Process record and replay target for GDB, the GNU debugger.
|
|
|
|
Copyright (C) 2008, 2009 Free Software Foundation, Inc.
|
|
|
|
This file is part of GDB.
|
|
|
|
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 "defs.h"
|
|
#include "gdbcmd.h"
|
|
#include "regcache.h"
|
|
#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 <signal.h>
|
|
|
|
/* This module implements "target record", also known as "process
|
|
record and replay". This target sits on top of a "normal" target
|
|
(a target that "has execution"), and provides a record and replay
|
|
functionality, including reverse debugging.
|
|
|
|
Target record has two modes: recording, and replaying.
|
|
|
|
In record mode, we intercept the to_resume and to_wait methods.
|
|
Whenever gdb resumes the target, we run the target in single step
|
|
mode, and we build up an execution log in which, for each executed
|
|
instruction, we record all changes in memory and register state.
|
|
This is invisible to the user, to whom it just looks like an
|
|
ordinary debugging session (except for performance degredation).
|
|
|
|
In replay mode, instead of actually letting the inferior run as a
|
|
process, we simulate its execution by playing back the recorded
|
|
execution log. For each instruction in the log, we simulate the
|
|
instruction's side effects by duplicating the changes that it would
|
|
have made on memory and registers. */
|
|
|
|
#define DEFAULT_RECORD_INSN_MAX_NUM 200000
|
|
|
|
#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
|
|
("record_reg") or a part of memory ("record_mem"). And each
|
|
instruction must have a struct record_entry ("record_end") that
|
|
indicates that this is the last struct record_entry of this
|
|
instruction.
|
|
|
|
Each struct record_entry is linked to "record_list" by "prev" and
|
|
"next" pointers. */
|
|
|
|
struct record_mem_entry
|
|
{
|
|
CORE_ADDR addr;
|
|
int len;
|
|
/* Set this flag if target memory for this entry
|
|
can no longer be accessed. */
|
|
int mem_entry_not_accessible;
|
|
union
|
|
{
|
|
gdb_byte *ptr;
|
|
gdb_byte buf[sizeof (gdb_byte *)];
|
|
} u;
|
|
};
|
|
|
|
struct record_reg_entry
|
|
{
|
|
unsigned short num;
|
|
unsigned short len;
|
|
union
|
|
{
|
|
gdb_byte *ptr;
|
|
gdb_byte buf[2 * sizeof (gdb_byte *)];
|
|
} u;
|
|
};
|
|
|
|
struct record_end_entry
|
|
{
|
|
enum target_signal sigval;
|
|
ULONGEST insn_num;
|
|
};
|
|
|
|
enum record_type
|
|
{
|
|
record_end = 0,
|
|
record_reg,
|
|
record_mem
|
|
};
|
|
|
|
/* This is the data structure that makes up the execution log.
|
|
|
|
The execution log consists of a single linked list of entries
|
|
of type "struct record_entry". It is doubly linked so that it
|
|
can be traversed in either direction.
|
|
|
|
The start of the list is anchored by a struct called
|
|
"record_first". The pointer "record_list" either points to the
|
|
last entry that was added to the list (in record mode), or to the
|
|
next entry in the list that will be executed (in replay mode).
|
|
|
|
Each list element (struct record_entry), in addition to next and
|
|
prev pointers, consists of a union of three entry types: mem, reg,
|
|
and end. A field called "type" determines which entry type is
|
|
represented by a given list element.
|
|
|
|
Each instruction that is added to the execution log is represented
|
|
by a variable number of list elements ('entries'). The instruction
|
|
will have one "reg" entry for each register that is changed by
|
|
executing the instruction (including the PC in every case). It
|
|
will also have one "mem" entry for each memory change. Finally,
|
|
each instruction will have an "end" entry that separates it from
|
|
the changes associated with the next instruction. */
|
|
|
|
struct record_entry
|
|
{
|
|
struct record_entry *prev;
|
|
struct record_entry *next;
|
|
enum record_type type;
|
|
union
|
|
{
|
|
/* reg */
|
|
struct record_reg_entry reg;
|
|
/* mem */
|
|
struct record_mem_entry mem;
|
|
/* end */
|
|
struct record_end_entry end;
|
|
} u;
|
|
};
|
|
|
|
/* This is the debug switch for process record. */
|
|
int record_debug = 0;
|
|
|
|
struct record_core_buf_entry
|
|
{
|
|
struct record_core_buf_entry *prev;
|
|
struct target_section *p;
|
|
bfd_byte *buf;
|
|
};
|
|
|
|
/* Record buf with core target. */
|
|
static gdb_byte *record_core_regbuf = NULL;
|
|
static struct target_section *record_core_start;
|
|
static struct target_section *record_core_end;
|
|
static struct record_core_buf_entry *record_core_buf_list = NULL;
|
|
|
|
/* The following variables are used for managing the linked list that
|
|
represents the execution log.
|
|
|
|
record_first is the anchor that holds down the beginning of the list.
|
|
|
|
record_list serves two functions:
|
|
1) In record mode, it anchors the end of the list.
|
|
2) In replay mode, it traverses the list and points to
|
|
the next instruction that must be emulated.
|
|
|
|
record_arch_list_head and record_arch_list_tail are used to manage
|
|
a separate list, which is used to build up the change elements of
|
|
the currently executing instruction during record mode. When this
|
|
instruction has been completely annotated in the "arch list", it
|
|
will be appended to the main execution log. */
|
|
|
|
static struct record_entry record_first;
|
|
static struct record_entry *record_list = &record_first;
|
|
static struct record_entry *record_arch_list_head = NULL;
|
|
static struct record_entry *record_arch_list_tail = NULL;
|
|
|
|
/* 1 ask user. 0 auto delete the last struct record_entry. */
|
|
static int record_stop_at_limit = 1;
|
|
/* Maximum allowed number of insns in execution log. */
|
|
static unsigned int record_insn_max_num = DEFAULT_RECORD_INSN_MAX_NUM;
|
|
/* Actual count of insns presently in execution log. */
|
|
static int record_insn_num = 0;
|
|
/* Count of insns logged so far (may be larger
|
|
than count of insns presently in execution log). */
|
|
static ULONGEST record_insn_count;
|
|
|
|
/* The target_ops of process record. */
|
|
static struct target_ops record_ops;
|
|
static struct target_ops record_core_ops;
|
|
|
|
/* The beneath function pointers. */
|
|
static struct target_ops *record_beneath_to_resume_ops;
|
|
static void (*record_beneath_to_resume) (struct target_ops *, ptid_t, int,
|
|
enum target_signal);
|
|
static struct target_ops *record_beneath_to_wait_ops;
|
|
static ptid_t (*record_beneath_to_wait) (struct target_ops *, ptid_t,
|
|
struct target_waitstatus *,
|
|
int);
|
|
static struct target_ops *record_beneath_to_store_registers_ops;
|
|
static void (*record_beneath_to_store_registers) (struct target_ops *,
|
|
struct regcache *,
|
|
int regno);
|
|
static struct target_ops *record_beneath_to_xfer_partial_ops;
|
|
static LONGEST (*record_beneath_to_xfer_partial) (struct target_ops *ops,
|
|
enum target_object object,
|
|
const char *annex,
|
|
gdb_byte *readbuf,
|
|
const gdb_byte *writebuf,
|
|
ULONGEST offset,
|
|
LONGEST len);
|
|
static int (*record_beneath_to_insert_breakpoint) (struct gdbarch *,
|
|
struct bp_target_info *);
|
|
static int (*record_beneath_to_remove_breakpoint) (struct gdbarch *,
|
|
struct bp_target_info *);
|
|
static int (*record_beneath_to_stopped_by_watchpoint) (void);
|
|
static int (*record_beneath_to_stopped_data_address) (struct target_ops *,
|
|
CORE_ADDR *);
|
|
|
|
/* Alloc and free functions for record_reg, record_mem, and record_end
|
|
entries. */
|
|
|
|
/* Alloc a record_reg record entry. */
|
|
|
|
static inline struct record_entry *
|
|
record_reg_alloc (struct regcache *regcache, int regnum)
|
|
{
|
|
struct record_entry *rec;
|
|
struct gdbarch *gdbarch = get_regcache_arch (regcache);
|
|
|
|
rec = (struct record_entry *) xcalloc (1, sizeof (struct record_entry));
|
|
rec->type = record_reg;
|
|
rec->u.reg.num = regnum;
|
|
rec->u.reg.len = register_size (gdbarch, regnum);
|
|
if (rec->u.reg.len > sizeof (rec->u.reg.u.buf))
|
|
rec->u.reg.u.ptr = (gdb_byte *) xmalloc (rec->u.reg.len);
|
|
|
|
return rec;
|
|
}
|
|
|
|
/* Free a record_reg record entry. */
|
|
|
|
static inline void
|
|
record_reg_release (struct record_entry *rec)
|
|
{
|
|
gdb_assert (rec->type == record_reg);
|
|
if (rec->u.reg.len > sizeof (rec->u.reg.u.buf))
|
|
xfree (rec->u.reg.u.ptr);
|
|
xfree (rec);
|
|
}
|
|
|
|
/* Alloc a record_mem record entry. */
|
|
|
|
static inline struct record_entry *
|
|
record_mem_alloc (CORE_ADDR addr, int len)
|
|
{
|
|
struct record_entry *rec;
|
|
|
|
rec = (struct record_entry *) xcalloc (1, sizeof (struct record_entry));
|
|
rec->type = record_mem;
|
|
rec->u.mem.addr = addr;
|
|
rec->u.mem.len = len;
|
|
if (rec->u.mem.len > sizeof (rec->u.mem.u.buf))
|
|
rec->u.mem.u.ptr = (gdb_byte *) xmalloc (len);
|
|
|
|
return rec;
|
|
}
|
|
|
|
/* Free a record_mem record entry. */
|
|
|
|
static inline void
|
|
record_mem_release (struct record_entry *rec)
|
|
{
|
|
gdb_assert (rec->type == record_mem);
|
|
if (rec->u.mem.len > sizeof (rec->u.mem.u.buf))
|
|
xfree (rec->u.mem.u.ptr);
|
|
xfree (rec);
|
|
}
|
|
|
|
/* Alloc a record_end record entry. */
|
|
|
|
static inline struct record_entry *
|
|
record_end_alloc (void)
|
|
{
|
|
struct record_entry *rec;
|
|
|
|
rec = (struct record_entry *) xcalloc (1, sizeof (struct record_entry));
|
|
rec->type = record_end;
|
|
|
|
return rec;
|
|
}
|
|
|
|
/* Free a record_end record entry. */
|
|
|
|
static inline void
|
|
record_end_release (struct record_entry *rec)
|
|
{
|
|
xfree (rec);
|
|
}
|
|
|
|
/* Free one record entry, any type.
|
|
Return entry->type, in case caller wants to know. */
|
|
|
|
static inline enum record_type
|
|
record_entry_release (struct record_entry *rec)
|
|
{
|
|
enum record_type type = rec->type;
|
|
|
|
switch (type) {
|
|
case record_reg:
|
|
record_reg_release (rec);
|
|
break;
|
|
case record_mem:
|
|
record_mem_release (rec);
|
|
break;
|
|
case record_end:
|
|
record_end_release (rec);
|
|
break;
|
|
}
|
|
return type;
|
|
}
|
|
|
|
/* Free all record entries in list pointed to by REC. */
|
|
|
|
static void
|
|
record_list_release (struct record_entry *rec)
|
|
{
|
|
if (!rec)
|
|
return;
|
|
|
|
while (rec->next)
|
|
rec = rec->next;
|
|
|
|
while (rec->prev)
|
|
{
|
|
rec = rec->prev;
|
|
record_entry_release (rec->next);
|
|
}
|
|
|
|
if (rec == &record_first)
|
|
{
|
|
record_insn_num = 0;
|
|
record_first.next = NULL;
|
|
}
|
|
else
|
|
record_entry_release (rec);
|
|
}
|
|
|
|
/* Free all record entries forward of the given list position. */
|
|
|
|
static void
|
|
record_list_release_following (struct record_entry *rec)
|
|
{
|
|
struct record_entry *tmp = rec->next;
|
|
|
|
rec->next = NULL;
|
|
while (tmp)
|
|
{
|
|
rec = tmp->next;
|
|
if (record_entry_release (tmp) == record_end)
|
|
{
|
|
record_insn_num--;
|
|
record_insn_count--;
|
|
}
|
|
tmp = rec;
|
|
}
|
|
}
|
|
|
|
/* Delete the first instruction from the beginning of the log, to make
|
|
room for adding a new instruction at the end of the log.
|
|
|
|
Note -- this function does not modify record_insn_num. */
|
|
|
|
static void
|
|
record_list_release_first (void)
|
|
{
|
|
struct record_entry *tmp;
|
|
|
|
if (!record_first.next)
|
|
return;
|
|
|
|
/* Loop until a record_end. */
|
|
while (1)
|
|
{
|
|
/* Cut record_first.next out of the linked list. */
|
|
tmp = record_first.next;
|
|
record_first.next = tmp->next;
|
|
tmp->next->prev = &record_first;
|
|
|
|
/* tmp is now isolated, and can be deleted. */
|
|
if (record_entry_release (tmp) == record_end)
|
|
break; /* End loop at first record_end. */
|
|
|
|
if (!record_first.next)
|
|
{
|
|
gdb_assert (record_insn_num == 1);
|
|
break; /* End loop when list is empty. */
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Add a struct record_entry to record_arch_list. */
|
|
|
|
static void
|
|
record_arch_list_add (struct record_entry *rec)
|
|
{
|
|
if (record_debug > 1)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: record_arch_list_add %s.\n",
|
|
host_address_to_string (rec));
|
|
|
|
if (record_arch_list_tail)
|
|
{
|
|
record_arch_list_tail->next = rec;
|
|
rec->prev = record_arch_list_tail;
|
|
record_arch_list_tail = rec;
|
|
}
|
|
else
|
|
{
|
|
record_arch_list_head = rec;
|
|
record_arch_list_tail = rec;
|
|
}
|
|
}
|
|
|
|
/* Return the value storage location of a record entry. */
|
|
static inline gdb_byte *
|
|
record_get_loc (struct record_entry *rec)
|
|
{
|
|
switch (rec->type) {
|
|
case record_mem:
|
|
if (rec->u.mem.len > sizeof (rec->u.mem.u.buf))
|
|
return rec->u.mem.u.ptr;
|
|
else
|
|
return rec->u.mem.u.buf;
|
|
case record_reg:
|
|
if (rec->u.reg.len > sizeof (rec->u.reg.u.buf))
|
|
return rec->u.reg.u.ptr;
|
|
else
|
|
return rec->u.reg.u.buf;
|
|
case record_end:
|
|
default:
|
|
gdb_assert (0);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
/* Record the value of a register NUM to record_arch_list. */
|
|
|
|
int
|
|
record_arch_list_add_reg (struct regcache *regcache, int regnum)
|
|
{
|
|
struct record_entry *rec;
|
|
|
|
if (record_debug > 1)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: add register num = %d to "
|
|
"record list.\n",
|
|
regnum);
|
|
|
|
rec = record_reg_alloc (regcache, regnum);
|
|
|
|
regcache_raw_read (regcache, regnum, record_get_loc (rec));
|
|
|
|
record_arch_list_add (rec);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Record the value of a region of memory whose address is ADDR and
|
|
length is LEN to record_arch_list. */
|
|
|
|
int
|
|
record_arch_list_add_mem (CORE_ADDR addr, int len)
|
|
{
|
|
struct record_entry *rec;
|
|
|
|
if (record_debug > 1)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: add mem addr = %s len = %d to "
|
|
"record list.\n",
|
|
paddress (target_gdbarch, addr), len);
|
|
|
|
if (!addr) /* FIXME: Why? Some arch must permit it... */
|
|
return 0;
|
|
|
|
rec = record_mem_alloc (addr, len);
|
|
|
|
if (target_read_memory (addr, record_get_loc (rec), len))
|
|
{
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: error reading memory at "
|
|
"addr = %s len = %d.\n",
|
|
paddress (target_gdbarch, addr), len);
|
|
record_mem_release (rec);
|
|
return -1;
|
|
}
|
|
|
|
record_arch_list_add (rec);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Add a record_end type struct record_entry to record_arch_list. */
|
|
|
|
int
|
|
record_arch_list_add_end (void)
|
|
{
|
|
struct record_entry *rec;
|
|
|
|
if (record_debug > 1)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: add end to arch list.\n");
|
|
|
|
rec = record_end_alloc ();
|
|
rec->u.end.sigval = TARGET_SIGNAL_0;
|
|
rec->u.end.insn_num = ++record_insn_count;
|
|
|
|
record_arch_list_add (rec);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
record_check_insn_num (int set_terminal)
|
|
{
|
|
if (record_insn_max_num)
|
|
{
|
|
gdb_assert (record_insn_num <= record_insn_max_num);
|
|
if (record_insn_num == record_insn_max_num)
|
|
{
|
|
/* Ask user what to do. */
|
|
if (record_stop_at_limit)
|
|
{
|
|
int q;
|
|
if (set_terminal)
|
|
target_terminal_ours ();
|
|
q = yquery (_("Do you want to auto delete previous execution "
|
|
"log entries when record/replay buffer becomes "
|
|
"full (record stop-at-limit)?"));
|
|
if (set_terminal)
|
|
target_terminal_inferior ();
|
|
if (q)
|
|
record_stop_at_limit = 0;
|
|
else
|
|
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 int
|
|
record_message (struct regcache *regcache, enum target_signal signal)
|
|
{
|
|
int ret;
|
|
struct gdbarch *gdbarch = get_regcache_arch (regcache);
|
|
struct cleanup *old_cleanups = make_cleanup (record_arch_list_cleanups, 0);
|
|
|
|
record_arch_list_head = NULL;
|
|
record_arch_list_tail = NULL;
|
|
|
|
/* Check record_insn_num. */
|
|
record_check_insn_num (1);
|
|
|
|
/* If gdb sends a signal value to target_resume,
|
|
save it in the 'end' field of the previous instruction.
|
|
|
|
Maybe process record should record what really happened,
|
|
rather than what gdb pretends has happened.
|
|
|
|
So if Linux delivered the signal to the child process during
|
|
the record mode, we will record it and deliver it again in
|
|
the replay mode.
|
|
|
|
If user says "ignore this signal" during the record mode, then
|
|
it will be ignored again during the replay mode (no matter if
|
|
the user says something different, like "deliver this signal"
|
|
during the replay mode).
|
|
|
|
User should understand that nothing he does during the replay
|
|
mode will change the behavior of the child. If he tries,
|
|
then that is a user error.
|
|
|
|
But we should still deliver the signal to gdb during the replay,
|
|
if we delivered it during the recording. Therefore we should
|
|
record the signal during record_wait, not record_resume. */
|
|
if (record_list != &record_first) /* FIXME better way to check */
|
|
{
|
|
gdb_assert (record_list->type == record_end);
|
|
record_list->u.end.sigval = signal;
|
|
}
|
|
|
|
if (signal == TARGET_SIGNAL_0
|
|
|| !gdbarch_process_record_signal_p (gdbarch))
|
|
ret = gdbarch_process_record (gdbarch,
|
|
regcache,
|
|
regcache_read_pc (regcache));
|
|
else
|
|
ret = gdbarch_process_record_signal (gdbarch,
|
|
regcache,
|
|
signal);
|
|
|
|
if (ret > 0)
|
|
error (_("Process record: inferior program stopped."));
|
|
if (ret < 0)
|
|
error (_("Process record: failed to record execution log."));
|
|
|
|
discard_cleanups (old_cleanups);
|
|
|
|
record_list->next = record_arch_list_head;
|
|
record_arch_list_head->prev = record_list;
|
|
record_list = record_arch_list_tail;
|
|
|
|
if (record_insn_num == record_insn_max_num && record_insn_max_num)
|
|
record_list_release_first ();
|
|
else
|
|
record_insn_num++;
|
|
|
|
return 1;
|
|
}
|
|
|
|
struct record_message_args {
|
|
struct regcache *regcache;
|
|
enum target_signal signal;
|
|
};
|
|
|
|
static int
|
|
record_message_wrapper (void *args)
|
|
{
|
|
struct record_message_args *record_args = args;
|
|
|
|
return record_message (record_args->regcache, record_args->signal);
|
|
}
|
|
|
|
static int
|
|
record_message_wrapper_safe (struct regcache *regcache,
|
|
enum target_signal signal)
|
|
{
|
|
struct record_message_args args;
|
|
|
|
args.regcache = regcache;
|
|
args.signal = signal;
|
|
|
|
return catch_errors (record_message_wrapper, &args, NULL, RETURN_MASK_ALL);
|
|
}
|
|
|
|
/* Set to 1 if record_store_registers and record_xfer_partial
|
|
doesn't need record. */
|
|
|
|
static int record_gdb_operation_disable = 0;
|
|
|
|
struct cleanup *
|
|
record_gdb_operation_disable_set (void)
|
|
{
|
|
struct cleanup *old_cleanups = NULL;
|
|
|
|
old_cleanups =
|
|
make_cleanup_restore_integer (&record_gdb_operation_disable);
|
|
record_gdb_operation_disable = 1;
|
|
|
|
return old_cleanups;
|
|
}
|
|
|
|
/* Flag set to TRUE for target_stopped_by_watchpoint. */
|
|
static int record_hw_watchpoint = 0;
|
|
|
|
/* Execute one instruction from the record log. Each instruction in
|
|
the log will be represented by an arbitrary sequence of register
|
|
entries and memory entries, followed by an 'end' entry. */
|
|
|
|
static inline void
|
|
record_exec_insn (struct regcache *regcache, struct gdbarch *gdbarch,
|
|
struct record_entry *entry)
|
|
{
|
|
switch (entry->type)
|
|
{
|
|
case record_reg: /* reg */
|
|
{
|
|
gdb_byte reg[MAX_REGISTER_SIZE];
|
|
|
|
if (record_debug > 1)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: record_reg %s to "
|
|
"inferior num = %d.\n",
|
|
host_address_to_string (entry),
|
|
entry->u.reg.num);
|
|
|
|
regcache_cooked_read (regcache, entry->u.reg.num, reg);
|
|
regcache_cooked_write (regcache, entry->u.reg.num,
|
|
record_get_loc (entry));
|
|
memcpy (record_get_loc (entry), reg, entry->u.reg.len);
|
|
}
|
|
break;
|
|
|
|
case record_mem: /* mem */
|
|
{
|
|
/* Nothing to do if the entry is flagged not_accessible. */
|
|
if (!entry->u.mem.mem_entry_not_accessible)
|
|
{
|
|
gdb_byte *mem = alloca (entry->u.mem.len);
|
|
|
|
if (record_debug > 1)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: record_mem %s to "
|
|
"inferior addr = %s len = %d.\n",
|
|
host_address_to_string (entry),
|
|
paddress (gdbarch, entry->u.mem.addr),
|
|
entry->u.mem.len);
|
|
|
|
if (target_read_memory (entry->u.mem.addr, mem, entry->u.mem.len))
|
|
{
|
|
entry->u.mem.mem_entry_not_accessible = 1;
|
|
if (record_debug)
|
|
warning ("Process record: error reading memory at "
|
|
"addr = %s len = %d.",
|
|
paddress (gdbarch, entry->u.mem.addr),
|
|
entry->u.mem.len);
|
|
}
|
|
else
|
|
{
|
|
if (target_write_memory (entry->u.mem.addr,
|
|
record_get_loc (entry),
|
|
entry->u.mem.len))
|
|
{
|
|
entry->u.mem.mem_entry_not_accessible = 1;
|
|
if (record_debug)
|
|
warning ("Process record: error writing memory at "
|
|
"addr = %s len = %d.",
|
|
paddress (gdbarch, entry->u.mem.addr),
|
|
entry->u.mem.len);
|
|
}
|
|
else
|
|
{
|
|
memcpy (record_get_loc (entry), mem, entry->u.mem.len);
|
|
|
|
/* We've changed memory --- check if a hardware
|
|
watchpoint should trap. Note that this
|
|
presently assumes the target beneath supports
|
|
continuable watchpoints. On non-continuable
|
|
watchpoints target, we'll want to check this
|
|
_before_ actually doing the memory change, and
|
|
not doing the change at all if the watchpoint
|
|
traps. */
|
|
if (hardware_watchpoint_inserted_in_range
|
|
(get_regcache_aspace (regcache),
|
|
entry->u.mem.addr, entry->u.mem.len))
|
|
record_hw_watchpoint = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static struct target_ops *tmp_to_resume_ops;
|
|
static void (*tmp_to_resume) (struct target_ops *, ptid_t, int,
|
|
enum target_signal);
|
|
static struct target_ops *tmp_to_wait_ops;
|
|
static ptid_t (*tmp_to_wait) (struct target_ops *, ptid_t,
|
|
struct target_waitstatus *,
|
|
int);
|
|
static struct target_ops *tmp_to_store_registers_ops;
|
|
static void (*tmp_to_store_registers) (struct target_ops *,
|
|
struct regcache *,
|
|
int regno);
|
|
static struct target_ops *tmp_to_xfer_partial_ops;
|
|
static LONGEST (*tmp_to_xfer_partial) (struct target_ops *ops,
|
|
enum target_object object,
|
|
const char *annex,
|
|
gdb_byte *readbuf,
|
|
const gdb_byte *writebuf,
|
|
ULONGEST offset,
|
|
LONGEST len);
|
|
static int (*tmp_to_insert_breakpoint) (struct gdbarch *,
|
|
struct bp_target_info *);
|
|
static int (*tmp_to_remove_breakpoint) (struct gdbarch *,
|
|
struct bp_target_info *);
|
|
static int (*tmp_to_stopped_by_watchpoint) (void);
|
|
static int (*tmp_to_stopped_data_address) (struct target_ops *, CORE_ADDR *);
|
|
|
|
static void record_restore (void);
|
|
|
|
/* Open the process record target. */
|
|
|
|
static void
|
|
record_core_open_1 (char *name, int from_tty)
|
|
{
|
|
struct regcache *regcache = get_current_regcache ();
|
|
int regnum = gdbarch_num_regs (get_regcache_arch (regcache));
|
|
int i;
|
|
|
|
/* Get record_core_regbuf. */
|
|
target_fetch_registers (regcache, -1);
|
|
record_core_regbuf = xmalloc (MAX_REGISTER_SIZE * regnum);
|
|
for (i = 0; i < regnum; i ++)
|
|
regcache_raw_collect (regcache, i,
|
|
record_core_regbuf + MAX_REGISTER_SIZE * i);
|
|
|
|
/* Get record_core_start and record_core_end. */
|
|
if (build_section_table (core_bfd, &record_core_start, &record_core_end))
|
|
{
|
|
xfree (record_core_regbuf);
|
|
record_core_regbuf = NULL;
|
|
error (_("\"%s\": Can't find sections: %s"),
|
|
bfd_get_filename (core_bfd), bfd_errmsg (bfd_get_error ()));
|
|
}
|
|
|
|
push_target (&record_core_ops);
|
|
record_restore ();
|
|
}
|
|
|
|
/* "to_open" target method for 'live' processes. */
|
|
|
|
static void
|
|
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."));
|
|
if (non_stop)
|
|
error (_("Process record target can't debug inferior in non-stop mode "
|
|
"(non-stop)."));
|
|
if (target_async_permitted)
|
|
error (_("Process record target can't debug inferior in asynchronous "
|
|
"mode (target-async)."));
|
|
|
|
if (!gdbarch_process_record_p (target_gdbarch))
|
|
error (_("Process record: the current architecture doesn't support "
|
|
"record function."));
|
|
|
|
if (!tmp_to_resume)
|
|
error (_("Could not find 'to_resume' method on the target stack."));
|
|
if (!tmp_to_wait)
|
|
error (_("Could not find 'to_wait' method on the target stack."));
|
|
if (!tmp_to_store_registers)
|
|
error (_("Could not find 'to_store_registers' method on the target stack."));
|
|
if (!tmp_to_insert_breakpoint)
|
|
error (_("Could not find 'to_insert_breakpoint' method on the target stack."));
|
|
if (!tmp_to_remove_breakpoint)
|
|
error (_("Could not find 'to_remove_breakpoint' method on the target stack."));
|
|
|
|
push_target (&record_ops);
|
|
}
|
|
|
|
/* "to_open" target method. Open the process record target. */
|
|
|
|
static void
|
|
record_open (char *name, int from_tty)
|
|
{
|
|
struct target_ops *t;
|
|
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog, "Process record: record_open\n");
|
|
|
|
/* Check if record target is already running. */
|
|
if (current_target.to_stratum == record_stratum)
|
|
error (_("Process record target already running. Use \"record stop\" to "
|
|
"stop record target first."));
|
|
|
|
/* Reset the tmp beneath pointers. */
|
|
tmp_to_resume_ops = NULL;
|
|
tmp_to_resume = NULL;
|
|
tmp_to_wait_ops = NULL;
|
|
tmp_to_wait = NULL;
|
|
tmp_to_store_registers_ops = NULL;
|
|
tmp_to_store_registers = NULL;
|
|
tmp_to_xfer_partial_ops = NULL;
|
|
tmp_to_xfer_partial = NULL;
|
|
tmp_to_insert_breakpoint = NULL;
|
|
tmp_to_remove_breakpoint = NULL;
|
|
|
|
/* Set the beneath function pointers. */
|
|
for (t = current_target.beneath; t != NULL; t = t->beneath)
|
|
{
|
|
if (!tmp_to_resume)
|
|
{
|
|
tmp_to_resume = t->to_resume;
|
|
tmp_to_resume_ops = t;
|
|
}
|
|
if (!tmp_to_wait)
|
|
{
|
|
tmp_to_wait = t->to_wait;
|
|
tmp_to_wait_ops = t;
|
|
}
|
|
if (!tmp_to_store_registers)
|
|
{
|
|
tmp_to_store_registers = t->to_store_registers;
|
|
tmp_to_store_registers_ops = t;
|
|
}
|
|
if (!tmp_to_xfer_partial)
|
|
{
|
|
tmp_to_xfer_partial = t->to_xfer_partial;
|
|
tmp_to_xfer_partial_ops = t;
|
|
}
|
|
if (!tmp_to_insert_breakpoint)
|
|
tmp_to_insert_breakpoint = t->to_insert_breakpoint;
|
|
if (!tmp_to_remove_breakpoint)
|
|
tmp_to_remove_breakpoint = t->to_remove_breakpoint;
|
|
if (!tmp_to_stopped_by_watchpoint)
|
|
tmp_to_stopped_by_watchpoint = t->to_stopped_by_watchpoint;
|
|
if (!tmp_to_stopped_data_address)
|
|
tmp_to_stopped_data_address = t->to_stopped_data_address;
|
|
}
|
|
if (!tmp_to_xfer_partial)
|
|
error (_("Could not find 'to_xfer_partial' method on the target stack."));
|
|
|
|
/* 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;
|
|
record_beneath_to_wait_ops = tmp_to_wait_ops;
|
|
record_beneath_to_wait = tmp_to_wait;
|
|
record_beneath_to_store_registers_ops = tmp_to_store_registers_ops;
|
|
record_beneath_to_store_registers = tmp_to_store_registers;
|
|
record_beneath_to_xfer_partial_ops = tmp_to_xfer_partial_ops;
|
|
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;
|
|
record_beneath_to_stopped_by_watchpoint = tmp_to_stopped_by_watchpoint;
|
|
record_beneath_to_stopped_data_address = tmp_to_stopped_data_address;
|
|
|
|
if (current_target.to_stratum == core_stratum)
|
|
record_core_open_1 (name, from_tty);
|
|
else
|
|
record_open_1 (name, from_tty);
|
|
}
|
|
|
|
/* "to_close" target method. Close the process record target. */
|
|
|
|
static void
|
|
record_close (int quitting)
|
|
{
|
|
struct record_core_buf_entry *entry;
|
|
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog, "Process record: record_close\n");
|
|
|
|
record_list_release (record_list);
|
|
|
|
/* Release record_core_regbuf. */
|
|
if (record_core_regbuf)
|
|
{
|
|
xfree (record_core_regbuf);
|
|
record_core_regbuf = NULL;
|
|
}
|
|
|
|
/* Release record_core_buf_list. */
|
|
if (record_core_buf_list)
|
|
{
|
|
for (entry = record_core_buf_list->prev; entry; entry = entry->prev)
|
|
{
|
|
xfree (record_core_buf_list);
|
|
record_core_buf_list = entry;
|
|
}
|
|
record_core_buf_list = NULL;
|
|
}
|
|
}
|
|
|
|
static int record_resume_step = 0;
|
|
|
|
/* "to_resume" target method. Resume the process record target. */
|
|
|
|
static void
|
|
record_resume (struct target_ops *ops, ptid_t ptid, int step,
|
|
enum target_signal signal)
|
|
{
|
|
record_resume_step = step;
|
|
|
|
if (!RECORD_IS_REPLAY)
|
|
{
|
|
record_message (get_current_regcache (), signal);
|
|
record_beneath_to_resume (record_beneath_to_resume_ops, ptid, 1,
|
|
signal);
|
|
}
|
|
}
|
|
|
|
static int record_get_sig = 0;
|
|
|
|
/* SIGINT signal handler, registered by "to_wait" method. */
|
|
|
|
static void
|
|
record_sig_handler (int signo)
|
|
{
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog, "Process record: get a signal\n");
|
|
|
|
/* It will break the running inferior in replay mode. */
|
|
record_resume_step = 1;
|
|
|
|
/* It will let record_wait set inferior status to get the signal
|
|
SIGINT. */
|
|
record_get_sig = 1;
|
|
}
|
|
|
|
static void
|
|
record_wait_cleanups (void *ignore)
|
|
{
|
|
if (execution_direction == EXEC_REVERSE)
|
|
{
|
|
if (record_list->next)
|
|
record_list = record_list->next;
|
|
}
|
|
else
|
|
record_list = record_list->prev;
|
|
}
|
|
|
|
/* "to_wait" target method for process record target.
|
|
|
|
In record mode, the target is always run in singlestep mode
|
|
(even when gdb says to continue). The to_wait method intercepts
|
|
the stop events and determines which ones are to be passed on to
|
|
gdb. Most stop events are just singlestep events that gdb is not
|
|
to know about, so the to_wait method just records them and keeps
|
|
singlestepping.
|
|
|
|
In replay mode, this function emulates the recorded execution log,
|
|
one instruction at a time (forward or backward), and determines
|
|
where to stop. */
|
|
|
|
static ptid_t
|
|
record_wait (struct target_ops *ops,
|
|
ptid_t ptid, struct target_waitstatus *status,
|
|
int options)
|
|
{
|
|
struct cleanup *set_cleanups = record_gdb_operation_disable_set ();
|
|
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: record_wait "
|
|
"record_resume_step = %d\n",
|
|
record_resume_step);
|
|
|
|
if (!RECORD_IS_REPLAY && ops != &record_core_ops)
|
|
{
|
|
if (record_resume_step)
|
|
{
|
|
/* This is a single step. */
|
|
return record_beneath_to_wait (record_beneath_to_wait_ops,
|
|
ptid, status, options);
|
|
}
|
|
else
|
|
{
|
|
/* This is not a single step. */
|
|
ptid_t ret;
|
|
CORE_ADDR tmp_pc;
|
|
|
|
while (1)
|
|
{
|
|
ret = record_beneath_to_wait (record_beneath_to_wait_ops,
|
|
ptid, status, options);
|
|
|
|
/* Is this a SIGTRAP? */
|
|
if (status->kind == TARGET_WAITKIND_STOPPED
|
|
&& status->value.sig == TARGET_SIGNAL_TRAP)
|
|
{
|
|
struct regcache *regcache;
|
|
struct address_space *aspace;
|
|
|
|
/* Yes -- this is likely our single-step finishing,
|
|
but check if there's any reason the core would be
|
|
interested in the event. */
|
|
|
|
registers_changed ();
|
|
regcache = get_current_regcache ();
|
|
tmp_pc = regcache_read_pc (regcache);
|
|
aspace = get_regcache_aspace (regcache);
|
|
|
|
if (target_stopped_by_watchpoint ())
|
|
{
|
|
/* Always interested in watchpoints. */
|
|
}
|
|
else if (breakpoint_inserted_here_p (aspace, tmp_pc))
|
|
{
|
|
/* There is a breakpoint here. Let the core
|
|
handle it. */
|
|
if (software_breakpoint_inserted_here_p (aspace, tmp_pc))
|
|
{
|
|
struct gdbarch *gdbarch = get_regcache_arch (regcache);
|
|
CORE_ADDR decr_pc_after_break
|
|
= gdbarch_decr_pc_after_break (gdbarch);
|
|
if (decr_pc_after_break)
|
|
regcache_write_pc (regcache,
|
|
tmp_pc + decr_pc_after_break);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* This must be a single-step trap. Record the
|
|
insn and issue another step. */
|
|
if (!record_message_wrapper_safe (regcache,
|
|
TARGET_SIGNAL_0))
|
|
{
|
|
status->kind = TARGET_WAITKIND_STOPPED;
|
|
status->value.sig = TARGET_SIGNAL_0;
|
|
break;
|
|
}
|
|
|
|
record_beneath_to_resume (record_beneath_to_resume_ops,
|
|
ptid, 1,
|
|
TARGET_SIGNAL_0);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/* The inferior is broken by a breakpoint or a signal. */
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
struct regcache *regcache = get_current_regcache ();
|
|
struct gdbarch *gdbarch = get_regcache_arch (regcache);
|
|
struct address_space *aspace = get_regcache_aspace (regcache);
|
|
int continue_flag = 1;
|
|
int first_record_end = 1;
|
|
struct cleanup *old_cleanups = make_cleanup (record_wait_cleanups, 0);
|
|
CORE_ADDR tmp_pc;
|
|
|
|
record_hw_watchpoint = 0;
|
|
status->kind = TARGET_WAITKIND_STOPPED;
|
|
|
|
/* Check breakpoint when forward execute. */
|
|
if (execution_direction == EXEC_FORWARD)
|
|
{
|
|
tmp_pc = regcache_read_pc (regcache);
|
|
if (breakpoint_inserted_here_p (aspace, tmp_pc))
|
|
{
|
|
int decr_pc_after_break = gdbarch_decr_pc_after_break (gdbarch);
|
|
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: break at %s.\n",
|
|
paddress (gdbarch, tmp_pc));
|
|
|
|
if (decr_pc_after_break
|
|
&& !record_resume_step
|
|
&& software_breakpoint_inserted_here_p (aspace, tmp_pc))
|
|
regcache_write_pc (regcache,
|
|
tmp_pc + decr_pc_after_break);
|
|
goto replay_out;
|
|
}
|
|
}
|
|
|
|
record_get_sig = 0;
|
|
signal (SIGINT, record_sig_handler);
|
|
/* If GDB is in terminal_inferior mode, it will not get the signal.
|
|
And in GDB replay mode, GDB doesn't need to be in terminal_inferior
|
|
mode, because inferior will not executed.
|
|
Then set it to terminal_ours to make GDB get the signal. */
|
|
target_terminal_ours ();
|
|
|
|
/* In EXEC_FORWARD mode, record_list points to the tail of prev
|
|
instruction. */
|
|
if (execution_direction == EXEC_FORWARD && record_list->next)
|
|
record_list = record_list->next;
|
|
|
|
/* Loop over the record_list, looking for the next place to
|
|
stop. */
|
|
do
|
|
{
|
|
/* Check for beginning and end of log. */
|
|
if (execution_direction == EXEC_REVERSE
|
|
&& record_list == &record_first)
|
|
{
|
|
/* Hit beginning of record log in reverse. */
|
|
status->kind = TARGET_WAITKIND_NO_HISTORY;
|
|
break;
|
|
}
|
|
if (execution_direction != EXEC_REVERSE && !record_list->next)
|
|
{
|
|
/* Hit end of record log going forward. */
|
|
status->kind = TARGET_WAITKIND_NO_HISTORY;
|
|
break;
|
|
}
|
|
|
|
record_exec_insn (regcache, gdbarch, record_list);
|
|
|
|
if (record_list->type == record_end)
|
|
{
|
|
if (record_debug > 1)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: record_end %s to "
|
|
"inferior.\n",
|
|
host_address_to_string (record_list));
|
|
|
|
if (first_record_end && execution_direction == EXEC_REVERSE)
|
|
{
|
|
/* When reverse excute, the first record_end is the part of
|
|
current instruction. */
|
|
first_record_end = 0;
|
|
}
|
|
else
|
|
{
|
|
/* In EXEC_REVERSE mode, this is the record_end of prev
|
|
instruction.
|
|
In EXEC_FORWARD mode, this is the record_end of current
|
|
instruction. */
|
|
/* step */
|
|
if (record_resume_step)
|
|
{
|
|
if (record_debug > 1)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: step.\n");
|
|
continue_flag = 0;
|
|
}
|
|
|
|
/* check breakpoint */
|
|
tmp_pc = regcache_read_pc (regcache);
|
|
if (breakpoint_inserted_here_p (aspace, tmp_pc))
|
|
{
|
|
int decr_pc_after_break
|
|
= gdbarch_decr_pc_after_break (gdbarch);
|
|
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: break "
|
|
"at %s.\n",
|
|
paddress (gdbarch, tmp_pc));
|
|
if (decr_pc_after_break
|
|
&& execution_direction == EXEC_FORWARD
|
|
&& !record_resume_step
|
|
&& software_breakpoint_inserted_here_p (aspace,
|
|
tmp_pc))
|
|
regcache_write_pc (regcache,
|
|
tmp_pc + decr_pc_after_break);
|
|
continue_flag = 0;
|
|
}
|
|
|
|
if (record_hw_watchpoint)
|
|
{
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog, "\
|
|
Process record: hit hw watchpoint.\n");
|
|
continue_flag = 0;
|
|
}
|
|
/* Check target signal */
|
|
if (record_list->u.end.sigval != TARGET_SIGNAL_0)
|
|
/* FIXME: better way to check */
|
|
continue_flag = 0;
|
|
}
|
|
}
|
|
|
|
if (continue_flag)
|
|
{
|
|
if (execution_direction == EXEC_REVERSE)
|
|
{
|
|
if (record_list->prev)
|
|
record_list = record_list->prev;
|
|
}
|
|
else
|
|
{
|
|
if (record_list->next)
|
|
record_list = record_list->next;
|
|
}
|
|
}
|
|
}
|
|
while (continue_flag);
|
|
|
|
signal (SIGINT, handle_sigint);
|
|
|
|
replay_out:
|
|
if (record_get_sig)
|
|
status->value.sig = TARGET_SIGNAL_INT;
|
|
else if (record_list->u.end.sigval != TARGET_SIGNAL_0)
|
|
/* FIXME: better way to check */
|
|
status->value.sig = record_list->u.end.sigval;
|
|
else
|
|
status->value.sig = TARGET_SIGNAL_TRAP;
|
|
|
|
discard_cleanups (old_cleanups);
|
|
}
|
|
|
|
do_cleanups (set_cleanups);
|
|
return inferior_ptid;
|
|
}
|
|
|
|
static int
|
|
record_stopped_by_watchpoint (void)
|
|
{
|
|
if (RECORD_IS_REPLAY)
|
|
return record_hw_watchpoint;
|
|
else
|
|
return record_beneath_to_stopped_by_watchpoint ();
|
|
}
|
|
|
|
static int
|
|
record_stopped_data_address (struct target_ops *ops, CORE_ADDR *addr_p)
|
|
{
|
|
if (RECORD_IS_REPLAY)
|
|
return 0;
|
|
else
|
|
return record_beneath_to_stopped_data_address (ops, addr_p);
|
|
}
|
|
|
|
/* "to_disconnect" method for process record target. */
|
|
|
|
static void
|
|
record_disconnect (struct target_ops *target, char *args, int from_tty)
|
|
{
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog, "Process record: record_disconnect\n");
|
|
|
|
unpush_target (&record_ops);
|
|
target_disconnect (args, from_tty);
|
|
}
|
|
|
|
/* "to_detach" method for process record target. */
|
|
|
|
static void
|
|
record_detach (struct target_ops *ops, char *args, int from_tty)
|
|
{
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog, "Process record: record_detach\n");
|
|
|
|
unpush_target (&record_ops);
|
|
target_detach (args, from_tty);
|
|
}
|
|
|
|
/* "to_mourn_inferior" method for process record target. */
|
|
|
|
static void
|
|
record_mourn_inferior (struct target_ops *ops)
|
|
{
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog, "Process record: "
|
|
"record_mourn_inferior\n");
|
|
|
|
unpush_target (&record_ops);
|
|
target_mourn_inferior ();
|
|
}
|
|
|
|
/* Close process record target before killing the inferior process. */
|
|
|
|
static void
|
|
record_kill (struct target_ops *ops)
|
|
{
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog, "Process record: record_kill\n");
|
|
|
|
unpush_target (&record_ops);
|
|
target_kill ();
|
|
}
|
|
|
|
/* Record registers change (by user or by GDB) to list as an instruction. */
|
|
|
|
static void
|
|
record_registers_change (struct regcache *regcache, int regnum)
|
|
{
|
|
/* Check record_insn_num. */
|
|
record_check_insn_num (0);
|
|
|
|
record_arch_list_head = NULL;
|
|
record_arch_list_tail = NULL;
|
|
|
|
if (regnum < 0)
|
|
{
|
|
int i;
|
|
for (i = 0; i < gdbarch_num_regs (get_regcache_arch (regcache)); i++)
|
|
{
|
|
if (record_arch_list_add_reg (regcache, i))
|
|
{
|
|
record_list_release (record_arch_list_tail);
|
|
error (_("Process record: failed to record execution log."));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (record_arch_list_add_reg (regcache, regnum))
|
|
{
|
|
record_list_release (record_arch_list_tail);
|
|
error (_("Process record: failed to record execution log."));
|
|
}
|
|
}
|
|
if (record_arch_list_add_end ())
|
|
{
|
|
record_list_release (record_arch_list_tail);
|
|
error (_("Process record: failed to record execution log."));
|
|
}
|
|
record_list->next = record_arch_list_head;
|
|
record_arch_list_head->prev = record_list;
|
|
record_list = record_arch_list_tail;
|
|
|
|
if (record_insn_num == record_insn_max_num && record_insn_max_num)
|
|
record_list_release_first ();
|
|
else
|
|
record_insn_num++;
|
|
}
|
|
|
|
/* "to_store_registers" method for process record target. */
|
|
|
|
static void
|
|
record_store_registers (struct target_ops *ops, struct regcache *regcache,
|
|
int regno)
|
|
{
|
|
if (!record_gdb_operation_disable)
|
|
{
|
|
if (RECORD_IS_REPLAY)
|
|
{
|
|
int n;
|
|
|
|
/* Let user choose if he wants to write register or not. */
|
|
if (regno < 0)
|
|
n =
|
|
query (_("Because GDB is in replay mode, changing the "
|
|
"value of a register will make the execution "
|
|
"log unusable from this point onward. "
|
|
"Change all registers?"));
|
|
else
|
|
n =
|
|
query (_("Because GDB is in replay mode, changing the value "
|
|
"of a register will make the execution log unusable "
|
|
"from this point onward. Change register %s?"),
|
|
gdbarch_register_name (get_regcache_arch (regcache),
|
|
regno));
|
|
|
|
if (!n)
|
|
{
|
|
/* Invalidate the value of regcache that was set in function
|
|
"regcache_raw_write". */
|
|
if (regno < 0)
|
|
{
|
|
int i;
|
|
for (i = 0;
|
|
i < gdbarch_num_regs (get_regcache_arch (regcache));
|
|
i++)
|
|
regcache_invalidate (regcache, i);
|
|
}
|
|
else
|
|
regcache_invalidate (regcache, regno);
|
|
|
|
error (_("Process record canceled the operation."));
|
|
}
|
|
|
|
/* Destroy the record from here forward. */
|
|
record_list_release_following (record_list);
|
|
}
|
|
|
|
record_registers_change (regcache, regno);
|
|
}
|
|
record_beneath_to_store_registers (record_beneath_to_store_registers_ops,
|
|
regcache, regno);
|
|
}
|
|
|
|
/* "to_xfer_partial" method. Behavior is conditional on RECORD_IS_REPLAY.
|
|
In replay mode, we cannot write memory unles we are willing to
|
|
invalidate the record/replay log from this point forward. */
|
|
|
|
static LONGEST
|
|
record_xfer_partial (struct target_ops *ops, enum target_object object,
|
|
const char *annex, gdb_byte *readbuf,
|
|
const gdb_byte *writebuf, ULONGEST offset, LONGEST len)
|
|
{
|
|
if (!record_gdb_operation_disable
|
|
&& (object == TARGET_OBJECT_MEMORY
|
|
|| object == TARGET_OBJECT_RAW_MEMORY) && writebuf)
|
|
{
|
|
if (RECORD_IS_REPLAY)
|
|
{
|
|
/* Let user choose if he wants to write memory or not. */
|
|
if (!query (_("Because GDB is in replay mode, writing to memory "
|
|
"will make the execution log unusable from this "
|
|
"point onward. Write memory at address %s?"),
|
|
paddress (target_gdbarch, offset)))
|
|
error (_("Process record canceled the operation."));
|
|
|
|
/* Destroy the record from here forward. */
|
|
record_list_release_following (record_list);
|
|
}
|
|
|
|
/* Check record_insn_num */
|
|
record_check_insn_num (0);
|
|
|
|
/* Record registers change to list as an instruction. */
|
|
record_arch_list_head = NULL;
|
|
record_arch_list_tail = NULL;
|
|
if (record_arch_list_add_mem (offset, len))
|
|
{
|
|
record_list_release (record_arch_list_tail);
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: failed to record "
|
|
"execution log.");
|
|
return -1;
|
|
}
|
|
if (record_arch_list_add_end ())
|
|
{
|
|
record_list_release (record_arch_list_tail);
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"Process record: failed to record "
|
|
"execution log.");
|
|
return -1;
|
|
}
|
|
record_list->next = record_arch_list_head;
|
|
record_arch_list_head->prev = record_list;
|
|
record_list = record_arch_list_tail;
|
|
|
|
if (record_insn_num == record_insn_max_num && record_insn_max_num)
|
|
record_list_release_first ();
|
|
else
|
|
record_insn_num++;
|
|
}
|
|
|
|
return record_beneath_to_xfer_partial (record_beneath_to_xfer_partial_ops,
|
|
object, annex, readbuf, writebuf,
|
|
offset, len);
|
|
}
|
|
|
|
/* Behavior is conditional on RECORD_IS_REPLAY.
|
|
We will not actually insert or remove breakpoints when replaying,
|
|
nor when recording. */
|
|
|
|
static int
|
|
record_insert_breakpoint (struct gdbarch *gdbarch,
|
|
struct bp_target_info *bp_tgt)
|
|
{
|
|
if (!RECORD_IS_REPLAY)
|
|
{
|
|
struct cleanup *old_cleanups = record_gdb_operation_disable_set ();
|
|
int ret = record_beneath_to_insert_breakpoint (gdbarch, bp_tgt);
|
|
|
|
do_cleanups (old_cleanups);
|
|
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* "to_remove_breakpoint" method for process record target. */
|
|
|
|
static int
|
|
record_remove_breakpoint (struct gdbarch *gdbarch,
|
|
struct bp_target_info *bp_tgt)
|
|
{
|
|
if (!RECORD_IS_REPLAY)
|
|
{
|
|
struct cleanup *old_cleanups = record_gdb_operation_disable_set ();
|
|
int ret = record_beneath_to_remove_breakpoint (gdbarch, bp_tgt);
|
|
|
|
do_cleanups (old_cleanups);
|
|
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* "to_can_execute_reverse" method for process record target. */
|
|
|
|
static int
|
|
record_can_execute_reverse (void)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
/* "to_get_bookmark" method for process record and prec over core. */
|
|
|
|
static gdb_byte *
|
|
record_get_bookmark (char *args, int from_tty)
|
|
{
|
|
gdb_byte *ret = NULL;
|
|
|
|
/* Return stringified form of instruction count. */
|
|
if (record_list && record_list->type == record_end)
|
|
ret = xstrdup (pulongest (record_list->u.end.insn_num));
|
|
|
|
if (record_debug)
|
|
{
|
|
if (ret)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"record_get_bookmark returns %s\n", ret);
|
|
else
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"record_get_bookmark returns NULL\n");
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/* The implementation of the command "record goto". */
|
|
static void cmd_record_goto (char *, int);
|
|
|
|
/* "to_goto_bookmark" method for process record and prec over core. */
|
|
|
|
static void
|
|
record_goto_bookmark (gdb_byte *bookmark, int from_tty)
|
|
{
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog,
|
|
"record_goto_bookmark receives %s\n", bookmark);
|
|
|
|
if (bookmark[0] == '\'' || bookmark[0] == '\"')
|
|
{
|
|
if (bookmark[strlen (bookmark) - 1] != bookmark[0])
|
|
error (_("Unbalanced quotes: %s"), bookmark);
|
|
|
|
/* Strip trailing quote. */
|
|
bookmark[strlen (bookmark) - 1] = '\0';
|
|
/* Strip leading quote. */
|
|
bookmark++;
|
|
/* Pass along to cmd_record_goto. */
|
|
}
|
|
|
|
cmd_record_goto ((char *) bookmark, from_tty);
|
|
return;
|
|
}
|
|
|
|
static void
|
|
init_record_ops (void)
|
|
{
|
|
record_ops.to_shortname = "record";
|
|
record_ops.to_longname = "Process record and replay target";
|
|
record_ops.to_doc =
|
|
"Log program while executing and replay execution from log.";
|
|
record_ops.to_open = record_open;
|
|
record_ops.to_close = record_close;
|
|
record_ops.to_resume = record_resume;
|
|
record_ops.to_wait = record_wait;
|
|
record_ops.to_disconnect = record_disconnect;
|
|
record_ops.to_detach = record_detach;
|
|
record_ops.to_mourn_inferior = record_mourn_inferior;
|
|
record_ops.to_kill = record_kill;
|
|
record_ops.to_create_inferior = find_default_create_inferior;
|
|
record_ops.to_store_registers = record_store_registers;
|
|
record_ops.to_xfer_partial = record_xfer_partial;
|
|
record_ops.to_insert_breakpoint = record_insert_breakpoint;
|
|
record_ops.to_remove_breakpoint = record_remove_breakpoint;
|
|
record_ops.to_stopped_by_watchpoint = record_stopped_by_watchpoint;
|
|
record_ops.to_stopped_data_address = record_stopped_data_address;
|
|
record_ops.to_can_execute_reverse = record_can_execute_reverse;
|
|
record_ops.to_stratum = record_stratum;
|
|
/* Add bookmark target methods. */
|
|
record_ops.to_get_bookmark = record_get_bookmark;
|
|
record_ops.to_goto_bookmark = record_goto_bookmark;
|
|
record_ops.to_magic = OPS_MAGIC;
|
|
}
|
|
|
|
/* "to_resume" method for prec over corefile. */
|
|
|
|
static void
|
|
record_core_resume (struct target_ops *ops, ptid_t ptid, int step,
|
|
enum target_signal signal)
|
|
{
|
|
record_resume_step = step;
|
|
}
|
|
|
|
/* "to_kill" method for prec over corefile. */
|
|
|
|
static void
|
|
record_core_kill (struct target_ops *ops)
|
|
{
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog, "Process record: record_core_kill\n");
|
|
|
|
unpush_target (&record_core_ops);
|
|
}
|
|
|
|
/* "to_fetch_registers" method for prec over corefile. */
|
|
|
|
static void
|
|
record_core_fetch_registers (struct target_ops *ops,
|
|
struct regcache *regcache,
|
|
int regno)
|
|
{
|
|
if (regno < 0)
|
|
{
|
|
int num = gdbarch_num_regs (get_regcache_arch (regcache));
|
|
int i;
|
|
|
|
for (i = 0; i < num; i ++)
|
|
regcache_raw_supply (regcache, i,
|
|
record_core_regbuf + MAX_REGISTER_SIZE * i);
|
|
}
|
|
else
|
|
regcache_raw_supply (regcache, regno,
|
|
record_core_regbuf + MAX_REGISTER_SIZE * regno);
|
|
}
|
|
|
|
/* "to_prepare_to_store" method for prec over corefile. */
|
|
|
|
static void
|
|
record_core_prepare_to_store (struct regcache *regcache)
|
|
{
|
|
}
|
|
|
|
/* "to_store_registers" method for prec over corefile. */
|
|
|
|
static void
|
|
record_core_store_registers (struct target_ops *ops,
|
|
struct regcache *regcache,
|
|
int regno)
|
|
{
|
|
if (record_gdb_operation_disable)
|
|
regcache_raw_collect (regcache, regno,
|
|
record_core_regbuf + MAX_REGISTER_SIZE * regno);
|
|
else
|
|
error (_("You can't do that without a process to debug."));
|
|
}
|
|
|
|
/* "to_xfer_partial" method for prec over corefile. */
|
|
|
|
static LONGEST
|
|
record_core_xfer_partial (struct target_ops *ops, enum target_object object,
|
|
const char *annex, gdb_byte *readbuf,
|
|
const gdb_byte *writebuf, ULONGEST offset,
|
|
LONGEST len)
|
|
{
|
|
if (object == TARGET_OBJECT_MEMORY)
|
|
{
|
|
if (record_gdb_operation_disable || !writebuf)
|
|
{
|
|
struct target_section *p;
|
|
for (p = record_core_start; p < record_core_end; p++)
|
|
{
|
|
if (offset >= p->addr)
|
|
{
|
|
struct record_core_buf_entry *entry;
|
|
ULONGEST sec_offset;
|
|
|
|
if (offset >= p->endaddr)
|
|
continue;
|
|
|
|
if (offset + len > p->endaddr)
|
|
len = p->endaddr - offset;
|
|
|
|
sec_offset = offset - p->addr;
|
|
|
|
/* Read readbuf or write writebuf p, offset, len. */
|
|
/* Check flags. */
|
|
if (p->the_bfd_section->flags & SEC_CONSTRUCTOR
|
|
|| (p->the_bfd_section->flags & SEC_HAS_CONTENTS) == 0)
|
|
{
|
|
if (readbuf)
|
|
memset (readbuf, 0, len);
|
|
return len;
|
|
}
|
|
/* Get record_core_buf_entry. */
|
|
for (entry = record_core_buf_list; entry;
|
|
entry = entry->prev)
|
|
if (entry->p == p)
|
|
break;
|
|
if (writebuf)
|
|
{
|
|
if (!entry)
|
|
{
|
|
/* Add a new entry. */
|
|
entry
|
|
= (struct record_core_buf_entry *)
|
|
xmalloc
|
|
(sizeof (struct record_core_buf_entry));
|
|
entry->p = p;
|
|
if (!bfd_malloc_and_get_section (p->bfd,
|
|
p->the_bfd_section,
|
|
&entry->buf))
|
|
{
|
|
xfree (entry);
|
|
return 0;
|
|
}
|
|
entry->prev = record_core_buf_list;
|
|
record_core_buf_list = entry;
|
|
}
|
|
|
|
memcpy (entry->buf + sec_offset, writebuf,
|
|
(size_t) len);
|
|
}
|
|
else
|
|
{
|
|
if (!entry)
|
|
return record_beneath_to_xfer_partial
|
|
(record_beneath_to_xfer_partial_ops,
|
|
object, annex, readbuf, writebuf,
|
|
offset, len);
|
|
|
|
memcpy (readbuf, entry->buf + sec_offset,
|
|
(size_t) len);
|
|
}
|
|
|
|
return len;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
else
|
|
error (_("You can't do that without a process to debug."));
|
|
}
|
|
|
|
return record_beneath_to_xfer_partial (record_beneath_to_xfer_partial_ops,
|
|
object, annex, readbuf, writebuf,
|
|
offset, len);
|
|
}
|
|
|
|
/* "to_insert_breakpoint" method for prec over corefile. */
|
|
|
|
static int
|
|
record_core_insert_breakpoint (struct gdbarch *gdbarch,
|
|
struct bp_target_info *bp_tgt)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
/* "to_remove_breakpoint" method for prec over corefile. */
|
|
|
|
static int
|
|
record_core_remove_breakpoint (struct gdbarch *gdbarch,
|
|
struct bp_target_info *bp_tgt)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
/* "to_has_execution" method for prec over corefile. */
|
|
|
|
int
|
|
record_core_has_execution (struct target_ops *ops)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
static void
|
|
init_record_core_ops (void)
|
|
{
|
|
record_core_ops.to_shortname = "record_core";
|
|
record_core_ops.to_longname = "Process record and replay target";
|
|
record_core_ops.to_doc =
|
|
"Log program while executing and replay execution from log.";
|
|
record_core_ops.to_open = record_open;
|
|
record_core_ops.to_close = record_close;
|
|
record_core_ops.to_resume = record_core_resume;
|
|
record_core_ops.to_wait = record_wait;
|
|
record_core_ops.to_kill = record_core_kill;
|
|
record_core_ops.to_fetch_registers = record_core_fetch_registers;
|
|
record_core_ops.to_prepare_to_store = record_core_prepare_to_store;
|
|
record_core_ops.to_store_registers = record_core_store_registers;
|
|
record_core_ops.to_xfer_partial = record_core_xfer_partial;
|
|
record_core_ops.to_insert_breakpoint = record_core_insert_breakpoint;
|
|
record_core_ops.to_remove_breakpoint = record_core_remove_breakpoint;
|
|
record_core_ops.to_stopped_by_watchpoint = record_stopped_by_watchpoint;
|
|
record_core_ops.to_stopped_data_address = record_stopped_data_address;
|
|
record_core_ops.to_can_execute_reverse = record_can_execute_reverse;
|
|
record_core_ops.to_has_execution = record_core_has_execution;
|
|
record_core_ops.to_stratum = record_stratum;
|
|
/* Add bookmark target methods. */
|
|
record_core_ops.to_get_bookmark = record_get_bookmark;
|
|
record_core_ops.to_goto_bookmark = record_goto_bookmark;
|
|
record_core_ops.to_magic = OPS_MAGIC;
|
|
}
|
|
|
|
/* Implement "show record debug" command. */
|
|
|
|
static void
|
|
show_record_debug (struct ui_file *file, int from_tty,
|
|
struct cmd_list_element *c, const char *value)
|
|
{
|
|
fprintf_filtered (file, _("Debugging of process record target is %s.\n"),
|
|
value);
|
|
}
|
|
|
|
/* Alias for "target record". */
|
|
|
|
static void
|
|
cmd_record_start (char *args, int from_tty)
|
|
{
|
|
execute_command ("target record", from_tty);
|
|
}
|
|
|
|
/* Truncate the record log from the present point
|
|
of replay until the end. */
|
|
|
|
static void
|
|
cmd_record_delete (char *args, int from_tty)
|
|
{
|
|
if (current_target.to_stratum == record_stratum)
|
|
{
|
|
if (RECORD_IS_REPLAY)
|
|
{
|
|
if (!from_tty || query (_("Delete the log from this point forward "
|
|
"and begin to record the running message "
|
|
"at current PC?")))
|
|
record_list_release_following (record_list);
|
|
}
|
|
else
|
|
printf_unfiltered (_("Already at end of record list.\n"));
|
|
|
|
}
|
|
else
|
|
printf_unfiltered (_("Process record is not started.\n"));
|
|
}
|
|
|
|
/* Implement the "stoprecord" or "record stop" command. */
|
|
|
|
static void
|
|
cmd_record_stop (char *args, int from_tty)
|
|
{
|
|
if (current_target.to_stratum == record_stratum)
|
|
{
|
|
unpush_target (&record_ops);
|
|
printf_unfiltered (_("Process record is stopped and all execution "
|
|
"logs are deleted.\n"));
|
|
}
|
|
else
|
|
printf_unfiltered (_("Process record is not started.\n"));
|
|
}
|
|
|
|
/* Set upper limit of record log size. */
|
|
|
|
static void
|
|
set_record_insn_max_num (char *args, int from_tty, struct cmd_list_element *c)
|
|
{
|
|
if (record_insn_num > record_insn_max_num && record_insn_max_num)
|
|
{
|
|
/* Count down record_insn_num while releasing records from list. */
|
|
while (record_insn_num > record_insn_max_num)
|
|
{
|
|
record_list_release_first ();
|
|
record_insn_num--;
|
|
}
|
|
}
|
|
}
|
|
|
|
static struct cmd_list_element *record_cmdlist, *set_record_cmdlist,
|
|
*show_record_cmdlist, *info_record_cmdlist;
|
|
|
|
static void
|
|
set_record_command (char *args, int from_tty)
|
|
{
|
|
printf_unfiltered (_("\
|
|
\"set record\" must be followed by an apporpriate subcommand.\n"));
|
|
help_list (set_record_cmdlist, "set record ", all_commands, gdb_stdout);
|
|
}
|
|
|
|
static void
|
|
show_record_command (char *args, int from_tty)
|
|
{
|
|
cmd_show_list (show_record_cmdlist, from_tty, "");
|
|
}
|
|
|
|
/* Display some statistics about the execution log. */
|
|
|
|
static void
|
|
info_record_command (char *args, int from_tty)
|
|
{
|
|
struct record_entry *p;
|
|
|
|
if (current_target.to_stratum == record_stratum)
|
|
{
|
|
if (RECORD_IS_REPLAY)
|
|
printf_filtered (_("Replay mode:\n"));
|
|
else
|
|
printf_filtered (_("Record mode:\n"));
|
|
|
|
/* Find entry for first actual instruction in the log. */
|
|
for (p = record_first.next;
|
|
p != NULL && p->type != record_end;
|
|
p = p->next)
|
|
;
|
|
|
|
/* Do we have a log at all? */
|
|
if (p != NULL && p->type == record_end)
|
|
{
|
|
/* Display instruction number for first instruction in the log. */
|
|
printf_filtered (_("Lowest recorded instruction number is %s.\n"),
|
|
pulongest (p->u.end.insn_num));
|
|
|
|
/* If in replay mode, display where we are in the log. */
|
|
if (RECORD_IS_REPLAY)
|
|
printf_filtered (_("Current instruction number is %s.\n"),
|
|
pulongest (record_list->u.end.insn_num));
|
|
|
|
/* Display instruction number for last instruction in the log. */
|
|
printf_filtered (_("Highest recorded instruction number is %s.\n"),
|
|
pulongest (record_insn_count));
|
|
|
|
/* Display log count. */
|
|
printf_filtered (_("Log contains %d instructions.\n"),
|
|
record_insn_num);
|
|
}
|
|
else
|
|
{
|
|
printf_filtered (_("No instructions have been logged.\n"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf_filtered (_("target record is not active.\n"));
|
|
}
|
|
|
|
/* Display max log size. */
|
|
printf_filtered (_("Max logged instructions is %d.\n"),
|
|
record_insn_max_num);
|
|
}
|
|
|
|
/* 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_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 input)
|
|
{
|
|
uint64_t ret;
|
|
|
|
store_unsigned_integer ((gdb_byte *) &ret, sizeof (ret),
|
|
BFD_ENDIAN_BIG, input);
|
|
return ret;
|
|
}
|
|
|
|
static inline uint32_t
|
|
netorder32 (uint32_t input)
|
|
{
|
|
uint32_t ret;
|
|
|
|
store_unsigned_integer ((gdb_byte *) &ret, sizeof (ret),
|
|
BFD_ENDIAN_BIG, input);
|
|
return ret;
|
|
}
|
|
|
|
static inline uint16_t
|
|
netorder16 (uint16_t input)
|
|
{
|
|
uint16_t ret;
|
|
|
|
store_unsigned_integer ((gdb_byte *) &ret, sizeof (ret),
|
|
BFD_ENDIAN_BIG, input);
|
|
return ret;
|
|
}
|
|
|
|
/* 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)
|
|
fprintf_unfiltered (gdb_stdlog, "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)
|
|
fprintf_unfiltered (gdb_stdlog, "Find precord section %s.\n",
|
|
osec ? "succeeded" : "failed");
|
|
if (osec == NULL)
|
|
return;
|
|
if (record_debug)
|
|
fprintf_unfiltered (gdb_stdlog, "%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)
|
|
fprintf_unfiltered (gdb_stdlog, "\
|
|
Reading 4-byte magic cookie RECORD_FILE_MAGIC (0x%s)\n",
|
|
phex_nz (netorder32 (magic), 4));
|
|
|
|
/* 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, ®num,
|
|
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)
|
|
fprintf_unfiltered (gdb_stdlog, "\
|
|
Reading register %d (1 plus %lu plus %d bytes)\n",
|
|
rec->u.reg.num,
|
|
(unsigned long) 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)
|
|
fprintf_unfiltered (gdb_stdlog, "\
|
|
Reading memory %s (1 plus %lu plus %lu plus %d bytes)\n",
|
|
paddress (get_current_arch (),
|
|
rec->u.mem.addr),
|
|
(unsigned long) sizeof (addr),
|
|
(unsigned long) 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)
|
|
fprintf_unfiltered (gdb_stdlog, "\
|
|
Reading record_end (1 + %lu + %lu bytes), offset == %s\n",
|
|
(unsigned long) sizeof (signal),
|
|
(unsigned long) 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);
|
|
}
|
|
|
|
/* 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)
|
|
fprintf_unfiltered (gdb_stdlog, "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)
|
|
fprintf_unfiltered (gdb_stdlog, "\
|
|
Writing 4-byte magic cookie RECORD_FILE_MAGIC (0x%s)\n",
|
|
phex_nz (magic, 4));
|
|
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)
|
|
fprintf_unfiltered (gdb_stdlog, "\
|
|
Writing register %d (1 plus %lu plus %d bytes)\n",
|
|
record_list->u.reg.num,
|
|
(unsigned long) sizeof (regnum),
|
|
record_list->u.reg.len);
|
|
|
|
/* Write regnum. */
|
|
regnum = netorder32 (record_list->u.reg.num);
|
|
bfdcore_write (obfd, osec, ®num,
|
|
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)
|
|
fprintf_unfiltered (gdb_stdlog, "\
|
|
Writing memory %s (1 plus %lu plus %lu plus %d bytes)\n",
|
|
paddress (gdbarch,
|
|
record_list->u.mem.addr),
|
|
(unsigned long) sizeof (addr),
|
|
(unsigned long) 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)
|
|
fprintf_unfiltered (gdb_stdlog, "\
|
|
Writing record_end (1 + %lu + %lu bytes)\n",
|
|
(unsigned long) sizeof (signal),
|
|
(unsigned long) 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);
|
|
}
|
|
|
|
/* record_goto_insn -- rewind the record log (forward or backward,
|
|
depending on DIR) to the given entry, changing the program state
|
|
correspondingly. */
|
|
|
|
static void
|
|
record_goto_insn (struct record_entry *entry,
|
|
enum exec_direction_kind dir)
|
|
{
|
|
struct cleanup *set_cleanups = record_gdb_operation_disable_set ();
|
|
struct regcache *regcache = get_current_regcache ();
|
|
struct gdbarch *gdbarch = get_regcache_arch (regcache);
|
|
|
|
/* Assume everything is valid: we will hit the entry,
|
|
and we will not hit the end of the recording. */
|
|
|
|
if (dir == EXEC_FORWARD)
|
|
record_list = record_list->next;
|
|
|
|
do
|
|
{
|
|
record_exec_insn (regcache, gdbarch, record_list);
|
|
if (dir == EXEC_REVERSE)
|
|
record_list = record_list->prev;
|
|
else
|
|
record_list = record_list->next;
|
|
} while (record_list != entry);
|
|
do_cleanups (set_cleanups);
|
|
}
|
|
|
|
/* "record goto" command. Argument is an instruction number,
|
|
as given by "info record".
|
|
|
|
Rewinds the recording (forward or backward) to the given instruction. */
|
|
|
|
static void
|
|
cmd_record_goto (char *arg, int from_tty)
|
|
{
|
|
struct record_entry *p = NULL;
|
|
ULONGEST target_insn = 0;
|
|
|
|
if (arg == NULL || *arg == '\0')
|
|
error (_("Command requires an argument (insn number to go to)."));
|
|
|
|
if (strncmp (arg, "start", strlen ("start")) == 0
|
|
|| strncmp (arg, "begin", strlen ("begin")) == 0)
|
|
{
|
|
/* Special case. Find first insn. */
|
|
for (p = &record_first; p != NULL; p = p->next)
|
|
if (p->type == record_end)
|
|
break;
|
|
if (p)
|
|
target_insn = p->u.end.insn_num;
|
|
}
|
|
else if (strncmp (arg, "end", strlen ("end")) == 0)
|
|
{
|
|
/* Special case. Find last insn. */
|
|
for (p = record_list; p->next != NULL; p = p->next)
|
|
;
|
|
for (; p!= NULL; p = p->prev)
|
|
if (p->type == record_end)
|
|
break;
|
|
if (p)
|
|
target_insn = p->u.end.insn_num;
|
|
}
|
|
else
|
|
{
|
|
/* General case. Find designated insn. */
|
|
target_insn = parse_and_eval_long (arg);
|
|
|
|
for (p = &record_first; p != NULL; p = p->next)
|
|
if (p->type == record_end && p->u.end.insn_num == target_insn)
|
|
break;
|
|
}
|
|
|
|
if (p == NULL)
|
|
error (_("Target insn '%s' not found."), arg);
|
|
else if (p == record_list)
|
|
error (_("Already at insn '%s'."), arg);
|
|
else if (p->u.end.insn_num > record_list->u.end.insn_num)
|
|
{
|
|
printf_filtered (_("Go forward to insn number %s\n"),
|
|
pulongest (target_insn));
|
|
record_goto_insn (p, EXEC_FORWARD);
|
|
}
|
|
else
|
|
{
|
|
printf_filtered (_("Go backward to insn number %s\n"),
|
|
pulongest (target_insn));
|
|
record_goto_insn (p, EXEC_REVERSE);
|
|
}
|
|
registers_changed ();
|
|
reinit_frame_cache ();
|
|
print_stack_frame (get_selected_frame (NULL), 1, SRC_AND_LOC);
|
|
}
|
|
|
|
void
|
|
_initialize_record (void)
|
|
{
|
|
struct cmd_list_element *c;
|
|
|
|
/* Init record_first. */
|
|
record_first.prev = NULL;
|
|
record_first.next = NULL;
|
|
record_first.type = record_end;
|
|
|
|
init_record_ops ();
|
|
add_target (&record_ops);
|
|
init_record_core_ops ();
|
|
add_target (&record_core_ops);
|
|
|
|
add_setshow_zinteger_cmd ("record", no_class, &record_debug,
|
|
_("Set debugging of record/replay feature."),
|
|
_("Show debugging of record/replay feature."),
|
|
_("When enabled, debugging output for "
|
|
"record/replay feature is displayed."),
|
|
NULL, show_record_debug, &setdebuglist,
|
|
&showdebuglist);
|
|
|
|
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);
|
|
add_alias_cmd ("rec", "record", class_obscure, 1, &setlist);
|
|
add_prefix_cmd ("record", class_support, show_record_command,
|
|
_("Show record options"), &show_record_cmdlist,
|
|
"show record ", 0, &showlist);
|
|
add_alias_cmd ("rec", "record", class_obscure, 1, &showlist);
|
|
add_prefix_cmd ("record", class_support, info_record_command,
|
|
_("Info record options"), &info_record_cmdlist,
|
|
"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);
|
|
set_cmd_completer (c, filename_completer);
|
|
|
|
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."),
|
|
&record_cmdlist);
|
|
add_alias_cmd ("d", "delete", class_obscure, 1, &record_cmdlist);
|
|
add_alias_cmd ("del", "delete", class_obscure, 1, &record_cmdlist);
|
|
|
|
add_cmd ("stop", class_obscure, cmd_record_stop,
|
|
_("Stop the record/replay target."),
|
|
&record_cmdlist);
|
|
add_alias_cmd ("s", "stop", class_obscure, 1, &record_cmdlist);
|
|
|
|
/* Record instructions number limit command. */
|
|
add_setshow_boolean_cmd ("stop-at-limit", no_class,
|
|
&record_stop_at_limit, _("\
|
|
Set whether record/replay stops when record/replay buffer becomes full."), _("\
|
|
Show whether record/replay stops when record/replay buffer becomes full."), _("\
|
|
Default is ON.\n\
|
|
When ON, if the record/replay buffer becomes full, ask user what to do.\n\
|
|
When OFF, if the record/replay buffer becomes full,\n\
|
|
delete the oldest recorded instruction to make room for each new one."),
|
|
NULL, NULL,
|
|
&set_record_cmdlist, &show_record_cmdlist);
|
|
add_setshow_uinteger_cmd ("insn-number-max", no_class,
|
|
&record_insn_max_num,
|
|
_("Set record/replay buffer limit."),
|
|
_("Show record/replay buffer limit."), _("\
|
|
Set the maximum number of instructions to be stored in the\n\
|
|
record/replay buffer. Zero means unlimited. Default is 200000."),
|
|
set_record_insn_max_num,
|
|
NULL, &set_record_cmdlist, &show_record_cmdlist);
|
|
|
|
add_cmd ("goto", class_obscure, cmd_record_goto, _("\
|
|
Restore the program to its state at instruction number N.\n\
|
|
Argument is instruction number, as shown by 'info record'."),
|
|
&record_cmdlist);
|
|
}
|