GDB 有一个支持反向调试的新版本(参见http://www.gnu.org/software/gdb/news/reversible.html)。我想知道这是如何工作的。
要使反向调试工作,在我看来,您需要存储整个机器状态,包括每个步骤的内存。这将使性能变得异常缓慢,更不用说使用大量内存了。这些问题是如何解决的?
GDB 有一个支持反向调试的新版本(参见http://www.gnu.org/software/gdb/news/reversible.html)。我想知道这是如何工作的。
要使反向调试工作,在我看来,您需要存储整个机器状态,包括每个步骤的内存。这将使性能变得异常缓慢,更不用说使用大量内存了。这些问题是如何解决的?
我是一名 gdb 维护者,也是新反向调试的作者之一。我很乐意谈谈它是如何工作的。正如一些人推测的那样,您需要保存足够的机器状态以便以后恢复。有多种方案,其中一种是简单地保存每条机器指令修改的寄存器或内存位置。然后,要“撤消”该指令,您只需恢复这些寄存器或内存位置中的数据。
是的,它很贵,但是现代 CPU 速度如此之快,以至于当您进行交互时(执行步进或断点),您并没有真正注意到它。
注意一定不要忘记使用模拟器、虚拟机和硬件记录器来实现逆向执行。
实现它的另一个解决方案是跟踪物理硬件上的执行,例如 GreenHills 和 Lauterbach 在其基于硬件的调试器中所做的。基于每条指令动作的固定轨迹,您可以通过依次移除每条指令的影响来移动到轨迹中的任何点。请注意,这假定您可以跟踪影响调试器中可见状态的所有事物。
另一种方法是使用检查点 + 重新执行方法,该方法由 VmWare Workstation 6.5 和 Virtutech Simics 3.0(及更高版本)使用,并且似乎与 Visual Studio 2010 一起提供。在这里,您使用虚拟机或模拟器获得系统执行的一定程度的间接性。您定期将整个状态转储到磁盘或内存中,然后依靠模拟器能够确定性地重新执行完全相同的程序路径。
简而言之,它的工作原理是这样的:假设您在时间 T 执行系统。要到达时间 T-1,您从点 t < T 拾取一些检查点,然后执行 (Tt-1) 循环以结束您所在位置的前一个循环。这可以很好地工作,甚至适用于执行磁盘 IO、由内核级代码组成并执行设备驱动程序工作的工作负载。关键是要有一个包含整个目标系统及其所有处理器、设备、存储器和 IO 的模拟器。有关更多详细信息,请参阅gdb 邮件列表和 gdb 邮件列表上的讨论。我自己经常使用这种方法来调试棘手的代码,尤其是在设备驱动程序和早期操作系统启动中。
另一个信息来源是关于检查点的 Virtutech 白皮书(我写的,完全公开)。
在 EclipseCon 会议期间,我们还询问了他们如何使用Chronon Debugger for Java 来做到这一点。那个不允许您实际退后一步,但可以以一种感觉就像反向调试的方式回放记录的程序执行。(主要区别在于您不能在 Chronon 调试器中更改正在运行的程序,而在大多数其他 Java 调试器中可以这样做。)
如果我理解正确的话,它会操纵正在运行的程序的字节码,从而记录程序内部状态的每一次变化。外部状态不需要额外记录。如果它们以某种方式影响您的程序,那么您必须有一个与该外部状态匹配的内部变量(因此该内部变量就足够了)。
在播放期间,他们基本上可以根据记录的状态变化重新创建正在运行的程序的每个状态。
有趣的是,状态变化比第一眼预期的要小得多。因此,如果您有条件“if”语句,您会认为您至少需要一位来记录程序是采用 then 语句还是 else 语句。在许多情况下,您甚至可以避免这种情况,例如那些不同的分支包含返回值的情况。然后只记录返回值(无论如何都需要)并从返回值本身重新计算有关已执行分支的决策就足够了。
尽管这个问题很老,但大多数答案也很老,而且由于反向调试仍然是一个有趣的话题,我发布了 2015 年的答案。我的硕士论文的第 1 章和第 2 章,结合逆向调试和实时编程实现计算机编程中的可视化思维,涵盖了逆向调试的一些历史方法(尤其侧重于快照(或检查点)和重放方法),以及解释了它和无所不知的调试之间的区别:
计算机在某种程度上已经向前执行了程序,应该真的能够为我们提供有关它的信息。这种改进是可能的,并且可以在所谓的无所不知的调试器中找到。它们通常被归类为反向调试器,尽管它们可能更准确地描述为“历史记录”调试器,因为它们仅在执行期间记录信息以供以后查看或查询,而不是允许程序员在执行程序中实际及时倒退. “无所不知”来自这样一个事实,即程序的整个状态历史已被记录,在执行后可供调试器使用。这样就不需要重新运行程序,也不需要手动代码检测。
基于软件的全知调试始于 1969 年的 EXDAMS 系统,当时它被称为“调试时间历史回放”。GNU 调试器 GDB 自 2009 年以来一直支持无所不知的调试,具有“进程记录和回放”功能。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两者都是基于跟踪/重放的,但我不确定它们在功能和性能方面的比较。
下面是另一个名为 ODB 的反向调试器的工作原理。提炼:
全能调试是在程序中的每个“兴趣点”(设置值、进行方法调用、抛出/捕获异常)收集“时间戳”,然后允许程序员使用这些时间戳来探索该程序运行的历史。
ODB ...在加载程序的类时将代码插入到程序的类中,并且在程序运行时记录事件。
我猜 gdb 以同样的方式工作。
反向调试意味着您可以反向运行程序,这对于追踪问题的原因非常有用。
您不需要存储每个步骤的完整机器状态,只需存储更改。它可能仍然相当昂贵。