这一篇文将结合一个案例谈谈解决随机BUG的一些经验。我这里说的随机BUG是指那些你没法通过一些确定的步骤可靠地重现的BUG。我想做软件开发的人都会同意,即使不是难,随机BUG应该也是难解决的BUG类型之一。有人也许说,只要找到问题的根源一定能可靠地重现问题,不能重现只是因为你还没找到问题的根源。这话也许没错,但也还是存在一些情况,即使找到了问题的根源也没法可靠地重现,比如我碰上的这个属于这种情况。

  问题是这样的,我们是一家设备制造商,有自己定制的主板,在主板上开发系统软件(Kernel+Drivers),再在系统软件上构建应用软件-这在嵌入式系统开发中是比较典型的一种模式。我们有一款产品使用的平台是Strongarm SA1110+Windows CE 4.1(这在当年是很常见的一种解决方案)。问题出在关机时,有时候屏幕已经黑了,看起来设备已经完全下电,但其实内部主板的电还没掉,这时按开机键没有反应-系统lockup了。结果是只能拔掉AC电源和电池让主板下电然后才能重新开机。恶劣的情况是,如果用户只使用电池供电,而且他没意识到系统处于 lockup状态,过不了多久电量耗尽电池会报废。

  要找出这个问题,当然必须搞清楚系统挂在了哪里,然后才能寻找对策。因此首先搞清楚系统关机的机制。

  在Windows CE中,关机操作触发后,下电过程由电源管理模块执行,大概是这样:

  1. 广播关机消息给关注该消息的 Application和Driver;

  2. 挂起图形界面子系统(GWES);

  3. 通知所有非块设备驱动(non-block drivers,这些driver可能需要访问注册表或文件因此需要在块设备驱动停止之前处理);

  4. 给 Kernel发送IOCTL_HAL_PRESUSPEND消息(OEM开发商可以利用这一消息做一些关机前处理);

  5. 禁止除电源管理模块以外的所有其他模块使用注册表和文件系统;

  6. 通知所有块设备驱动(block drivers);

  7. 关闭图形子系统;

  8. 关闭文件系统;

  9. 调用OEMPowerOff。OEMPowerOff由OEM厂商实现,执行真正的硬件下电操作。

  Windows CE系统在关机时执行的操作还是挺多的,有必要想办法缩小问题的范围。对付这种情况一种很有效的办法是在相关模块中添加打印语句输出一些调试信息。初步调查很快发现问题出在OEMPowerOff里面,OEMPowerOff是操作系统把一切事情都做完后调用的函数,执行真正的关机操作,通常会涉及一些 外设的下电和CPU的电源状态的切换。我们这个平台的关机逻辑设计有点儿怪:关机操作在一个CPLD中实现,为防止信号干扰导致的误操作,它监控CPU的两根GPIO管脚,这两根管脚信号符合特定时序时CPLD执行关机动作。真正需要关机时,OEMPowerOff通过控制这两根GPIO管脚产生特定时序,触发CPLD的关机操作,然后CPLD依次给CPU Core和主板下电。这个设计看起来没什么问题,但是为什么会导致随机lock-up呢?我们百思不得其解。困难的地方在于这个问题极难重现,在大多数情况下关机是很正常的,反复开开关关也不见得能重现一次。如果找不到办法重现问题,解决方案当然也无从谈起-即使你有再好再合理的想法,你甚至都无法验证你的想法正确与否。另一方面,客户的反馈又表明这个问题确实存在,而且随着出货量增加,案例越来越多,不容忽视。

  对付这类随机BUG,我有一些固定的套路。首先一定要想办法把测试自动化,这样可以利用多台机器,24小时进行不间断测试,通过增大样本空间来缩短重现问题的时间。在这个例子中,开机键可以用脉冲信号发生器产生的脉冲模拟,关机动作可以在系统跑起来后运行特定程序来触发。这两个要点解决了,自动测试装置也建立起来了。

  测试的自动化虽然产生了大量的测试样本,使问题重现变得容易。但是问题的根源并不因此变得显而易见。要找出问题还必须做一些跟踪调试和测量。但是在这么底层的地方,软件级的调试器显然是没法用的,基于JTAG的硬件调试器的intrusive本质,会影响信号状态,因而也不应使用。能够利用的只能是通过示波器和逻辑分析仪直接测量管脚的信号状态。做了很多对比实验,对主板上一些信号测量的结果表明出现问题时,CPU的一些GPIO管脚状态和我们的预期不符。

  比如说有一根GPIO管脚控制着LCD背光,在关机时它显然应该处于使背光关闭的状态,但是出问题时偶尔却会处于打开状态。这似乎表明在关机时它的状态失去了控制!随后更多的实验表明不单是这只管脚,其他管脚的状态似乎也不受控制!这是一个很严重的情况,因为GPIO控制着很多重要的外围设备,如果失去控制,会导致设备的物理损害甚至危害到人身安全。

  比如有一根GPIO管脚控制Flash memory的可写性,失去控制将会导致Flash上的软件系统损坏,使整个设备变“砖”。我们测试的时候也真的出现了这种状况!由于系统关机也由 GPIO控制,看起来这个问题的根源可能在这里:GPIO在关机时失去控制,导致整个系统陷入未定义状态,关机操作无法完成。那么应该怎样避免关机时 GPIO的状态混乱?有人提出一个解决方法,在关机前把这些处于输出状态的GPIO设为输入,这样GPIO的状态由外部电路决定,也许能解决问题。这个方案貌似合理,但是实验结果表明问题依然存在。绞尽脑汁地思考,有人提出另外一个想法,GPIO状态的混乱可能是由于CPU的Core电压突然掉电引起的, 因此可以在关机之前先让CPU进入sleep mode,在sleep mode中这些输出管脚的状态可以由PGSR寄存器设定。之后再掉CPU Core电压应该没问题。后来的实验证明了这种想法是正确的,我们确实观测到了CPU Core电压在掉到1伏多的时候有一个关键的GPIO管脚状态发生了翻转,导致本该接着往下掉的CPU Core电压又升上来了,于是后面对主板下电的动作也没法执行,而且这时CPU似乎进入一种未定义状态,系统在这里挂住了。

  随后大量的自动化测试也证明了让CPU进入sleep mode是正确的解决方案。到这里解决方案已经找到,但是随着而来的问题是:CPU的内部状态不可控制,显然这个问题无法可靠重现,那你怎么验证你的解决 方案呢?换句话说,做多少次重复实验才能算通过,这个标准在哪里?100次够不够?还是1000次甚至10000次?这个问题没有标准的答案,我个人的标准是,先测出问题发生的概率水平(以我的经验,这种随机的BUG一般都有一个比较稳定的概率),解决方案的测试次数必须比这个概率高一个数量级才能算基本通过;如果测试次数能提高两个数量级,我会对这个解决方案非常有信心。

  在这个例子中,我们测出来的概率水平大约在1/2000(这种概率水平的问题在 LAB里很难发现,但是在用户那里又很容易出现)。后验证方案的测试大约做了十几万次。

  总结:解决一个问题,对技术细节不清楚不重要,重要的是要有清醒的头脑和解决问题的方向感,也是说任何时候你都应该知道下一步应该做什么。其实我在接手这个问题时,对嵌入式系统还很陌生,我甚至都不知道有脉冲发生器和逻辑分析仪这种设备,对arm架构和WINCE都一窍不通,但这并不影响我对问题的分析和思考,随着研究的深入,技术细节可以通过翻阅资料和与同事交流慢慢获得。一般来说,我解决问题的思路是这样的:对问题产生的原因进行猜测,根据猜测提出 解决方案,然后设计实验证明或者证否方案,再基于新的实验结果进行猜测,提出新的解决方案,逐步缩小范围,归纳出其中的规律。对于随机BUG,我的思路是 一定要针对问题设计自动化测试;确定问题出现的概率水平;验证解决方案的测试次数必须比问题出现的概率高至少一个数量级。