游戏开发论坛

 找回密码
 立即注册
搜索
查看: 7323|回复: 0

那些年,我在游戏开发中改过的bug:靠不住的OS和SDK

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2016-5-5 14:30:30 | 显示全部楼层 |阅读模式
QQ截图20160505142840.jpg

  GameRes游资网授权发布 文/ 顾煜 游戏开发随笔专栏

  记忆中有很多次了,几个程序员朋友聊天,聊着聊着,就聊到自己遇到过的bug。然后大家开始口沫横飞交流那些或诡异或神奇的bug,谈论自己当年是如何搞定bug或是被bug搞定。

  正好看见Gamesutra上也登了篇Dirty Coding Tricks ,发现老外也有这个癖好,原来天下程序员本一家。一路走来,程序员的成长,便是一路刀光剑影,与bug斗个你死我活。了解别人的Debug过程,或是回忆一下自己Debug的时候思路,也是很有意思的事情,值得定期总结。

  下面分享一下自己遇到过印象深刻的bug:

  靠不住的c:Memcpy的传说

  做一个PC项目的时候,凶猛的测试兄弟把Winxp 64单独列出来,作为一个测试平台,然后我们的噩梦就开始了。游戏在Winxp 64上面频繁Crash,经常在更新Octree的时候访问到空指针,但逻辑上来看那个指针不可能是空的。Crash位置很随机,到处都有,通常都是玩了一个小时在一个莫名其妙的地方Crash。

  第一反应就是那些地址被非法访问,可能是某个错误的指针指向那里,往里面写了不该写的值。于是我根据最常Crash的地址设下数据断点,试了好几天,从来没有断下过,Crash还是依旧。然后同事试图加上大量的保护代码,判断一个指针是不是空指针后才使用,很好的降低了Crash机率,但偶尔还是会有。想想问题根源没有找到,降低Crash概率只是让自己更难修bug,而且访问Octree也比较多,乱加保护会影响性能,我一狠心又把保护代码全去掉了。

Takeaway: 不到万不得已,不要用保护代码掩盖Bug,它知会让你的日子更难过。

  来回几轮搞下来,根据某次比较靠谱的Crash Callstack,怀疑到了memcpy。memcpy是个老同志了,兢兢业业地忙碌在各个程序里,负责搬运数据很多年,工作绩效有口皆碑。它有什么问题呢?它还能有什么问题呢?

  为了保证多线程能同步并行执行,我们程序中有个很大的memcpy,把一块Octree从后台用memcpy复制到前台的工作buffer。当然这个做法的设计优劣不在此讨论,存在即合理,2007年,多线程引擎我们还不是那么擅长搞。

  既然有怀疑,就要捉奸。我做了试验,在memcpy后面直接加一个循环,逐字节比较源数据和目标数据,有时候居然会不相等... 这个可颠覆了我的世界观。我试图写了一个函数,里面就一个循环,逐字节复制数据,然后把所有的memcpy全替换成这个函数,果然不Crash了。但显然这是不行的,速度太慢了。

  既然有了点线索,就可以试图简化bug重现条件了。我不能每次都花一个小时去运行游戏,寻找那一次crash。我在游戏load起来,开始走主循环后,加了一个死循环,不停用memcpy复制一块内存,然后比较源数据和目标数据。源数据里面没有0,都是1-255的值,可是运行几十秒以后目标数据居然有0。这样,我们成功地把重现一次bug的平均时间从一个小时降低到一分钟。

  我们的怀疑从3d代码转移到多线程,在进入那个死循环之前,我们设下断点,把其他无关的线程全部都Freeze,只有那个线程会运行。这样,任何多线程的干扰全部排除,memcpy在一个理想的环境中欢快的运行,但memcpy还是会出错。

  继续简化,我单独写一个小程序,里面只做死循环和memcpy,来判断是不是OS的问题(实在是走投无路了)。试验结果是,Winxp 64没有问题,memcpy始终如一地正常工作着(本该如此^_^)。 可是某一次,当我们的游戏在后台运行的时候,再启动这个小程序,居然memcpy又出问题了...无语了,原来我们的游戏还能万里追杀,跨进程搞垮OS下面的其他进程。

  山穷水尽疑无路,我无奈下单步跟踪了一下memcpy的汇编代码,上来有两句

  1. cmp DWORD PTR __sse2__available,0

  2. je Dword_align
复制代码

  也就是说,memcpy上来看有没有设置__sse2_available,这个值估计是CRT库里面设的,如果有SSE2就执行sse优化的memcpy,没有就跳到Dword_align那里执行普通版本的流程。我开始怀疑是不是我们的游戏里面对系统做了点什么手脚,导致在__sse2_available允许的情况下,优化的代码会执行出错。游戏的代码规模实在太大,又用了n个中间件,我无力一一查看,且我也看不懂SSE代码(哎呀好羞射),就随手做了个试验,在那句判断的地方,通过Debugger把__sse2_available的值改成了0。从此memcpy再也不出错了。

  所以最终的解决方案是,Win64下,我在游戏一开始初始化的地方,加上谜一样的初始化代码:

  1. extern "C" extern DWORD __sse2_available;

  2. __sse2_available = 0;
