【问题标题】:How does reverse debugging work?反向调试如何工作?
【发布时间】:2009-09-24 08:35:34
【问题描述】:

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

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

【问题讨论】:

  • 我想你可以通过存储状态增量而不是整个状态来解决问题,但它似乎仍然可能很昂贵。
  • 保存增量确实可以很好地工作,并且对于高效的全系统可逆解决方案确实是必要的。

标签: reverse-debugging


【解决方案1】:

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

是的,它很贵,但是现代 CPU 速度如此之快,以至于当您进行交互时(执行步进或断点),您并不会真正注意到它。

【讨论】:

  • 但是反向调试只允许您回滚您键入的nextstep 命令,还是允许您撤消任意数量的指令?例如,如果我在一条指令上设置断点并让它一直运行到那时,我是否可以回滚到上一条指令,即使我跳过了它?
  • > 但是反向调试是否只允许您回滚您键入的下一步和步骤命令,或者它是否允许您撤消任意数量的指令您可以撤消任意数量的指令。您不限于,例如仅在您前进时停止的点停止。您可以设置一个新断点并向后运行 > 例如,如果我在一条指令上设置一个断点并让它运行到那时,我是否可以回滚到上一条指令,即使我跳过它是只要你在你跑到断点之前打开录制模式
  • 抱歉没有格式化的文本,不知道这是怎么回事。
  • 我担心反向调试会浪费时间,让我们回到 60 或 70 年代。我不想再穿喇叭裤,再留长头发。
  • 还有那些在操作系统中修改状态的系统调用?这只是无法正常工作吗?当它修改一个不透明的句柄时呢?
【解决方案2】:

注意一定不要忘记使用模拟器、虚拟机、硬件记录器来实现逆向执行。

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

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

简单来说,它的工作原理是这样的:假设你在时间 T 正在执行一个系统。要进入时间 T-1,您从点 t the gdb mailinglist 和 gdb 邮件列表上的讨论。我自己经常使用这种方法来调试棘手的代码,尤其是在设备驱动程序和早期操作系统启动中。

另一个信息来源是Virtutech white paper on checkpointing(我写的,完全公开)。

【讨论】:

  • 另请参阅 jakob.engbloms.se/archives/1547 及其以下两篇博客文章,以更全面地了解反向调试技术。
  • “设置保存点”而不是执行反向步进的能力怎么样。因此,您进行调试,并且在某些时候您可以选择当前步骤作为“保存点”,稍后您可以跳回该保存点并再次前进,如有必要,可以编辑您的变量。有点像虚拟机的“快照”,或操作系统的“还原点”。
【解决方案3】:

在 EclipseCon 会议期间,我们还询问了他们如何使用 Chronon Debugger for Java 做到这一点。该功能不允许您实际上后退一步,但可以以一种感觉类似于反向调试的方式回放记录的程序执行。 (主要区别在于您不能在 Chronon 调试器中更改正在运行的程序,而在大多数其他 Java 调试器中可以这样做。)

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

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

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

【讨论】:

    【解决方案4】:

    虽然这个问题很老,但大多数答案也很老,而且 仍然是一个有趣的话题,我发布了一个 2015 年的答案。我的理学硕士论文Combining reverse debugging and live programming towards visual thinking in computer programming 的第 1 章和第 2 章介绍了逆向调试的一些历史方法(特别关注快照(或检查点)和重放方法),并解释了它与全知调试之间的区别:

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

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

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

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

    【讨论】:

      【解决方案5】:

      内森·费尔曼写道:

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

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

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

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

      【讨论】:

      • 任何反向解决方案的关键部分是您在某个时间点将其打开,并且只能反向到该点。没有任何魔法可以让机器真正反向运行并找出之前发生的事情,而无需对发生的事情进行某种记录。
      【解决方案6】:

      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 接口,是一个很好的替代品
      • 大多数程序的性能下降很小,我自己没有进行测量就没有注意到
      • 生成的跟踪在磁盘上很小,因为只记录了很少的非确定性事件,到目前为止我从来不用担心它们的大小

      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 我不确定,但很可能。首先尝试将 ddd 连接到 gdbserver。如果可行,它也应该与 rr 一起使用。
      • @spraff 但是,不要使用 ddd,使用 gdb 仪表板 ;-) stackoverflow.com/questions/10115540/gdb-split-view-with-code/… 这肯定会起作用,因为它只是普通的 GDB。
      【解决方案7】:

      Here 是另一个名为 ODB 的反向调试器的工作方式。摘录:

      无所不知的调试是 在每个地方收集“时间戳” “兴趣点”(设置一个值, 进行方法调用, 抛出/捕获异常)在 程序,然后允许 程序员使用这些时间戳来 探索该计划的历史 运行。

      ODB ...插入 代码到程序的类中 它们被加载并且当程序 运行,事件被记录。

      我猜 gdb 的工作方式相同。

      【讨论】:

      • 那么这是否需要代码中的指令来告诉编译器和调试器这些有趣的点在哪里?
      • 没有。 www.LambdaCS.com/debugger/debugger.html 上有一个 Java Web Start 演示,它向您展示了它是如何工作的。它看起来像一个普通的程序。反正就是ODB,不知道gdb。不过这很酷:)
      • 请注意,gdb 解决方案不会以任何方式更改目标程序。如果您必须对程序进行调试以对其进行调试,则很有可能由于时间差异和其他干扰而使问题消失。所有商业 revexec 工具都基于某种形式的外部记录,不会改变程序本身的代码。
      • @jakobengblom2:我认为您过于强调通过写入内存来更改目标、模拟执行或简单地添加硬件断点之间的区别。他们都改变时间。事实上,目标检测可能改变时间最少。
      【解决方案8】:

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

      您不需要存储每个步骤的完整机器状态,只需存储更改。估计还是挺贵的。

      【讨论】:

      • 我明白了,但您仍然需要在每次更改时中断执行以保存更改。
      • 是的,没错,但现在机器速度相当快,从人类的角度来看,我不认为减速是无法忍受的。它与 valgrind 相当,可能不如 valgrind 慢。
      猜你喜欢
      • 2018-08-14
      • 1970-01-01
      • 2017-03-11
      • 1970-01-01
      • 2010-09-18
      • 2017-07-24
      • 1970-01-01
      • 2022-10-03
      相关资源
      最近更新 更多