虫趣:Win8系统Bug分析一例

作者:张佩】【原文URL http://www.yiiyee.cn/Blog/win80x7e/
[download id=”3″]

有肉的地方总能发现蚊蝇。有软件的地方,就有臭虫相随(注1)。操作系统就是个大软件,所以臭虫是少不了的。最近碰到一个Windows 8的系统臭虫,报给微软并得到了确认,他们确保会在Windows Blue上解决此问题。到底是什么问题呢?今日有空,和大家一起来聊聊。

发现问题

项目过程中,测试人员发现,他们如果在Win8平台上同时播放视频并跑3D程序,连续测试一个晚上,隔天系统就可能挂掉(死亡蓝屏)。错误发生的概率很高,有时候十多个小时能做出来,有时候两三个小时就能做出来。不管怎样,只要时间够长,每台机子都能做出来。所以和硬件平台无关。

问题被报出来的时候,大家很紧张,因为如果这个问题不解决,有将近30%的测试进行不下去。更严重的是,大家直觉上认为,这应该是我们自家的问题。因为大家相信这是一个新问题,是以前的同类测试中未发现的。测试人员非常尽责地做回归(regression)实验,希望能找到一版可以正常工作的驱动,试图定位出问题驱动的版本。但结果却非常失望,他们即便使用两个月前的旧驱动,长时间测试后,仍能复现问题。

这时候大家就有点没脾气了。我们每次发布驱动之前,都会走一套非常严格的测试流程,包括了几百项压力测试。但这个十分严重的蓝屏问题,却偏偏从大家的眼皮底下溜过了。同时运行视频播放+3D程序是一项基本测试,每次必跑。但也许正因为这样,QA以前已经跑过几十遍这项测试,基于他以往的经验,觉得这项测试绝对不可能出现问题,所以就没有足够重视它,在有限的几台机器上,也许只跑了四五个小时就直接过了。

测试也是不容易的工作,需要大量的重复劳动。QA能发现一个软件臭虫是件很兴奋的事情,但问题是,少量的兴奋被巨量的重复所淹没,很容易产生疲劳,疲劳则会导致麻痹,人麻痹了,就会错过眼皮底下的东西。

BSOD 0x7E

问题最终被塞给了我,经过一番周折拿到了dump文件。立刻使用!analyze 命令简单分析一下:

0: kd> !analyze -v
 ERROR: FindPlugIns 8007007b
 ERROR: Some plugins may not be available [8007007b]
 *******************************************************************************
 * *
 * Bugcheck Analysis *
 * *
 *******************************************************************************

SYSTEM_THREAD_EXCEPTION_NOT_HANDLED (7e)
 This is a very common bugcheck. Usually the exception address pinpoints
 the driver/function that caused the problem. Always note this address
 as well as the link date of the driver/image that contains this address.
 Arguments:
 Arg1: ffffffffc0000005, The exception code that was not handled
 Arg2: fffff88003c70dd7, The address that the exception occurred at
 Arg3: fffff88005195228, Exception Record Address
 Arg4: fffff88005194a60, Context Record Address

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%08lx referenced memory at 0x%08lx. The memory could not be %s.

