那些年,我在游戏开发中改过的bug:坑爹的Vista与中间件
繼續(xù)說那些奇葩的Bug。
靠不住的系統(tǒng)組件:Vista和Speech Recogition
我們游戲使用了語音識別,使用了DirectX里面的XAudio來采集聲音。Windows Vista里面有一個語音識別的組件,啟動那個程序,然后玩我們的游戲,玩了一段時間,因?yàn)闆]有使用麥克風(fēng),那個程序會自動關(guān)閉使用麥克風(fēng),然后我們的游戲就Crash了。又是一個跨進(jìn)程的奇葩Bug。
從現(xiàn)場來看,Crash在使用XAudio的庫代碼里面。看不出什么線索,最后發(fā)現(xiàn)在日志里面,Crash前一會兒,XAudio的dll被Unload掉了。Vista那個語音組件很強(qiáng)悍,也會跨進(jìn)程追殺大法,在自己進(jìn)程把我的進(jìn)程dll給Unload掉了。
雖然遇事先要懷疑自己的代碼有問題,但這個情況太匪夷所思,所以我試圖推卸責(zé)任,我跑了幾個其他DirectX程序和Demo,但凡用到XAudio組件的,在Vista Speech Recognition這一大招前無一幸免,全部Crash。
問題不在我們這里,但是作為一個職業(yè)殺蟲人,我有著“只解沙場為國死,何須bug裹尸還”的覺悟,決定還是要想辦法搞定它。
我猜這個組件退出的時候,廣播了點(diǎn)什么消息,讓系統(tǒng)所有進(jìn)程去卸載這個Dll。我找了一個微軟提供的庫Detours Express,專做Hook API的勾當(dāng)。它會反匯編API的入口代碼,然后動態(tài)替換入口,插入jmp指令去執(zhí)行我們自己的函數(shù)。先看了文檔,搞懂這個東西怎么用,然后猜測是一個OS內(nèi)部的消息導(dǎo)致Unload DLL,我們就攔截了RegisterWindowMessage。一通翻箱倒柜,啥有意義的東西都沒發(fā)現(xiàn)。
再用spy++來攔截消息,發(fā)現(xiàn)有一個MSUIM.msg.rpcsendreceive的消息面目猙獰,很可疑。就在收到這個消息以后,Dll就被Unload了。于是我們用Detours截住這個消息,直接返回。
以為搞定了,但結(jié)果還是不行,有幾條別的消息也會觸發(fā)Unload Dll,我一一用Detours攔截返回。攔截的消息越來越多,看來不是個解決辦法。于是我懷疑我們沒有攔截到正確的消息,懷疑OS啟動的時候便注冊了一堆內(nèi)部用的消息,而不是在運(yùn)行進(jìn)程的時候才注冊。那些消息里肯定有我想要的,我便異想天開地Hook了User32.dll,攔截下所有消息,在安全模式下把修改過的user32.dll覆蓋原來的文件,然后滿懷希望的重啟動Vista的時候。結(jié)果不出所料的藍(lán)屏了...User32.dll是凡人能隨便碰的嗎?這不是一個User friendly的庫,應(yīng)該改名叫God32.dll。
這個bug搞了1周,最后才想到最基本的道理,說不定只是Dll內(nèi)部引用計數(shù)被清掉了,所以系統(tǒng)就卸掉Dll了。當(dāng)Vista語音組件Unload XAudio的時候,系統(tǒng)去看看XAudio庫有沒有什么別人引用,結(jié)果發(fā)現(xiàn)居然沒有,那就順手清理了。就像地上有100塊錢,你東張西望,發(fā)現(xiàn)沒有人宣布擁有那張錢,沒有一點(diǎn)點(diǎn)猶豫,你隨手就把它放進(jìn)了褲兜,進(jìn)行了回收。嗯,就是這樣的,我喜歡比喻這種修辭手法。
理論上創(chuàng)建XAudio設(shè)備的時候沒有做什么額外的加Dll引用計數(shù)的事情,懷疑是MS的bug,創(chuàng)建D3D設(shè)備、DInput設(shè)備的時候都不需要做什么特別的事情的,但他們的引用計數(shù)都沒有問題。
于是我惡搞了一下,在初始化XAudio完成以后,手動做了一次LoadLibrary,把XAudio2_1.dll強(qiáng)行Load一遍,相當(dāng)于自行增加了引用計數(shù)。于是再無Crash。
從此我們的游戲,在Vista 32上過上了幸福的生活。
可得結(jié)論:Vista靠不住
?
性能的迷思
臨近最終版本發(fā)布的時候,測試人員發(fā)現(xiàn)一個問題,當(dāng)在Vista上連續(xù)打游戲過7關(guān)以后,游戲幀數(shù)一下子變成原來一半了。大家覺得很詭異,都不相信,之前整天玩游戲都沒問題的,于是責(zé)令測試人員再重現(xiàn)幾次,否則拖出去刨坑埋了。我也沒當(dāng)回事,繼續(xù)調(diào)試別的bug。結(jié)果他們測了幾遍都是這種情況,看來我必須花點(diǎn)力氣看看了。
先編譯了一個Profile版本,有大量的Profile代碼,可以用我們自己做的工具看Profile結(jié)果。跑游戲,一個小時后順利重現(xiàn)之,按下快捷鍵,截下一段性能分析數(shù)據(jù),打開工具就開始分析。顯示結(jié)果是在某個線程的聲音處理函數(shù)里面特別慢,占了20多ms,找來做聲音的程序員,讓他也去幫忙看。自己繼續(xù)研究,發(fā)現(xiàn)那個聲音函數(shù)簡單,估計最多1-2ms了,絕對不會有問題的。
于是開始懷疑多線程的問題,之前有過類似bug,在加載關(guān)卡的最后一小段時間,音樂和語音會很卡,音頻程序員查不出原因。后來我發(fā)現(xiàn)聲音線程根本分配不出內(nèi)存,聲音線程的內(nèi)存分配器和主線程共享,通過Critical Section保護(hù)著。聲音線程進(jìn)不了Critical Section,因?yàn)橹骶€程那時忙于加載,正在瘋狂地分配內(nèi)存,導(dǎo)致聲音線程被無法分配所需資源。我又不能調(diào)低主線程的優(yōu)先級,否則加載速度會受到很大影響。最后解決方案是為聲音線程單獨(dú)開了一個內(nèi)存分配器,不和主線程搶了。會不會是這個分配器有問題呢?我驗(yàn)證了很久,把那個聲音線程的內(nèi)存分配器換成絕無性能問題的版本,還是有問題,估計不是內(nèi)存分配的問題。
也許是那個很慢的聲音處理函數(shù)里面有些資源被別的線程占了,游戲賬號轉(zhuǎn)讓導(dǎo)致在那里傻等,最后耽誤了系統(tǒng)所有線程的同步,使幀數(shù)下降。有了想法我就開始驗(yàn)證,我在那里的每個多線程同步操作里都加上了Profile代碼,繼續(xù)花了一個小時重現(xiàn)bug,并檢查Profile結(jié)果,發(fā)現(xiàn)那個線程就是慢,沒什么特別原因,每個Critical Section都很快就過去了,也就是說,沒有哪一個部分特別慢,是整個函數(shù)被均勻地拖慢了。
忙了幾天,死去活來,找不到線索。領(lǐng)導(dǎo)決定游戲還是照樣發(fā)布,我繼續(xù)看這個問題,準(zhǔn)備在Patch里面修正這個bug。
在一次次重現(xiàn)中,又簡化了重現(xiàn)方法,我發(fā)現(xiàn)只需要順序進(jìn)入那幾關(guān),不需要把每一關(guān)都打過去的,所以我用作弊碼直接完成每一關(guān),重現(xiàn)一次 bug的時間縮短到10分鐘以內(nèi)了。
看來自己的工具是搞不定這個問題了,請出Intel Vtune來收拾它。公司比較摳門,不肯買Vtune,只好申請了評估版,一個月試用期,應(yīng)該夠搞定這個問題了。用Vtune Profile了幾次,每次拖慢的函數(shù)都不一定,而且里面真沒什么特別的地方。
又郁悶了兩天,某天看著Vtune里面紅紅綠綠的代表線程忙碌狀態(tài)的條條,突然發(fā)現(xiàn)有一個線程條全是密密麻麻的紅條,其他線程的紅藍(lán)條相對稀疏一點(diǎn)。開始猜想,是不是有某個線程太忙了,導(dǎo)致?lián)屃怂械臅r間片,讓其他線程都沒機(jī)會拿到時間片。有了理論依據(jù),大膽求證一番,重現(xiàn)一次bug,幀數(shù)很低了以后,斷下游戲,把系統(tǒng)里面的n個線程用二分法,Freeze一半,再運(yùn)行,看幀數(shù),然后恢復(fù)一半線程再跑,來回幾次,終于發(fā)現(xiàn)當(dāng)某一個線程恢復(fù)以后幀數(shù)就很低了,其他線程開關(guān)都沒關(guān)系。關(guān)掉那個線程游戲馬上全速歡快跑開了。再恢復(fù)運(yùn)行那個線程,又是老樣子。
找到了嫌疑目標(biāo)后,我全力追查這個線程是如何創(chuàng)建的。先在所有創(chuàng)建線程的地方加上日志,輸出線程 id,重現(xiàn)bug后找出那個問題線程,然后對照線程 id的日志,試圖找出這個線程創(chuàng)建的地方。結(jié)果很杯具,那個線程根本就不是我們自己程序創(chuàng)建的。也就是說,OS偷偷幫我們創(chuàng)了一個線程。這個就比較難查了,線程創(chuàng)建的時候我又沒法設(shè)斷點(diǎn),也不知道系統(tǒng)內(nèi)部用什么函數(shù)去創(chuàng)建線程,無法用Detour去Hook API。
轉(zhuǎn)機(jī)出現(xiàn)在Intel的Thread Profiler,照例又是一個月評估版。Thread Profiler可以顯示出創(chuàng)建這個Thread的Callstack,雖然不是特別準(zhǔn)確,不過已經(jīng)是很有用的信息了。我發(fā)現(xiàn)那個線程創(chuàng)建的時候Callstack里面有Winhttp之類的函數(shù)。
繼續(xù)轉(zhuǎn)移戰(zhàn)場,看Msdn上介紹的Winhttp系列函數(shù),然后搜索整個項(xiàng)目里面所有用到Winhttp系列函數(shù)的地方。應(yīng)該是我們調(diào)用Winhttp的時候方法不正確吧,我猜。好在項(xiàng)目里面用的Winhttp系列函數(shù)也不多,每個地方讀一遍代碼,似乎都沒問題。繼續(xù)想,會不會是中間件的問題,我們用了一個其他分公司開發(fā)的網(wǎng)絡(luò)組件,那個組件沒有包含在項(xiàng)目里面,只是弄了個lib過來。我連忙找到那個組件的源碼,一搜Winhttp又一大堆,一個個看,也都貌似正確。
既然是連續(xù)玩n關(guān)才出問題,可能和什么資源泄露有關(guān)吧?我恍然大悟,注意看Winhttp 句柄的生命周期,發(fā)現(xiàn)那個中間件,在Xbox360版本上正常釋放了句柄,可是Win32上就沒有...沒什么好說的,WinHttpCloseHandle伺候,問題迎刃而解。修正這個bug耗時2周,一路殺到聲音、內(nèi)存管理、網(wǎng)絡(luò)模塊,中間還順手修復(fù)了無數(shù)其他bug,最后終于將其正法,改動只是一行而已。
可得結(jié)論:中間件靠不住。
總結(jié)
以上是生活随笔為你收集整理的那些年,我在游戏开发中改过的bug:坑爹的Vista与中间件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Debug经验总结:优化、程序员和概率
- 下一篇: 浅谈RTS游戏网络同步:3种同步机制模式