复制代码

  这样memcpy就永远使用不做sse2优化的代码了。

  memcpy不使用sse2后会不会有性能问题?经过测试,发现问题不大,对于频繁调用的少量数据复制,memcpy不太能从sse2里面得到多少好处。对于大量数据的复制,我们用得也不多,profile了一下,没有发现明显的瓶颈,无视了。这事情也可以从反向理解,由于游戏规模太大,各种多线程和GPU/CPU同步,导致任何一点的效率损失,可能不会扩散到整个游戏,被其他同步和等待吸收掉了…我真是一个好程序员,能想到这么好的理由说服自己。

  对游戏跨进程影响其他程序的memcpy,实在没能力解决了,Winxp 64是一个太小众的环境,用户要么用Winxp 32, 要么用Vista 32/64,市场占有率很低,我们也算仁至义尽了。

  可得结论:Winxp 64靠不住(其实问题还是应该出在我们内部,不过其他OS都没问题,就赖在它身上了)

Takeaway: 首先,你要有一个有耐心的老板,才能给你时间去查这么奇怪的Bug

  为了达成目的,我们要不择手段。

  靠不住的SDK:OutputDebugString

  话说当年开发Splinter Cell 4,使用的还是XBOX360的Alpha kit。微软早期的360 Kit,全不像后期的Kit,后期kit长得和主机差不多。而当时的KIt,是用一个很大的Power Mac G5,换上一块ATI显卡,再刷上MS的固件,连马甲都不穿一件就出来见人了。

  Xbox 360开发SDK,早期bug一大堆,比如预编译头文件太大了,编译器抱怨说预编译头文件预留内存不够,这个好办,加上/Zm512编译选项即可。加上,编译,没用?!只好写信去MS问,他们说,哦,原来如此啊,今天天气真好,哈哈哈哈,请等待下一次更新,谢谢您汇报云云...虽是MS的bug,可是我也不能等着他修复才工作,只好手动拆分预编译头文件,把很多内容放在预编译头文件外面,预编译头文件就会变小,编译就可以顺利通过了。

  扯远了,回头来说这个OutputDebugString的问题。

  360有3个cores,每个core有2个Hyperthreads,总计6个线程,我们的游戏在逻辑线程、声音线程和渲染线程外,还开了3个辅助线程,用自己写的Thread Pool管理系统来管理这些线程,初始化的时候就是一个循环把每个线程创建出来。

  这个bug的表现现象就是加载失败,程序僵死。团队当时有几十个测试人员,每天打游戏八小时,从没碰到过游戏加载失败情况。但是开发人员这里就有很低概率会加载失败,表现情况为用VC启动游戏,然后过一会加载屏幕就僵死不动了。开发人员往往过了5分钟还没在电视上看见游戏画面才知道游戏又挂了,重现概率是每个程序员每一到两天碰到一次。我们担心这是线程管理系统内部的问题,就让每个开发人员碰到这种情况不要急着重启动,把现场给我看一下。每次僵死的时候都是在系统内核死锁,Callstack也没有有价值的信息,基本都是在创建每个线程的时候打印一句语句的时候就内核就死了。在接下来一周里,我试图在线程管理系统内部加一些日志输出,每次重现bug的时候查看日志,也没有找到更好的线索。

  重现概率实在太低,不好调试,于是我试图用简单的程序片段来重现这个bug。因为都是创建新线程时候死锁,第一个想到的就是写一个小程序,直接一个死循环,创建线程,打印日志,然后杀掉线程,重复再做。果然能够重现bug,程序运行1分钟以后,就死锁了。同样的现场,还是在OutputDebugString内部死锁了。难道bug不是在线程库,而是在OutputDebugString内?

Takeaway: 不要轻易相信你看见的表层问题,问题可能在别处

  正好那些天有个微软360开发组的人员在我们组Onsite支持,于是他带着大量的360 Sdk的符号库(Symbol)来帮助调试,因为他不是做这一个模块的,最后也没有什么结果。最后他把我的小程序发回微软,找到内部开发人员处理,这比我们直接走正式support流程快很多。

Takeaway: 隔离问题,编写更小的测试案例,便于和别人沟通交流,寻求帮助

  一天后,微软发回Email,说这是内部的Bug,请无视,不会影响Release版本,是Debug协议上的问题。

  可得结论:微软靠不住。(开玩笑,请微软公司不要追杀)

  相关阅读几个理由告诉你,为什么现在的游戏Bug那么多

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-4-20 02:19

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表