【作者:张佩】【原文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,573 total views, 5 views today
锁放在获取链表头的前一句,好像我也犯过这样的错误了。
最后是怎么发现错误的呢?
Good case!请问你是在上海AMD?
是的。同事?
不是同事,但想去AMD从事内核方面的开发,不知道有什么职位,有什么要求?
你可以投简历过来,我帮你投也可以。
好的,多谢!
In the cotlipcamed world we live in, it’s good to find simple solutions.
张老师,非常感兴趣的是,你是如何将汇编反成C代码的?
全手动反的。比较短,所以没试过用工具。