反向调试如何工作?


82

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

为了使反向调试正常工作,在我看来,您需要存储整个计算机状态,包括每个步骤的内存。这将使性能异常下降,更不用说使用大量内存了。这些问题如何解决?


4
我想您可以通过存储状态增量而不是整个状态来解决问题,但这似乎仍然很昂贵。
支出者


节省增量确实可以很好地工作,并且对于有效的全系统可逆解决方案而言确实是必要的。
jakobengblom2,2009

Answers:


131

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

是的,它很昂贵,但是现代的cpus是如此之快,以至于无论如何您都要进行交互(执行步进或断点)时,您都不会真正注意到它。


4
但是反向调试是否只允许您回滚nextstep键入的命令,还是允许您撤消任何数量的指令?例如,如果我在一条指令上设置了一个断点并让它一直运行到那时,那么即使我跳过了它,我也可以回滚到上一条指令吗?
内森·费尔曼

10
>但是反向调试仅允许您回退所键入的命令的下一个和步骤,还是允许您撤消任何数量的指令。您不受限制,例如仅在前进时停在了您停止的位置。您可以设置一个新的断点并向后运行它>例如,如果我在一条指令上设置了一个断点并让它一直运行到那时,那么即使我跳过了它,我也可以回滚到上一条指令吗,是的,只要您在运行到断点之前打开了录制模式
Michael Snyder

3
抱歉,未格式化的文本不知道怎么回事。
Michael Snyder

10
我担心反向调试会浪费时间,并使我们回到60或70年代。我不想穿喇叭裤再长发。
Tin Man

3
以及在操作系统中修改状态的syscall?那会不能正常工作?修改不透明手柄时该怎么办?
阿德里安

12

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

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

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

简化后,它的工作方式是这样的:说您在系统执行的时间T处。要转到时间T-1,请从点t <T拾取一些检查点,然后执行(Tt-1)循环以结束之前的一个循环。这可以使其工作得很好,甚至适用于执行磁盘IO,由内核级代码组成并执行设备驱动程序工作的工作负载。关键是要拥有一个包含整个目标系统以及所有处理器,设备,存储器和IO的模拟器。有关更多详细信息,请参见gdb邮件列表以及gdb邮件列表上的后续讨论。我自己经常使用这种方法来调试棘手的代码,尤其是在设备驱动程序和早期的OS启动中。

信息的另一个来源是有关检查点Virtutech白皮书(我写的是完整披露)。


另请参阅jakob.engbloms.se/archives/1547及其后续的两个博客文章,以更全面地了解反向调试技术。
jakobengblom2 2012年

“设置保存点”而不是执行反向步进的能力如何?因此,您进行了调试,然后可以选择当前步骤作为“保存点”,以后便可以跳回到该保存点并再次前进,并在必要时编辑变量。对于虚拟机来说有点像“快照”,对于操作系统来说有点像“还原点”。
罗尔夫2014年

9

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

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

在回放期间,它们基本上可以从记录的状态变化中重新创建运行程序的每个状态。

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


8

尽管这个问题很古老,但大多数答案也是 仍然是一个有趣的话题,我发布了2015年的答案。我的硕士学位论文的第1章和第2章,将反向调试和实时编程与计算机编程中的可视化思维相结合,涵盖了一些历史悠久的反向调试方法(特别是针对快照(或检查点)和重放方法),以及解释了它与全能调试之间的区别:

在某种程度上向前执行了该程序的计算机实际上应该能够向我们提供有关该程序的信息。这样的改进是可能的,并且可以在全知的调试器中找到。它们通常被归类为反向调试器,尽管它们可能更准确地被描述为“历史记录”调试器,因为它们仅在执行过程中记录信息以供以后查看或查询,而不是允许程序员在执行程序中实际向后退步。“无所不知”来自以下事实:已记录的程序的整个状态历史记录在执行后可供调试器使用。这样就无需重新运行该程序,也不需要手动代码检测。

基于软件的全方位调试始于1969年的EXDAMS系统,该系统被称为“调试时历史记录回放”。自2009年以来,GNU调试器GDB就凭借其“过程记录和重播”功能支持了全方位的调试。TotalView,UndoDB和Chronon似乎是当前可用的最佳全能调试器,但它们是商业系统。对于Java,TOD似乎是最好的开源替代方案,它利用部分确定性重放,部分跟踪捕获和分布式数据库来记录涉及的大量信息。

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

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


4

Mozillarr是GDB反向调试的更强大的替代品

https://github.com/mozilla/rr

GDB的内置记录和重放具有严格的限制,例如,不支持AVX指令:gdb反向调试失败,显示“过程记录不支持地址处的指令0xf0d”

