反向调试如何工作?

时间:2009-09-24 08:35:34

标签: reverse-debugging

GDB有一个支持反向调试的新版本(参见http://www.gnu.org/software/gdb/news/reversible.html)。我想知道它是如何工作的。

要使反向调试正常工作,在我看来,您需要存储整个机器状态,包括每个步骤的内存。这会使性能变得非常慢,更不用说使用大量内存了。这些问题是如何解决的?

8 个答案:

答案 0 :(得分:118)

我是gdb维护者,也是新的反向调试的作者之一。我很乐意谈谈它是如何运作的。正如一些人推测的那样,您需要保存足够的机器状态,以便以后恢复。有许多方案,其中之一是简单地保存由每个机器指令修改的寄存器或存储器位置。然后,要“撤消”该指令,只需恢复这些寄存器或存储器位置中的数据。

是的,它很贵,但是现代的cpu是如此之快,以至于当你是交互式的(做踩踏或断点)时,你并没有真正注意到它。

答案 1 :(得分:11)

请注意,您不能忘记使用模拟器,虚拟机和硬件记录器来实现反向执行。

实现它的另一个解决方案是跟踪物理硬件上的执行,例如GreenHills和Lauterbach在其基于硬件的调试器中执行的操作。根据每条指令动作的固定轨迹,您可以依次移除每条指令的效果,移动到轨迹中的任意点。请注意,这假设您可以跟踪影响调试器中可见状态的所有事物。

另一种方法是使用检查点+重新执行方法,该方法由VmWare Workstation 6.5和Virtutech Simics 3.0(及更高版本)使用,并且似乎随Visual Studio 2010一起提供。在这里,您使用虚拟机或模拟器,以获得系统执行的间接级别。您经常将整个状态转储到磁盘或内存,然后依赖模拟器确定性地重新执行完全相同的程序路径。

简化,它的工作原理如下:说你在执行系统的时候是T.要进入时间T-1,你可以从点t <1那里拿起一些检查点。 T,然后执行(T-t-1)个循环,结束一个循环,然后再到达。这可以很好地工作,甚至适用于执行磁盘IO的工作负载,包括内核级代码,并执行设备驱动程序工作。关键是要有一个包含整个目标系统的模拟器,包括所有处理器,设备,内存和IO。有关详细信息,请参阅gdb邮件列表中的the gdb mailinglist及其后的讨论。我经常使用这种方法调试棘手的代码,特别是在设备驱动程序和早期操作系统启动时。

另一个信息来源是Virtutech white paper on checkpointing(我在完全披露时写过)。

答案 2 :(得分:9)

在EclipseCon会话期间,我们还询问了他们如何使用 Chronon Debugger for Java执行此操作。那个不允许你实际后退,但可以回放录制的程序执行,使其感觉像反向调试一样。 (主要区别在于您无法在Chronon调试器中更改正在运行的程序,而您可以在大多数其他Java调试器中执行此操作。)

如果我理解正确,它会操纵正在运行的程序的字节代码,以便记录程序内部状态的每次更改。外部状态不需要另外记录。如果它们以某种方式影响您的程序,那么您必须具有与该外部状态匹配的内部变量(因此该内部变量就足够了)。

在播放时间内,他们可以基本上从记录的状态变化中重新创建正在运行的程序的每个状态。

有趣的是,州的变化远远小于初看时的预期。因此,如果您有一个条件“if”语句,您会认为至少需要一位来记录该程序是否采用了then或else语句。在许多情况下,您甚至可以避免这种情况,例如在那些不同的分支包含返回值的情况下。然后只记录返回值(无论如何都需要),并从返回值本身重新计算关于已执行分支的决定。

答案 3 :(得分:8)

虽然这个问题已经过时了,但大部分答案都是如此,而仍然是一个有趣的话题,我发表了2015年的答案。我的硕士论文的第1章和第2章Combining reverse debugging and live programming towards visual thinking in computer programming涵盖了一些反向调试的历史方法(特别关注快照(或检查点)和重放方法),并解释了它与无所不知的区别。调试:

  

计算机在某些方面已向前执行该程序,应该能够真正向我们提供有关它的信息。这种改进是可能的,并且可以在所谓的无所不知的调试器中找到。它们通常被归类为反向调试器,尽管它们可能更准确地被描述为“历史记录”调试器,因为它们仅在执行期间记录信息以便稍后查看或查询,而不是允许程序员在执行程序中实际倒退。 。 “Omniscient”来自这样一个事实,即已经记录的程序的整个状态历史在执行后可供调试器使用。因此无需重新运行程序,也无需手动编写代码。

     

基于软件的全方位调试始于1969年的EXDAMS系统,称为“调试时历史 - 回放”。自2009年以来,GNU调试器GDB一直支持无所不知的调试,具有“进程记录和重放”功能。 TotalView,UndoDB和Chronon似乎是目前可用的最好的全知调试器,但它们是商业系统。对于Java来说,TOD似乎是最好的开源替代方案,它利用部分确定性重放,以及部分跟踪捕获和分布式数据库来记录所涉及的大量信息。

     

不仅允许导航记录,而且实际上能够在执行时间内倒退的调试器也存在。它们可以更准确地描述为回溯时间,时间旅行,双向或反向调试器。

     

第一个这样的系统是1981年的COPE原型......

答案 4 :(得分:3)

Nathan Fellman写道:

  

但是,反向调试是否只允许您回滚下一步和您键入的步骤命令,还是允许您撤消任意数量的指令?

您可以撤消任意数量的说明。例如,你并不局限于此 只在你前进时停在你停下的地方。您可以 设置一个新的断点并向后运行。

  

例如,如果我在一条指令上设置一个断点并让它运行到那时,那么我可以回滚到上一条指令,即使我跳过了它吗?

是。只要在跑到断点之前打开录制模式。

答案 5 :(得分:2)

Here是另一个名为ODB的反向调试器。提取物:

  

Omniscient Debugging是一个想法   每个人都收集“时间戳”   “兴趣点”(设定值,   进行方法调用,   抛出/捕捉异常)   程序,然后允许   程序员使用那些时间戳来   探索该计划的历史   运行

     

ODB ...插入   将代码编入程序的类中   他们加载和程序时   跑,记录事件。

我猜测gdb可以用同样的方式工作。

答案 6 :(得分:2)

反向调试意味着您可以向后运行程序,这对于追踪问题的原因非常有用。

您不需要为每个步骤存储整个机器状态,只需存储更改。它可能还很贵。

答案 7 :(得分:1)

mozilla rr是GDB反向调试的更强大的替代方法

https://github.com/mozilla/rr

GDB的内置记录和重放具有严重的局限性,例如不支持AVX指令:gdb reverse debugging fails with "Process record does not support instruction 0xf0d at address"

rr的优点:

  • 当前更加可靠
  • 还提供了带有gdbserver协议的GDB接口,使其成为了很好的替代品
  • 许多程序的性能下降很小

以下示例展示了其某些功能,尤其是reverse-nextreverse-stepreverse-continue命令。

安装Ubuntu 16.04:

sudo apt-get install rr linux-tools-common linux-tools-generic linux-cloud-tools-generic
sudo cpupower frequency-set -g performance

但是也可以考虑从源代码进行编译以获取最新更新,这并不难。

测试程序:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int f() {
    int i;
    i = 0;
    i = 1;
    i = 2;
    return i;
}

int main(void) {
    int i;

    i = 0;
    i = 1;
    i = 2;

    /* Local call. */
    f();

    printf("i = %d\n", i);

    /* Is randomness completely removed?
     * Recently fixed: https://github.com/mozilla/rr/issues/2088 */
    i = time(NULL);
    printf("time(NULL) = %d\n", i);

    return EXIT_SUCCESS;
}

编译并运行:

gcc -O0 -ggdb3 -o reverse.out -std=c89 -Wextra reverse.c
rr record ./reverse.out
rr replay

现在您被留在了GDB会话中,可以正确地进行调试了:

(rr) break main
Breakpoint 1 at 0x55da250e96b0: file a.c, line 16.
(rr) continue
Continuing.

Breakpoint 1, main () at a.c:16
16          i = 0;
(rr) next
17          i = 1;
(rr) print i
$1 = 0
(rr) next
18          i = 2;
(rr) print i
$2 = 1
(rr) reverse-next
17          i = 1;
(rr) print i
$3 = 0
(rr) next
18          i = 2;
(rr) print i
$4 = 1
(rr) next
21          f();
(rr) step
f () at a.c:7
7           i = 0;
(rr) reverse-step
main () at a.c:21
21          f();
(rr) next
23          printf("i = %d\n", i);
(rr) next
i = 2
27          i = time(NULL);
(rr) reverse-next
23          printf("i = %d\n", i);
(rr) next
i = 2
27          i = time(NULL);
(rr) next
28          printf("time(NULL) = %d\n", i);
(rr) print i
$5 = 1509245372
(rr) reverse-next
27          i = time(NULL);
(rr) next
28          printf("time(NULL) = %d\n", i);
(rr) print i
$6 = 1509245372
(rr) reverse-continue
Continuing.

Breakpoint 1, main () at a.c:16
16          i = 0;