蟲趣:Win8系統Bug分析一例

作者:張佩】【原文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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章