继续说那些奇葩的Bug。
靠不住的系统组件:Vista和Speech Recogition
我们游戏使用了语音识别,使用了DirectX里面的XAudio来采集声音。Windows Vista里面有一个语音识别的组件,启动那个程序,然后玩我们的游戏,玩了一段时间,因为没有使用麦克风,那个程序会自动关闭使用麦克风,然后我们的游戏就Crash了。又是一个跨进程的奇葩Bug。
从现场来看,Crash在使用XAudio的库代码里面。看不出什么线索,最后发现在日志里面,Crash前一会儿,XAudio的dll被Unload掉了。Vista那个语音组件很强悍,也会跨进程追杀大法,在自己进程把我的进程dll给Unload掉了。
虽然遇事先要怀疑自己的代码有问题,但这个情况太匪夷所思,所以我试图推卸责任,我跑了几个其他DirectX程序和Demo,但凡用到XAudio组件的,在Vista Speech Recognition这一大招前无一幸免,全部Crash。
问题不在我们这里,但是作为一个职业杀虫人,我有着「只解沙场为国死,何须bug裹尸还」的觉悟,决定还是要想办法搞定它。
我猜这个组件退出的时候,广播了点什么消息,让系统所有进程去卸载这个Dll。我找了一个微软提供的库Detours Express,专做Hook API的勾当。它会反汇编API的入口代码,然后动态替换入口,插入jmp指令去执行我们自己的函数。先看了文档,搞懂这个东西怎么用,然后猜测是一个OS内部的消息导致Unload DLL,我们就拦截了RegisterWindowMessage。一通翻箱倒柜,啥有意义的东西都没发现。
再用spy++来拦截消息,发现有一个MSUIM.msg.rpcsendreceive的消息面目狰狞,很可疑。就在收到这个消息以后,Dll就被Unload了。于是我们用Detours截住这个消息,直接返回。
以为搞定了,但结果还是不行,有几条别的消息也会触发Unload Dll,我一一用Detours拦截返回。拦截的消息越来越多,看来不是个解决办法。于是我怀疑我们没有拦截到正确的消息,怀疑OS启动的时候便注册了一堆内部用的消息,而不是在运行进程的时候才注册。那些消息里肯定有我想要的,我便异想天开地Hook了User32.dll,拦截下所有消息,在安全模式下把修改过的user32.dll覆盖原来的文件,然后满怀希望的重启动Vista的时候。结果不出所料的蓝屏了...User32.dll是凡人能随便碰的吗?这不是一个User friendly的库,应该改名叫God32.dll。
这个bug搞了1周,最后才想到最基本的道理,说不定只是Dll内部引用计数被清掉了,所以系统就卸掉Dll了。当Vista语音组件Unload XAudio的时候,系统去看看XAudio库有没有什么别人引用,结果发现居然没有,那就顺手清理了。就像地上有100块钱,你东张西望,发现没有人宣布拥有那张钱,没有一点点犹豫,你随手就把它放进了裤兜,进行了回收。嗯,就是这样的,我喜欢比喻这种修辞手法。
理论上创建XAudio设备的时候没有做什么额外的加Dll引用计数的事情,怀疑是MS的bug,创建D3D设备、DInput设备的时候都不需要做什么特别的事情的,但他们的引用计数都没有问题。
于是我恶搞了一下,在初始化XAudio完成以后,手动做了一次LoadLibrary,把XAudio2_1.dll强行Load一遍,相当于自行增加了引用计数。于是再无Crash。
从此我们的游戏,在Vista 32上过上了幸福的生活。
可得结论:Vista靠不住
Takeaway: 不知道该说什么了,这个bug太奇葩了。
性能的迷思
临近最终版本发布的时候,测试人员发现一个问题,当在Vista上连续打游戏过7关以后,游戏帧数一下子变成原来一半了。大家觉得很诡异,都不相信,之前整天玩游戏都没问题的,于是责令测试人员再重现几次,否则拖出去刨坑埋了。我也没当回事,