FAULTING_IP:
 dxgmms1!VIDMM_SEGMENT::TrimOfferList+87 [d:\w8rtm\windows\core\dxkernel\dxgkrnl\dxgmms1\vidmm\mmseg.cxx @ 2151]
 fffff880`03c70dd7 49394008 cmp qword ptr [r8+8],rax

EXCEPTION_RECORD: fffff88005195228 -- (.exr 0xfffff88005195228)
 ExceptionAddress: fffff88003c70dd7 (dxgmms1!RemoveEntryList+0x0000000000000007)
 ExceptionCode: c0000005 (Access violation)
 ExceptionFlags: 00000000
 NumberParameters: 2
 Parameter[0]: 0000000000000000
 Parameter[1]: 0000000000000008
 Attempt to read from address 0000000000000008

STACK_TEXT:
 dxgmms1!RemoveEntryList
 dxgmms1!VIDMM_SEGMENT::TrimOfferList
 dxgmms1!VIDMM_SEGMENT::TrimProcess
 dxgmms1!VIDMM_SEGMENT::ReserveResource
 dxgmms1!VIDMM_GLOBAL::AcquireGPUResourcesFromPreferredSegmentSet
 dxgmms1!VIDMM_GLOBAL::FindResourcesForOneAllocation
 dxgmms1!VIDMM_GLOBAL::FindResourcesForAllocations
 dxgmms1!VIDMM_GLOBAL::PrepareDmaBuffer
 dxgmms1!VidSchiSubmitRenderCommand
 dxgmms1!VidSchiSubmitQueueCommand
 dxgmms1!VidSchiRun_PriorityTable
 dxgmms1!VidSchiWorkerThread
 nt!PspSystemThreadStartup
 nt!KxStartSystemThread

FAULTING_SOURCE_LINE: d:\w8rtm\windows\core\dxkernel\dxgkrnl\dxgmms1\vidmm\mmseg.cxx
 FAULTING_SOURCE_FILE: d:\w8rtm\windows\core\dxkernel\dxgkrnl\dxgmms1\vidmm\mmseg.cxx
 FAULTING_SOURCE_LINE_NUMBER: 2151
 FAULTING_SOURCE_CODE:
 No source found for 'd:\w8rtm.public.amd64fre\internal\minwin\priv_sdk\inc\ntrtl_x.h'
 SYMBOL_STACK_INDEX: 0
 SYMBOL_NAME: dxgmms1!VIDMM_SEGMENT::TrimOfferList+87
 FOLLOWUP_NAME: MachineOwner
 MODULE_NAME: dxgmms1

BSOD 0x7E是在系统线程中发生了一个异常,并且这个异常没有被解决而导致的。系统线程是由system进程创建的线程,系统的很多功能模块,都运行在系统线程中。系统或者内核驱动可以通过调用DDI函数IoCreateSytemThread来创建系统线程。

从上面的自动分析中可以看到,出问题的模块式Dxgmms1.sys,出问题的指令是:

dxgmms1!VIDMM_SEGMENT::TrimOfferList+87,

对应的汇编指令是:

cmp     qword ptr [r8+8],rax

切换到问题现场后,查看r8寄存器,发现它的值是0:

0: kd> r r8
Last set context:
r8=0000000000000000

看来这是一个常见的空指针访问的Bug。我的初步判断是,r8寄存器保存的是一个结构体指针,当前指令试图通过它访问偏移为8的成员变量,并当场挂掉。出问题的这个函数位于操作系统的dxgmms1模块,它是系统用来管理Graphic内存的内核模块。

我的第一直觉是,VIDMM_SEGMENT::TrimOfferList这个函数写得不好,强壮性不够,它没有进行空指针判断。如果有判断的话,也许可以避免这个尴尬的蓝屏。但随后我又推翻了这个看法,因为根据过往的编程经验,不是所有的空指针都需要判断的,有时候一个指针永远不能为空,如果为空,就表明隐含有重大Bug。对于这种Bug,越早把问题报出越有助于问题的解决,所以尽早蓝屏是有益的。

由于我的公司是做显卡的,而我所在的team是做显卡驱动的。所以即使发现问题出在系统的Graphic模块中,也不能有丝毫的轻松,因为一种很可能的情况是底下的显卡驱动有问题,导致了上面的系统模块崩溃。

反汇编

但经过进一步的分析,发现显卡驱动的作案动机不明显,基本消除其作案可能性。但这对问题的根本解决没有太大帮助,尚有待真凶的揭示,我必须继续奋战。我的目光最后回到VIDMM_SEGMENT::TrimOfferList函数本身,有没有可能是这个函数自己的问题呢?

使用uf dxgmms1!VIDMM_SEGMENT::TrimOfferList命令打印出这个函数的全部汇编代码。我花了半天的时间,把它反汇编成了C代码,代码不多,粘贴如下(汇编代码文中省略,有兴趣的读者可通过本文开头处的链接下载研究):

1.	NT_STATUS dxgmms1!VIDMM_SEGMENT::TrimOfferList (LIST_ENTRY* ListHead)
2.	{
3.	DXGPUSHLOCK *pLock = _pVidMmGlobal->_PendingOfferListLock;
4.	NT_STATUS status = STATUS_UNSUCCESSFUL;

5.	LIST_ENTRY *pEntry = ListHead->FLink; // !!! code bug!!! should move this line to L7
6.	pLock->AcquireExclusive ();

7.	while (pEntry != ListHead)
8.	{
9.	LIST_ENTRY* pNextEntry = pEntry->FLink;
10.	VIDMM_GLOBAL_ALLOC *pAlloc = CONTAINING_RECORD (pEntry, VIDMM_GLOBAL_ALLOC, OfferListEntry);
11.	if (pAlloc->pNonPaged->OfferState == 1) 
12.	{continue;}

13.	if (pEntry != pEntry->Flink->BLink ||
14.	    pEntry != pEntry->Blink->Flink)
15.	{
16.	// assert software interrupt 29h
17.	}    

18.	pEntry->Blink->Flink = pEntry->Flink;
19.	pEntry->Flink->Blink = pEntry->Blink;
20.	pEntry->Flink = NULL; // the removed entry's Flink is cleared to NULL.

21.	if (pAlloc->state == VidMmCommited ||
22.	    pAlloc->segment != this ||
23.	    pAlloc->pNonpaged->offerState != 2)
24.	{continue;}

25.	status = TrimAllocation (...);

26.	if (!NT_SUCCESS (status))
27.	    break;
28.	}

29.	// release the lock which is contained in pLock
30.	pLock->m_pOwnerThread = NULL;
31.	KeLeaveCriticalRegion (pLock->m_PushLock);

32.	return status;
33.	}

几百行汇编代码,我用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行所对应的汇编代码,正是自动分析所指明的问题代码:

cmp     qword ptr [r8+8], rax

在把这个问题报给微软的时候,我提出了自己简单的修改方案:确保所有的链表操作都位于安全域中,把第5行代码下移两行就能解决问题。

其它

后来微软很快给出了回复,从回复来看,这个Bug在他们内部也是一个已知问题,但他们因为时间的关系,决定不在Win8系统中解决它,而把Fix放到Windows Blue中。

读者如果正在使用的是Win8操作系统,就有可能碰到这个问题。不过我分析了一下,对于个人用户,这个Bug的发生概率并不高。在测试中我们发现,单跑3D程序或视频播放,是不会有问题的。同时跑3D和视频程序,正是导致了多线程竞争的原因。而对一般用户而言,一边看电影,一边玩游戏,这种情况应该不多见。另外,由于函数本身较短(观察发现链表也总是很短),所以即便多线程同时运行的情况下,也要很长时间才能碰到引发问题的时机。

注1:软件中的错误俗称臭虫(Bug),典故解释见http://zh.wikipedia.org/wiki/Bug

7,522 total views, 2 views today

《虫趣:Win8系统Bug分析一例》有10个想法

发表评论

电子邮件地址不会被公开。