rr的优点:

  • 目前更加可靠。我已经测试了几种复杂软件的相对长期运行。
  • 还提供了带有gdbserver协议的GDB接口,使其成为了很好的替代品
  • 大多数程序的性能下降很小,如果没有进行测量,我自己并没有注意到
  • 生成的跟踪在磁盘上很小,因为只记录了很少的非确定性事件,到目前为止,我不必担心它们的大小

rr通过首先运行程序来实现此目的,该程序以记录在每个非确定性事件(例如线程切换)中发生的情况的方式进行记录。

然后,在第二次重播运行中,它使用该跟踪文件(该文件非常小)来准确地重构原始非确定性运行中发生的事情,但是以确定性的方式向前或向后进行。

rr最初是由Mozilla开发的,旨在帮助他们重现在第二天的夜间测试中出现的计时错误。但是,当您有一个只在执行过程中几个小时内发生的错误时,反向调试方面也是至关重要的,因为您通常想退后一步检查以前的状态导致了后来的失败。

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

在Ubuntu 18.04上安装:

sudo apt-get install rr linux-tools-common linux-tools-generic linux-cloud-tools-generic
sudo cpupower frequency-set -g performance
# Overcome "rr needs /proc/sys/kernel/perf_event_paranoid <= 1, but it is 3."
echo 'kernel.perf_event_paranoid=1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

测试程序:

#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;

在调试复杂的软件时,您可能会运行到崩溃点,然后陷入深渊。在这种情况下,请不要忘记reverse-next在更高的帧上必须首先:

reverse-finish

到那个帧,仅仅照常做up是不够的。

我认为rr最严重的局限性是:

UndoDB是rr的商业替代品:https ://undo.io两者都是基于跟踪/重播的,但是我不确定它们在功能和性能方面如何进行比较。


您知道我如何用ddd做到这一点吗?谢谢
spraff '19

@spraff我不确定,但是可能。首先尝试将ddd连接到gdbserver。如果可行,它也应与rr一起使用。
西罗Santilli郝海东冠状病六四事件法轮功

1
@spraff,但是,请不要使用ddd,请使用gdb仪表板;-) stackoverflow.com/questions/10115540/gdb-split-view-with-code / ...这肯定是可行的,因为它只是常规的GDB。
西罗Santilli郝海东冠状病六四事件法轮功

3

内森·费尔曼(Nathan Fellman)写道:

但是,反向调试是否仅允许您回退下一个并逐步执行键入的命令,还是允许您撤消任何数量的指令?

您可以撤消任意数量的指令。例如,您不仅限于在前进时停下来的地方停下来。您可以设置一个新的断点并向后运行。

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

是。只要您在运行到断点之前就打开了录制模式。


2
任何反向解决方案的关键部分是您必须在某个时间点将其打开,并且只能在该点之前反向。没有魔法可以使机器真正反向运行,并且无需某种记录就可以发现以前发生的事情。
jakobengblom2

2

是另一个称为ODB的反向调试器的工作方式。提取:

Omniscient Debugging是一种在程序中的每个“关注点”(设置值,进行方法调用,引发/捕获异常)收集“时间戳”的想法,然后允许程序员使用这些时间戳来探索“时间戳”。该程序运行的历史记录。

ODB ...在加载程序时将代码插入程序的类中,并在程序运行时记录事件。

我猜gdb的工作方式相同。


那么,这是否需要代码中的指令来告诉编译器和调试器那些有趣的地方在哪里?
内森·费尔曼

否。www.LambdaCS.com/ debugger / debugger.html上有一个Java Web Start演示,向您展示了它的工作方式。它看起来像一个普通程序。无论如何,这就是ODB,不了解gdb。虽然这很酷:)
demoncodemonkey

请注意,gdb解决方案不会以任何方式更改目标程序。如果必须使用程序来调试它,则由于时序差异和其他干扰,很有可能使问题消失。所有商业revexec工具都基于某种形式的外部记录,这些记录不会更改程序本身的代码。
jakobengblom2,2009

@ jakobengblom2:我认为您过于强调通过写入目标内存,模拟执行或只是添加硬件断点来更改目标之间的区别。它们都改变了时间。实际上,目标仪器的更改时间可能最少。
Ben Voigt

2

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

您不需要为每个步骤存储完整的机器状态,而只需存储更改。它可能仍然很昂贵。


我知道了,但是您仍然需要在每次更改时中断执行以保存更改。
内森·费尔曼

是的,这是正确的,但是机器现在已经相当快了,从人的角度来讲,我不认为这种减速是很可怕的。它与valgrind相当,也许不如valgrind慢。
Michael Snyder
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.