【作者:張佩】【原文URL: http://www.yiiyee.cn/Blog/win80x7e/】
有肉的地方總能發現蚊蠅。有軟件的地方,就有臭蟲相隨(注1)。操作系統就是個大軟件,所以臭蟲是少不了的。最近碰到一個Windows 8的系統臭蟲,報給微軟並得到了確認,他們確保會在Windows Blue上解決此問題。到底是什麼問題呢?今日有空,和大家一起來聊聊。
發現問題
項目過程中,測試人員發現,他們如果在Win8平臺上同時播放視頻並跑3D程序,連續測試一個晚上,隔天系統就可能掛掉(死亡藍屏)。錯誤發生的概率很高,有時候十多個小時能做出來,有時候兩三個小時就能做出來。不管怎樣,只要時間夠長,每臺機子都能做出來。所以和硬件平臺無關。
問題被報出來的時候,大家很緊張,因爲如果這個問題不解決,有將近30%的測試進行不下去。更嚴重的是,大家直覺上認爲,這應該是我們自家的問題。因爲大家相信這是一個新問題,是以前的同類測試中未發現的。測試人員非常盡責地做迴歸(regression)實驗,希望能找到一版可以正常工作的驅動,試圖定位出問題驅動的版本。但結果卻非常失望,他們即便使用兩個月前的舊驅動,長時間測試後,仍能復現問題。
這時候大家就有點沒脾氣了。我們每次發佈驅動之前,都會走一套非常嚴格的測試流程,包括了幾百項壓力測試。但這個十分嚴重的藍屏問題,卻偏偏從大家的眼皮底下溜過了。同時運行視頻播放+3D程序是一項基本測試,每次必跑。但也許正因爲這樣,QA以前已經跑過幾十遍這項測試,基於他以往的經驗,覺得這項測試絕對不可能出現問題,所以就沒有足夠重視它,在有限的幾臺機器上,也許只跑了四五個小時就直接過了。
測試也是不容易的工作,需要大量的重複勞動。QA能發現一個軟件臭蟲是件很興奮的事情,但問題是,少量的興奮被巨量的重複所淹沒,很容易產生疲勞,疲勞則會導致麻痹,人麻痹了,就會錯過眼皮底下的東西。
BSOD 0x7E
問題最終被塞給了我,經過一番周折拿到了dump文件。立刻使用!analyze 命令簡單分析一下:
!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