有肉的地方总能发现蚊蝇。有软件的地方,就有臭虫相随(注1)。操作系统就是个大软件,所以臭虫是少不了的。最近碰到一个Windows 8的系统臭虫, 给微软并得到了确认,他们确保会在Windows Blue上解决此问题。到底是什么问题呢日有空,和大家一起来聊聊。
发现问题
项目过程中,测试人员发现,他们如果在Win8平台上同时播放视频并跑3D程序,连续测试一个晚上,隔天系统就可能挂掉(死亡蓝屏)。错误发生的概率很高,有时候十多个小时能做出来,有时候两三个小时就能做出来。不管怎样,只要时间够长,每台机子都能做出来。所以和硬件平台无关。
问题被 出来的时候,大家很紧张,因为如果这个问题不解决,有将近30%的测试进行不下去。更严重的是,大家直觉上认为,这应该是我们自家的问题。因为大家相信这是一个新问题,是以前的同类测试中未发现的。测试人员非常尽责地做回归(regression)实验,希望能找到一版可以正常工作的驱动,试图定位出问题驱动的版本。但结果却非常失望,他们即便使用两个月前的旧驱动,长时间测试后,仍能复现问题。
这时候大家就有点没脾气了。我们每次发布驱动之前,都会走一套非常严格的测试流程,包括了几百项压力测试。但这个十分严重的蓝屏问题,却偏偏从大家的眼皮底下溜过了。同时运行视频播放+3D程序是一项基本测试,每次必跑。但也许正因为这样,QA以前已经跑过几十遍这项测试,基于他以往的经验,觉得这项测试绝对不可能出现问题,所以就没有足够重视它,在有限的几台机器上,也许只跑了四五个小时就直接过了。
测试也是不容易的工作,需要大量的重复劳动。QA能发现一个软件臭虫是件很兴奋的事情,但问题是,少量的兴奋被巨量的重复所淹没,很容易产生疲劳,疲劳则会导致麻痹,人麻痹了,就会错过眼皮底下的东西。
BSOD 0x7E
问题最终被塞给了我,经过一番周折拿到了dump文件。立刻使用!analyze 命令简单分析一下:
BSOD 0x7E是在系统线程中发生了一个异常,并且这个异常没有被解决而导致的。系统线程是由system进程创建的线程,系统的很多功能模块,都运行在系统线程中。系统或者内核驱动可以通过调用DDI函数IoCreateSytemThread来创建系统线程。
从上面的自动分析中可以看到,出问题的模块式Dxgmms1.sys,出问题的指令是:
对应的汇编指令是:
切换到问题现场后,查看r8寄存器,发现它的值是0:
看来这是一个常见的空指针访问的Bug。我的初步判断是,r8寄存器保存的是一个结构体指针,当前指令试图通过它访问偏移为8的成员变量,并当场挂掉。出问题的这个函数位于操作系统的dxgmms1模块,它是系统用来管理Graphic内存的内核模块。
我的第一直觉是,VIDMM_SEGMENT::TrimOfferList这个函数写得不好,强壮性不够,它没有进行空指针判断。如果有判断的话,也许可以避免这个尴尬的蓝屏。但随后我又推翻了这个看法,因为根据过往的编程经验,不是所有的空指针都需要判断的,有时候一个指针永远不能为空,如果为空,就表明隐含有重大Bug。对于这种Bug,越早把问题 出越有助于问题的解决,所以尽早蓝屏是有益的。
由于我的公司是做显卡的,而我所在的team是做显卡驱动的。所以即使发现问题出在系统的Graphic模块中,也不能有丝毫的轻松,因为一种很可能的情况是底下的显卡驱动有问题,导致了上面的系统模块崩溃。
反汇编
但经过进一步的分析,发现显卡驱动的作案动机不明显,基本消除其作案可能性。但这对问题的根本解决没有太大帮助,尚有待真凶的揭示,我必须继续奋战。我的目光最后回到VIDMM_SEGMENT::TrimOfferList函数本身,有没有可能是这个函数自己的问题呢/p>
几百行汇编代码,我用A4纸,打印了整整6页。反汇编出来的C代码却只有二三十行。这个函数的逻辑非常简单明了,我看了好几遍之后,没有发现任何问题。它使用一个内部锁保护一个双向链表,并把链表中符合条件的Entry删除掉。
残缺的锁
正当没有出路可想的时候,机会却来了。中午吃饭时,在往食堂去的路上和同事讨论这个问题,同事的一句话点拨了我:注意多线程的情况。午饭回来继续看这段代码,这次考虑到多线程安全,我很快发现了代码中存在的问题。原来这虽然是一段逻辑简单的代码,却藏着一个糟糕的Bug!
看看它是怎么使用锁的的基本逻辑是,在第6行获取锁,获取成功后操作链表,在第31行将锁释放。我们把6-30行之间的这个区域,看成是安全域,在这个安全域里面操作链表是安全的。可问题出在第5行,它在这个安全域之外,获取了链表头,并随后使用。
想象多线程的情况。A/B两个线程同时调用TrimOfferList函数并操作同一个链表,A线程先到并得到锁,运行到第7行。这时候B线程也到了,它先取了链表头保存在局部变量pEntry中,运行到第6行的时候,因为锁已经被A线程获取,所以等在那里。A线程继续运行,它把链表中符合条件的Entry统统删掉,在第20行的地方,它把删掉的Entry的Flink指针清零。出问题的时候,链表头是符合条件的Entry,也被A线程删除掉了。A线程操作完之后释放锁并退出这个函数。这时候B线程继续进场,此时它手里拿着的pEntry这个指针其实已经失效,是被删除掉的链表头指针。在第13行,它使用了pEntry->Flink这个被清零的指针,并试图获取这个指针8个字节偏移处的Blink成员变量的值,造成了空指针应用,从而立即引爆炸弹。
第13行所对应的汇编代码,正是自动分析所指明的问题代码:
在把这个问题 给微软的时候,我提出了自己简单的修改方案:确保所有的链表操作都位于安全域中,把第5行代码下移两行就能解决问题。
其它
后来微软很快给出了回复,从回复来看,这个Bug在他们内部也是一个已知问题,但他们因为时间的关系,决定不在Win8系统中解决它,而把Fix放到Windows Blue中。
读者如果正在使用的是Win8操作系统,就有可能碰到这个问题。不过我分析了一下,对于个人用户,这个Bug的发生概率并不高。在测试中我们发现,单跑3D程序或视频播放,是不会有问题的。同时跑3D和视频程序,正是导致了多线程竞争的原因。而对一般用户而言,一边看电影,一边玩游戏,这种情况应该不多见。另外,由于函数本身较短(观察发现链表也总是很短),所以即便多线程同时运行的情况下,也要很长时间才能碰到引发问题的时机。
注1:软件中的错误俗称臭虫(Bug),典故解释见http://zh.wikipedia.org/wiki/Bug
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!