一、gdb中record命令的功能
在有些时候,程序运行到某个阶段出现了异常,例如堆栈发生了破坏,此时希望有一个“时光机”能反方向执行,直到首次发生异常的地方。当然,如果是内存修改,其实可以使用watch这个更简单高效的命令来实现,这可能也是这个命令不怎么常见的一个原因吧。不论如何,这个命令还是有点意思的,这里简单讨论下它实现的原理。
二、实现原理
从gdb的源码来看,它实现的原理就是在单步执行的情况下,每次都解析当前执行的指令,“理解每条指令的语义”并进行相应的记录。当然,这个记录没必要是增量或者undo之类的记录,只需要记录修改前的全量值可以用来进行恢复即可。例如,一条指令是修改eax寄存器的值,那么记录下这条指令位置和执行前EAX寄存器的值即可。
三、实现代码
对于386系统,在gdb-7.6.1\gdb\i386-tdep.c文件中,对于乘法、除法之类的指令,它们通常需要修改EAX、EDX、EFLAGS寄存器,所以在执行这条机器指令的时候就需要把这些寄存器的值保存起来,从而在反向执行的时候恢复它修改的内容。
/* "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_full_wait_1 (struct target_ops *ops,
ptid_t ptid, struct target_waitstatus *status,
int options)
{
……
case 4: /* mul */
case 5: /* imul */
case 6: /* div */
case 7: /* idiv */
I386_RECORD_FULL_ARCH_LIST_ADD_REG (X86_RECORD_REAX_REGNUM);
if (ir.ot != OT_BYTE)
I386_RECORD_FULL_ARCH_LIST_ADD_REG (X86_RECORD_REDX_REGNUM);
I386_RECORD_FULL_ARCH_LIST_ADD_REG (X86_RECORD_EFLAGS_REGNUM);
break;
……
}
1、对寄存器的保存
/* Parse the current instruction, and record the values of the
registers and memory that will be changed by the current
instruction. Returns -1 if something goes wrong, 0 otherwise. */
#define I386_RECORD_FULL_ARCH_LIST_ADD_REG(regnum) \
record_full_arch_list_add_reg (ir.regcache, ir.regmap[(regnum)])
/* Record the value of a register NUM to record_full_arch_list. */
int
record_full_arch_list_add_reg (struct regcache *regcache, int regnum)
{
struct record_full_entry *rec;
if (record_debug > 1)
fprintf_unfiltered (gdb_stdlog,
"Process record: add register num = %d to "
"record list.\n",
regnum);
rec = record_full_reg_alloc (regcache, regnum);
regcache_raw_read (regcache, regnum, record_full_get_loc (rec));
record_full_arch_list_add (rec);
return 0;
}
2、对于内存内容的保存
/* Record the address and contents of the memory that will be changed
by the current instruction. Return -1 if something goes wrong, 0
otherwise. */
static int
i386_record_lea_modrm (struct i386_record_s *irp)
{
struct gdbarch *gdbarch = irp->gdbarch;
uint64_t addr;
if (irp->override >= 0)
{
if (record_full_memory_query)
{
int q;
target_terminal_ours ();
q = yquery (_("\
Process record ignores the memory change of instruction at address %s\n\
because it can‘t get the value of the segment register.\n\
Do you want to stop the program?"),
paddress (gdbarch, irp->orig_addr));
target_terminal_inferior ();
if (q)
return -1;
}
return 0;
}
if (i386_record_lea_modrm_addr (irp, &addr))
return -1;
if (record_full_arch_list_add_mem (addr, 1 << irp->ot))
return -1;
return 0;
}
3、对一些系统调用的记录
gdb-7.6.1\gdb\linux-record.c
/* When the architecture process record get a Linux syscall
instruction, it will get a Linux syscall number of this
architecture and convert it to the Linux syscall number "num" which
is internal to GDB. Most Linux syscalls across architectures in
Linux would be similar and mostly differ by sizes of types and
structures. This sizes are put to "tdep".
Record the values of the registers and memory that will be changed
in current system call.
Return -1 if something wrong. */
int
record_linux_system_call (enum gdb_syscall syscall,
struct regcache *regcache,
struct linux_record_tdep *tdep)
{
struct gdbarch *gdbarch = get_regcache_arch (regcache);
enum bfd_endian byte_order = gdbarch_byte_order (gdbarch);
ULONGEST tmpulongest;
CORE_ADDR tmpaddr;
int tmpint;
switch (syscall)
{
case gdb_sys_restart_syscall:
break;
case gdb_sys_exit:
{
int q;
target_terminal_ours ();
q = yquery (_("The next instruction is syscall exit. "
"It will make the program exit. "
"Do you want to stop the program?"));
target_terminal_inferior ();
if (q)
return 1;
}
break;
case gdb_sys_fork:
break;
case gdb_sys_read:
{
ULONGEST addr, count;
regcache_raw_read_unsigned (regcache, tdep->arg2, &addr);
regcache_raw_read_unsigned (regcache, tdep->arg3, &count);
if (record_full_arch_list_add_mem ((CORE_ADDR) addr, (int) count))
return -1;
}
break;
……
}
四、举个例子
这个例子可能不太恰当
tsecer@harry: cat gdb.reverse.cpp
int main(int argc, const char *argv[])
{
int *pi = (int*)&argc;
for (long i = 1; i < 100; i++)
{
pi[i] = 0;
}
return 0;
}
tsecer@harry: gcc -g gdb.reverse.cpp
tsecer@harry: gdb ./a.out
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-80.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/harry/gdb.reverse/a.out...done.
(gdb) start
Temporary breakpoint 1 at 0x4004e2: file gdb.reverse.cpp, line 3.
Starting program: /home/harry/gdb.reverse/./a.out
Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe548) at gdb.reverse.cpp:3
3 int *pi = (int*)&argc;
(gdb) record
(gdb) c
Continuing.
Process record: failed to record execution log.
[process 22631] #1 stopped.
0x000000000040050c in main (argc=1, argv=0x7fffffffe548) at gdb.reverse.cpp:6
6 pi[i] = 0;
(gdb) display i
1: i = 2
(gdb) reverse-next
6 pi[i] = 0;
1: i = 2
(gdb)
4 for (long i = 1; i < 100; i++)
1: i = 1
(gdb)
6 pi[i] = 0;
1: i = 1
(gdb)
4 for (long i = 1; i < 100; i++)
1: i = 0
(gdb)
No more reverse-execution history.
main (argc=1, argv=0x7fffffffe548) at gdb.reverse.cpp:3
3 int *pi = (int*)&argc;
(gdb)
可以看到,这个地方是由于在循环中pi的值被修改导致的。