调研是一门学问,但是我并不觉得我非常擅长。过去,我没有立志于成为一个研究性的程序员,实践对于我来说更有感觉。只是呢,随着编程年轮的一圈一圈地增长,研究性的开发也变成一个不可缺少的日常活动。虽也说不上是每日必备的活动,但是呢,每隔几天、向周也得做一些相关性的研究。
调研成为日常活动时,那不可避免的,我们也会发现一些研究的手法。尽管,我想总结一些相关的模式,但是对于我来说,时机还不够成熟,我也缺乏相关的经验。
调研是一门艺术。
同样的,起因也是项目的缘故,话题是 Time Travel Debugging,也被译为时间旅行调试。所以,从这次的经验来看,我把过程分为这么几部分:
概念定义。即什么是时间旅行调试
资料收集。
流程模式。
抽象概念要素。即从上一步中抽象所需要的相关关键要素
原型设计。
看上去平淡无奇,和普通的技术研究没啥两样。而本文所针对的场景同是,大家都研究得相对较少的领域。
定义概念本身可以分为两部分:
收集其他人对于这个概念的定义。
抽象出自己对于概念的理解。
持续完善这个概念的定义。
万事就是得这么开头易。定义概念有几个好出处:
最好的出处是『*的英文版』,一个世界上大部分国家都能正常访问的知识网站。*有一个重定义的功能,还能帮助我们找到正确的用语。*有一个非常大的特点:啰嗦。
其次是 Google 索引,就是通过 Google 来找到热门的、大部分情况下可信的介绍网站。
所以,我们先简单引用微软文档的定义(机翻版):
Time Travel Debugging 是一种工具,它使您可以记录(record)正在运行的进程的执行情况,然后在以后向前和向后重放(replay)它。Time Travel Debugging(TTD)通过让您“倒带”(rewind)调试器会话,来帮助您更轻松地调试问题,而不必在发现错误之前重现问题。
我刚看到介绍的时候觉得平谈无奇,直到写这篇文章的时候,我发现了隐藏了关键字:record-replay,于是先在这里提醒一下。
然后呢,*上来了一个更详细的定义:
时间旅行调试是通过源码在时间上的倒退,以了解在执行计算机程序期间发生的事情的过程。
还有对应的说明:
通常来说,调试和调试器是帮助用户进行调试过程的工具,允许用户暂停正在运行的软件的执行并检查程序的当前状态。而后,用户可以及时前进,进入或跳过语句,然后向前执行。而交互(Interactive)式调试器呢,则包括修改代码并根据更新的信息前进的功能。反向(Reverse)调试工具,使用户可以在时间上向后退,以逐步达到程序中的特定点。时间旅行调试器提供了这些功能,还允许用户与程序交互,如果需要,可以更改历史记录,并观察程序如何响应。
从结论上来说,*给了概念上的定义,而微软的文档则是侧重于实现方式上的定义。这样一结论,我们就得到了简单的结论:
时间旅行调试是一种软件开发的调试方式,通过将时间与源码关联,来让开发者了解程序运行期间发生的变化。它记录(record)下了程序在不同时间的状态,以便于在调试时可以向前和向后重放(replay)状态,来展示程序的运行情况。
从理论上来说,这一步并不是过于复杂,套路都很简单,常见的来源有:
*。*的概念上一般都会有对应的实现示例。
论文。来源虽多,但是对于我来说,我习惯于 Google Scholar,可以下载。
论文网络。通过论文的 Related Works 和 References,扩大搜索范围,然后借助于 Google Scholar 的被引用数来判定
社交网络。
书和网络。
*给了一些相应的示例调试器:
Elm Debugger
Elm Reactor
Meiosis Tracer
Microsoft Time Travel Debugging (TTD) Tool for native Windows software (x86, x64, ARM, ARM64)
ocamldebug for OCaml
UDB for Linux
rr for x86 Linux
provDebugR for R
Wallaby.js for JavaScript
RevDeBug for C# and Java
WhyLine for Java
直接进行相关的搜索,然后阅读,如:
Expositor: Scriptable time-travel debugging with first-class traces
Tardis: Affordable time-travel debugging in managed runtimes
……
有意思的事情是,我找到了一个作者写的大量相关论文。即微软的 Mark Marron,写的相关论文(主要是由第一篇看到的,然后搜索作者的相关论文)
Time-Travel Debugging for JavaScript/Node.js
A Gray Box Approach For High-Fidelity, High-Speed Time-Travel Debugging
TARDIS: Affordable Time-Travel Debugging in Managed Runtimes
……
所以,我便深入研究了相关的论文和作品,然后就中奖了 —— 发现了一种解决方案:
通过查找论文 References 及对应的 Related Work,我又找到了一系列的论文。诸如于:
Framework for Instruction-level Tracing and Analysis of Program Executions
Effcient Algorithms for Bidirectional Debugging
ReVirt - Enabling Intrusion Analy- sis Through Virtual-Machine Logging and Replay
……
这一点对于那些搞学术的人来说,应该算是比较常见的。
对于诸如时间旅行调试这一类属学术上的事物。并不能像其它领域,可以通过阅读书的方式来解决,但是搜索成本点高。所以,我并没有怎么尝试去找。
一次偶然的机会,我在知乎上搜索了 Time Travel Debugging,然后看到了『存在实现了后退功能的调试器吗?这种功能在实现上有什么难点呢?』这个问题, 又搜索到一波资料。
Visual Studio:IntelliTrace
GDB:GDB and Reverse Debugging
Java:Chronon | DVR for Java
.NET:RevDeBug Blog: What Is Reverse Debugging?
Chakra / ChakraCore:JavaScript Time-Travel Debugger - Microsoft Research
Firefox:WebReplay
用于 C/C++ 的 rr
用于 QEMU 交互运行时分析的 Qira
C++ 下的 UndoDB
用于 Node.js 的 Wallaby.js
用于 React 的 Redux
然而,大部分的意义不大 —— 并非开源,还得自己反向编译。
对于工程师而言,我们读论文的目的嘛,不就是为了知晓他们是如何解决问题的。所以,我更关注于它实现这些问题的模式。这些会在论文中进行大致的介绍,我们只需要有耐心阅读就可以了。
如『TARDIS: Affordable Time-Travel Debugging in Managed Runtimes』 中介绍的实施 TTD 系统的标准方法是:
定期捕获程序状态的快照
记录快照之间发生的所有不确定的环境交互,例如控制台 I/O 或计时器事件。
基于此,在反向执行时,首先还原在反向执行目标之前的最接近的快照,然后从该快照重新执行,从而重放环境和与幂等的环境写入之间的交互,以达到目标。
对应的
又或者是『Framework for Instruction-level Tracing and Analysis of Program Executions』中引入的方式,
我们的框架由两个主要组件组成:一个称为 Nirvana 的运行时引擎和一个称为 iDNA(使用 Nirvana 的诊断基础设施)的跟踪记录和检索工具。
Nirvana。运行时引擎结合使用动态二进制翻译和解释来模拟目标机器的指令集。在模拟过程中,它向客户端应用程序插入回调,该回调记录的信息,足以在以后重新模拟应用程序的执行。记录的信息可以包括正在执行的 guest 指令的属性(例如,指令的地址、地址以及内存位置的读写值(如果是内存访问指令)、模块加载、异常、线程创建等事件,它还可以包括有关符号信息的位置信息(如果可用)。
iDNA 由两个主要组件组成:iDNA Trace Writer 在跟踪记录期间(trace recording)使用,iDNA Trace Reader ,在从记录的跟踪重新模拟期间使用。
对应的一些关键代码的设计:
Event nameDescriptionTranslationEventwhen a new instruction is translatedSequencingEventstart of a sequence point and other special eventsInstructionStartEventstart of every instructionMemReadEventmemory readMemWriteEventmemory writeMemRefEventmemory referenceFlowChangeEventflow control instruction (branch, call, ret)CallRetscalls and returnsDllLoadEventload of a new module
相似的,还有其它不同的模式,由于篇幅原因,这里就不展开介绍了。
走马观花式的阅读了一系列论文之后,我大概有了如何设计一个系统的思路了。
首先,让我们来看一下所有的一些关键信息:
语言类型:脚本语言、编译型原生语言、编译型虚拟机语言等
创建快照时机:编译时、转译时、编译后等
操作级别:指令级、代码级、虚拟机、操作系统等(PS:从理论上来说,应该还 MIR)
关键要素:Recording / Replay、Checkpoint / Snapshots 等
注意事项:I/O、Network、Filesystem、Event 等
其它:GC 算法、快速管理、缓存管理等
因为它需要根据具体的场景来做出选择,所以便只是简单的逻列一下。
如『Framework for Instruction-level Tracing and Analysis of Program Executions』中的示例
Original(guest) code:
mov eax,[ebp+4]
Translated(host) code:
mov esi, nirvContext._ebp
add esi, 4; callee savedregister
mov edx, esi ; 2ndargument → address of memory accessed
mov ecx, nirvContext ; 1stargument → pointer toNirvContext
push 4;3rdargument → number of bytes accessed
call MemRefCallback; calling convention assumes ecxandedx hold1stand2ndargument
mov eax, [esi] ; simulate intended memory read
mov nirvContext._eax, eax ; update NirvContextwithnewvalue
有了论文,阅读了相关的源码之后,我大概有了一个思路:
通过回调的思路,在运行时收集应用的状态信息
针对特殊的事件、IO、网络等,进行特殊的记录和缓存
在编译时,对代码运行转换
回放时,根据指令返回相应的结果
当然了 Demo 已经写好,只是呢,因为某种原因不方便贴在这里。
我一直在寻找一种方式,以系统性的记录对于某一领域的调研,这一篇文章相当于作为一个开始。