GDB推出了一个新版本,该版本支持反向调试(请参阅http://www.gnu.org/software/gdb/news/reversible.html)。我想知道它是如何工作的。
为了使反向调试正常工作,在我看来,您需要存储整个计算机状态,包括每个步骤的内存。这将使性能异常下降,更不用说使用大量内存了。这些问题如何解决?
GDB推出了一个新版本,该版本支持反向调试(请参阅http://www.gnu.org/software/gdb/news/reversible.html)。我想知道它是如何工作的。
为了使反向调试正常工作,在我看来,您需要存储整个计算机状态,包括每个步骤的内存。这将使性能异常下降,更不用说使用大量内存了。这些问题如何解决?
Answers:
我是gdb的维护者,也是新的反向调试的作者之一。我很乐意谈论它的工作原理。正如一些人推测的那样,您需要保存足够的计算机状态,以便以后还原。有许多方案,其中一种是简单地保存由每条机器指令修改的寄存器或存储器位置。然后,要“撤消”该指令,只需还原那些寄存器或存储器位置中的数据。
是的,它很昂贵,但是现代的cpus是如此之快,以至于无论如何您都要进行交互(执行步进或断点)时,您都不会真正注意到它。
next
和step
键入的命令,还是允许您撤消任何数量的指令?例如,如果我在一条指令上设置了一个断点并让它一直运行到那时,那么即使我跳过了它,我也可以回滚到上一条指令吗?
请注意,您一定不要忘记使用模拟器,虚拟机和硬件记录器来实现反向执行。
实现它的另一个解决方案是跟踪物理硬件上的执行,例如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白皮书(我写的是完整披露)。
在EclipseCon会话中,我们还询问了他们如何使用Java的Chronon Debugger进行此操作。该操作不允许您实际退后,但可以回放录制的程序执行,感觉就像是反向调试。(主要区别是您无法在Chronon调试器中更改正在运行的程序,而可以在大多数其他Java调试器中进行更改。)
如果我正确理解它,它将操纵正在运行的程序的字节码,以便记录程序内部状态的每次更改。外部状态不需要额外记录。如果它们以某种方式影响您的程序,那么您必须具有一个与该外部状态匹配的内部变量(因此该内部变量就足够了)。
在回放期间,它们基本上可以从记录的状态变化中重新创建运行程序的每个状态。
有趣的是,状态变化比初看起来要小得多。因此,如果您有条件的“ if”语句,您会认为您至少需要一位来记录程序是否采用了then或else语句。在许多情况下,您甚至可以避免这种情况,例如在那些不同的分支包含返回值的情况下。然后,仅记录返回值(无论如何还是需要)并从返回值本身重新计算关于已执行分支的决策就足够了。
尽管这个问题很古老,但大多数答案也是 反向调试仍然是一个有趣的话题,我发布了2015年的答案。我的硕士学位论文的第1章和第2章,将反向调试和实时编程与计算机编程中的可视化思维相结合,涵盖了一些历史悠久的反向调试方法(特别是针对快照(或检查点)和重放方法),以及解释了它与全能调试之间的区别:
在某种程度上向前执行了该程序的计算机实际上应该能够向我们提供有关该程序的信息。这样的改进是可能的,并且可以在全知的调试器中找到。它们通常被归类为反向调试器,尽管它们可能更准确地被描述为“历史记录”调试器,因为它们仅在执行过程中记录信息以供以后查看或查询,而不是允许程序员在执行程序中实际向后退步。“无所不知”来自以下事实:已记录的程序的整个状态历史记录在执行后可供调试器使用。这样就无需重新运行该程序,也不需要手动代码检测。
基于软件的全方位调试始于1969年的EXDAMS系统,该系统被称为“调试时历史记录回放”。自2009年以来,GNU调试器GDB就凭借其“过程记录和重播”功能支持了全方位的调试。TotalView,UndoDB和Chronon似乎是当前可用的最佳全能调试器,但它们是商业系统。对于Java,TOD似乎是最好的开源替代方案,它利用部分确定性重放,部分跟踪捕获和分布式数据库来记录涉及的大量信息。
还存在调试器,它们不仅允许导航记录,而且实际上能够在执行时间上向后退。可以将它们更准确地描述为时间回溯,时间旅行,双向或反向调试器。
第一个这样的系统是1981年的COPE原型...
Mozillarr
是GDB反向调试的更强大的替代品
GDB的内置记录和重放具有严格的限制,例如,不支持AVX指令:gdb反向调试失败,显示“过程记录不支持地址处的指令0xf0d”
rr的优点:
rr通过首先运行程序来实现此目的,该程序以记录在每个非确定性事件(例如线程切换)中发生的情况的方式进行记录。
然后,在第二次重播运行中,它使用该跟踪文件(该文件非常小)来准确地重构原始非确定性运行中发生的事情,但是以确定性的方式向前或向后进行。
rr最初是由Mozilla开发的,旨在帮助他们重现在第二天的夜间测试中出现的计时错误。但是,当您有一个只在执行过程中几个小时内发生的错误时,反向调试方面也是至关重要的,因为您通常想退后一步检查以前的状态导致了后来的失败。
下面的例子展示了它的某些功能,尤其是reverse-next
,reverse-step
和reverse-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两者都是基于跟踪/重播的,但是我不确定它们在功能和性能方面如何进行比较。
内森·费尔曼(Nathan Fellman)写道:
但是,反向调试是否仅允许您回退下一个并逐步执行键入的命令,还是允许您撤消任何数量的指令?
您可以撤消任意数量的指令。例如,您不仅限于在前进时停下来的地方停下来。您可以设置一个新的断点并向后运行。
例如,如果我在一条指令上设置了一个断点并让它一直运行到那时,那么即使我跳过了该指令,也可以回滚到上一条指令吗?
是。只要您在运行到断点之前就打开了录制模式。
这是另一个称为ODB的反向调试器的工作方式。提取:
Omniscient Debugging是一种在程序中的每个“关注点”(设置值,进行方法调用,引发/捕获异常)收集“时间戳”的想法,然后允许程序员使用这些时间戳来探索“时间戳”。该程序运行的历史记录。
ODB ...在加载程序时将代码插入程序的类中,并在程序运行时记录事件。
我猜gdb的工作方式相同。
反向调试意味着您可以向后运行程序,这对于查找问题原因非常有用。
您不需要为每个步骤存储完整的机器状态,而只需存储更改。它可能仍然很昂贵。