那些年,我在游戏开发中改过的bug:靠不住的OS和SDK
記憶中有很多次了,幾個(gè)程序員朋友聊天,聊著聊著,就聊到自己遇到過的bug。然后大家開始口沫橫飛交流那些或詭異或神奇的bug,談?wù)撟约寒?dāng)年是如何搞定bug或是被bug搞定。
正好看見Gamesutra上也登了篇Dirty Coding Tricks ,發(fā)現(xiàn)老外也有這個(gè)癖好,原來天下程序員本一家。一路走來,程序員的成長,便是一路刀光劍影,與bug斗個(gè)你死我活。了解別人的Debug過程,或是回憶一下自己Debug的時(shí)候思路,也是很有意思的事情,值得定期總結(jié)。
下面分享一下自己遇到過印象深刻的bug:
靠不住的c:Memcpy的傳說
做一個(gè)PC項(xiàng)目的時(shí)候,兇猛的測試兄弟把Winxp 64單獨(dú)列出來,作為一個(gè)測試平臺(tái),然后我們的噩夢就開始了。游戲在Winxp 64上面頻繁Crash,經(jīng)常在更新Octree的時(shí)候訪問到空指針,但邏輯上來看那個(gè)指針不可能是空的。Crash位置很隨機(jī),到處都有,通常都是玩了一個(gè)小時(shí)在一個(gè)莫名其妙的地方Crash。
第一反應(yīng)就是那些地址被非法訪問,可能是某個(gè)錯(cuò)誤的指針指向那里,往里面寫了不該寫的值。于是我根據(jù)最常Crash的地址設(shè)下數(shù)據(jù)斷點(diǎn),試了好幾天,從來沒有斷下過,Crash還是依舊。然后同事試圖加上大量的保護(hù)代碼,判斷一個(gè)指針是不是空指針后才使用,很好的降低了Crash機(jī)率,但偶爾還是會(huì)有。想想問題根源沒有找到,降低Crash概率只是讓自己更難修bug,而且訪問Octree也比較多,亂加保護(hù)會(huì)影響性能,我一狠心又把保護(hù)代碼全去掉了。
?
來回幾輪搞下來,根據(jù)某次比較靠譜的Crash Callstack,懷疑到了memcpy。memcpy是個(gè)老同志了,兢兢業(yè)業(yè)地忙碌在各個(gè)程序里,負(fù)責(zé)搬運(yùn)數(shù)據(jù)很多年,工作績效有口皆碑。它有什么問題呢?它還能有什么問題呢?
為了保證多線程能同步并行執(zhí)行,我們程序中有個(gè)很大的memcpy,把一塊Octree從后臺(tái)用memcpy復(fù)制到前臺(tái)的工作buffer。當(dāng)然這個(gè)做法的設(shè)計(jì)優(yōu)劣不在此討論,存在即合理,2007年,多線程引擎我們還不是那么擅長搞。
既然有懷疑,就要捉奸。我做了試驗(yàn),在memcpy后面直接加一個(gè)循環(huán),逐字節(jié)比較源數(shù)據(jù)和目標(biāo)數(shù)據(jù),有時(shí)候居然會(huì)不相等... 這個(gè)可顛覆了我的世界觀。我試圖寫了一個(gè)函數(shù),里面就一個(gè)循環(huán),逐字節(jié)復(fù)制數(shù)據(jù),然后把所有的memcpy全替換成這個(gè)函數(shù),果然不Crash了。但顯然這是不行的,速度太慢了。
既然有了點(diǎn)線索,就可以試圖簡化bug重現(xiàn)條件了。我不能每次都花一個(gè)小時(shí)去運(yùn)行游戲,尋找那一次crash。我在游戲load起來,開始走主循環(huán)后,加了一個(gè)死循環(huán),不停用memcpy復(fù)制一塊內(nèi)存,然后比較源數(shù)據(jù)和目標(biāo)數(shù)據(jù)。源數(shù)據(jù)里面沒有0,都是1-255的值,可是運(yùn)行幾十秒以后目標(biāo)數(shù)據(jù)居然有0。這樣,我們成功地把重現(xiàn)一次bug的平均時(shí)間從一個(gè)小時(shí)降低到一分鐘。
我們的懷疑從3d代碼轉(zhuǎn)移到多線程,在進(jìn)入那個(gè)死循環(huán)之前,我們設(shè)下斷點(diǎn),把其他無關(guān)的線程全部都Freeze,只有那個(gè)線程會(huì)運(yùn)行。這樣,任何多線程的干擾全部排除,memcpy在一個(gè)理想的環(huán)境中歡快的運(yùn)行,但memcpy還是會(huì)出錯(cuò)。
繼續(xù)簡化,我單獨(dú)寫一個(gè)小程序,里面只做死循環(huán)和memcpy,游戲賬號(hào)交易平臺(tái)來判斷是不是OS的問題(實(shí)在是走投無路了)。試驗(yàn)結(jié)果是,Winxp 64沒有問題,memcpy始終如一地正常工作著(本該如此^_^)。 可是某一次,當(dāng)我們的游戲在后臺(tái)運(yùn)行的時(shí)候,再啟動(dòng)這個(gè)小程序,居然memcpy又出問題了...無語了,原來我們的游戲還能萬里追殺,跨進(jìn)程搞垮OS下面的其他進(jìn)程。
山窮水盡疑無路,我無奈下單步跟蹤了一下memcpy的匯編代碼,上來有兩句
也就是說,memcpy上來看有沒有設(shè)置__sse2_available,這個(gè)值估計(jì)是CRT庫里面設(shè)的,如果有SSE2就執(zhí)行sse優(yōu)化的memcpy,沒有就跳到Dword_align那里執(zhí)行普通版本的流程。我開始懷疑是不是我們的游戲里面對系統(tǒng)做了點(diǎn)什么手腳,導(dǎo)致在__sse2_available允許的情況下,優(yōu)化的代碼會(huì)執(zhí)行出錯(cuò)。游戲的代碼規(guī)模實(shí)在太大,又用了n個(gè)中間件,我無力一一查看,且我也看不懂SSE代碼(哎呀好羞射),就隨手做了個(gè)試驗(yàn),在那句判斷的地方,通過Debugger把__sse2_available的值改成了0。從此memcpy再也不出錯(cuò)了。
所以最終的解決方案是,Win64下,我在游戲一開始初始化的地方,加上謎一樣的初始化代碼:
?
這樣memcpy就永遠(yuǎn)使用不做sse2優(yōu)化的代碼了。
memcpy不使用sse2后會(huì)不會(huì)有性能問題?經(jīng)過測試,發(fā)現(xiàn)問題不大,對于頻繁調(diào)用的少量數(shù)據(jù)復(fù)制,memcpy不太能從sse2里面得到多少好處。對于大量數(shù)據(jù)的復(fù)制,我們用得也不多,profile了一下,沒有發(fā)現(xiàn)明顯的瓶頸,無視了。這事情也可以從反向理解,由于游戲規(guī)模太大,各種多線程和GPU/CPU同步,導(dǎo)致任何一點(diǎn)的效率損失,可能不會(huì)擴(kuò)散到整個(gè)游戲,被其他同步和等待吸收掉了…我真是一個(gè)好程序員,能想到這么好的理由說服自己。
對游戲跨進(jìn)程影響其他程序的memcpy,實(shí)在沒能力解決了,Winxp 64是一個(gè)太小眾的環(huán)境,用戶要么用Winxp 32, 要么用Vista 32/64,市場占有率很低,我們也算仁至義盡了。
可得結(jié)論:Winxp 64靠不住(其實(shí)問題還是應(yīng)該出在我們內(nèi)部,不過其他OS都沒問題,就賴在它身上了)
?
為了達(dá)成目的,我們要不擇手段。
靠不住的SDK:OutputDebugString
話說當(dāng)年開發(fā)Splinter Cell 4,使用的還是XBOX360的Alpha kit。微軟早期的360 Kit,全不像后期的Kit,后期kit長得和主機(jī)差不多。而當(dāng)時(shí)的KIt,是用一個(gè)很大的Power Mac G5,換上一塊ATI顯卡,再刷上MS的固件,連馬甲都不穿一件就出來見人了。
Xbox 360開發(fā)SDK,早期bug一大堆,比如預(yù)編譯頭文件太大了,編譯器抱怨說預(yù)編譯頭文件預(yù)留內(nèi)存不夠,這個(gè)好辦,加上/Zm512編譯選項(xiàng)即可。加上,編譯,沒用?!只好寫信去MS問,他們說,哦,原來如此啊,今天天氣真好,哈哈哈哈,請等待下一次更新,謝謝您匯報(bào)云云...雖是MS的bug,可是我也不能等著他修復(fù)才工作,只好手動(dòng)拆分預(yù)編譯頭文件,把很多內(nèi)容放在預(yù)編譯頭文件外面,預(yù)編譯頭文件就會(huì)變小,編譯就可以順利通過了。
扯遠(yuǎn)了,回頭來說這個(gè)OutputDebugString的問題。
360有3個(gè)cores,每個(gè)core有2個(gè)Hyperthreads,總計(jì)6個(gè)線程,我們的游戲在邏輯線程、聲音線程和渲染線程外,還開了3個(gè)輔助線程,用自己寫的Thread Pool管理系統(tǒng)來管理這些線程,初始化的時(shí)候就是一個(gè)循環(huán)把每個(gè)線程創(chuàng)建出來。
這個(gè)bug的表現(xiàn)現(xiàn)象就是加載失敗,程序僵死。團(tuán)隊(duì)當(dāng)時(shí)有幾十個(gè)測試人員,每天打游戲八小時(shí),從沒碰到過游戲加載失敗情況。但是開發(fā)人員這里就有很低概率會(huì)加載失敗,表現(xiàn)情況為用VC啟動(dòng)游戲,然后過一會(huì)加載屏幕就僵死不動(dòng)了。開發(fā)人員往往過了5分鐘還沒在電視上看見游戲畫面才知道游戲又掛了,重現(xiàn)概率是每個(gè)程序員每一到兩天碰到一次。我們擔(dān)心這是線程管理系統(tǒng)內(nèi)部的問題,就讓每個(gè)開發(fā)人員碰到這種情況不要急著重啟動(dòng),把現(xiàn)場給我看一下。每次僵死的時(shí)候都是在系統(tǒng)內(nèi)核死鎖,Callstack也沒有有價(jià)值的信息,基本都是在創(chuàng)建每個(gè)線程的時(shí)候打印一句語句的時(shí)候就內(nèi)核就死了。在接下來一周里,我試圖在線程管理系統(tǒng)內(nèi)部加一些日志輸出,每次重現(xiàn)bug的時(shí)候查看日志,也沒有找到更好的線索。
重現(xiàn)概率實(shí)在太低,不好調(diào)試,于是我試圖用簡單的程序片段來重現(xiàn)這個(gè)bug。因?yàn)槎际莿?chuàng)建新線程時(shí)候死鎖,第一個(gè)想到的就是寫一個(gè)小程序,直接一個(gè)死循環(huán),創(chuàng)建線程,打印日志,然后殺掉線程,重復(fù)再做。果然能夠重現(xiàn)bug,程序運(yùn)行1分鐘以后,就死鎖了。同樣的現(xiàn)場,還是在OutputDebugString內(nèi)部死鎖了。難道bug不是在線程庫,而是在OutputDebugString內(nèi)?
?
正好那些天有個(gè)微軟360開發(fā)組的人員在我們組Onsite支持,于是他帶著大量的360 Sdk的符號(hào)庫(Symbol)來幫助調(diào)試,因?yàn)樗皇亲鲞@一個(gè)模塊的,最后也沒有什么結(jié)果。最后他把我的小程序發(fā)回微軟,找到內(nèi)部開發(fā)人員處理,這比我們直接走正式support流程快很多。
?
一天后,微軟發(fā)回Email,說這是內(nèi)部的Bug,請無視,不會(huì)影響Release版本,是Debug協(xié)議上的問題。
可得結(jié)論:微軟靠不住。
總結(jié)
以上是生活随笔為你收集整理的那些年,我在游戏开发中改过的bug:靠不住的OS和SDK的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅谈RTS游戏网络同步:3种同步机制模式
- 下一篇: 游戏编程设计模式——Game Loop