开发人员的月光宝盒 —— 基于CPU的“时光倒流”技术畅想
一. 重现罕见问题——开发人员永远的痛
但凡做过程序调试的开发人员,一定都有过面临难以重现的问题时仰天兴叹的经历,祈祷老天赐予传说中的“月光宝盒”,从而逆转时光揪出那引发程序错误的元凶!
即使在错误跟踪技术经历长足发展的今天,哪怕是经过周密故障跟踪机制加持的代码,程序员仍然对那些难以重现的问题提心吊胆。错误跟踪机制再先进,也难以完整记录整个程序的运行轨迹和任何过去时刻的系统状态。数据覆盖的不可逆决定了程序执行的单向性,就像时间之箭,谁也不能让它转向。但时间的单向性并不意味着过去的时光不能从头来过,就像“月光宝盒”虽然不能使你回身飞上万丈绝壁,但却可以将你带回过去的某一时刻,让后面的历史重新来过。
二. 挑战时间之箭,已逝历史也能重历?
倘若我们能完整的记录系统在某一时刻的所有状态,那么回退此后的程序执行路径是否就成为可能?一旦历史得以回放,首先,必须保证回退后的程序执行路径具有唯一确定性,否则利用这一技术定位问题的希望就成了镜花水月。影响CPU执行序列确定性的因素有哪些呢?
寄存器、标志位、内存数据和IO操作(CPU的工作模式也可以看作是特殊的标志)。其中,前两者的保存可以轻松实现,而内存和IO操作就要复杂很多了。
我们知道,现代的CPU中均集成了多级的缓存(Cache),用作内存数据的暂存区。CPU指令对内存的数据写入其实都先写入了缓存中,大部分通用CPU的写缓存策略并不立即将其同步到物理内存,而是等到该数据项从缓存中排挤出后才写回到物理内存中。这种“回写(Write-back)”缓存机制正好为内存数据的回退创造了条件,后面将会详细描述我们如何对它加以利用。
最后,让我们来看看IO操作。尽管计算机中的IO操作包罗万象,涉及的器件和设备也千差万别,但最常见的IO操作离不开磁盘和输入输出设备。而后者的操作频率相对CPU的执行速度来说,实在只能算是“难得一遇”了。对于前者,磁盘的IO操作,虽然CPU本身没有能力记录其状态,但幸运的是,操作系统已经为存储设备准备好了类似于CPU内的缓存机制,不过是拿内存作为其缓存介质。于是,这个看似合情合理的设计再次为我们创造了绝好的铺垫。因为只要磁盘缓存尚未过期,从CPU的视角来看,并没有出现真正的IO操作(变成了内存读写),那么只需通过回退内存改变(包括了磁盘缓存的刷新)的改变即可间接实现磁盘IO操作的回退。事实上,由于操作系统提供的磁盘缓存通常都远大于CPU中的缓存,所以两者的缓存淘汰算法没有太大分歧的话,都不太可能出现磁盘缓存先于CPU缓存被挤出。当然,为策万全也可以让操作系统与CPU对磁盘缓存进行协同管理。
三. 铸造月光宝盒,驾驭程序的命运之轮
好了,万事俱备,就让我们来勾勒出“月光宝盒”的完整设计吧。为了表述的方便,下面均以进入“时光守护模式”的CPU为主角。
首先,我们需要设定“回退窗口”的尺寸,权且以CPU指令为单位,比如100条指令。那么CPU每执行100条指令,就产生一个硬中断,进入特殊的“快照”流程,完成下面几个步骤:
(1)保存中断现场,包括全部寄存器、标志位和内部状态。
(2)将特殊寄存器——“回退计数器”的值加一,作为本次“回退点”的唯一标识。
(3)将回退点标识写入CPU缓存队列中,并可以视作普通缓存条目一样被淘汰。但它将影响该标识前后数据缓存条目的“刷新(Renew)”机制:比最新的回退点标识更旧的数据缓存条目在“刷新”时并不删除现有的条目,而是新创建一个条目记录最近的内存数据变化。这是为了保证缓存中的数据能以回退点标识为界限分阶段回退。
OK,现在CPU可以回到先前被中断的流程继续执行了。此后重复上述流程,周而复始的记录下每一个可回退时刻的快照(可以适当限定需保存的快照数上限)。
四. 重回历史轨迹,让Bug无所遁形
当CPU或者软件层的故障捕获机制遇到一个严重异常后,就该让我们的“月光宝盒”发挥奇效了!这时候,首先找出CPU缓存中所记录最早的回退点标识,以此为界,“回写(Flush)”之前的数据缓存项(这一步不是必须的)并丢弃之后的全部缓存项,然后将CPU上下文恢复到前面保存的现场(包括全部寄存器、标志位和内部状态)。
下一个瞬间,我们已然回到了这段程序在它的历史中曾经经历过的瞬间,在继续发动CPU重现历史轨迹之前,请容我们首先准备好必要的故障定位器材,比如开启单步跟踪模式、激活内存读写监视、重定向异常截获入口…… 在可以反复重历的程序轨迹前,相信即使是初出茅庐的程序员也不会再错过任何故障定位信息了。
作为后续拓展空间,我们一方面可以通过优化CPU内部的执行序列(比如结合乱序执行)进一步降低快照动作对程序执行效率的影响;另一方面可以进一步发展出无人干预的自动故障记录和恢复机制,亦或是带有执行回溯能力的“Core Dump”,提高故障定位的便利性。
五. 前路虽多阴霾,却难掩神器之光芒
作为我们力所能及范围之外的IO操作,由于它们可能破坏回退后程序执行路径的确定性,所以在目前的机制里只能以“邪恶”的反面形象出现,每次与它们的照面会使得我们此前辛苦积累起来的全部快照失去意义,不得不再从头来过,这就意味着任何回退企图都无法超越最后一次出现IO操作的时间。虽然通过为IO增设缓存通常能缓解大部分的矛盾,但IO操作的多样化和复杂性使得进一步化解困境努力显得效果甚微。
不过瑕不掩瑜,在CPU执行速度远超IO访问速度的今天,至少从概率学的角度来看,我们所面临的大部分程序执行异常都将出现在远离IO操作的时刻,使得我们仍然有较大的回退空间去捕捉问题。
六. 后记
本文纯属带点技术色彩的调侃和畅想,疏漏、谬误和欠成熟之处在所难免,也欢迎大家加入讨论!相信技术本身总是能在辩论中变得更加清晰和透彻。