前言:这是一篇发布在 10 年前的文章(原文发布于 2013年10月31日),作者讲述了它为 PS1 开发《古惑狼》时遇到的一个奇怪的 Bug。原文是问答网站 Quora 的一个回答,它回答的问题是:你调试过最硬的 Bug 是什么?这个答案最终也获得了 6.7K 的投票。
原文链接丨作者:Dave Baggett
说起这事我现在都还有点痛苦。
作为一个程序员,你会有一个 bug 原因的追查列表。首先你会把 bug 归咎于你的代码,然后是第二、第三、……,在数到第一万的时候你可能会怪编译器。直到最后,你只能去责问硬件。
这就是我遇到的硬件 bug 的故事。
我当时在为《古惑狼》写一些操作(加载/存储)记忆卡的代码。对一个游戏开发者老兵来说这就是稀松平常的事,我估计我几天就可以搞定。结果我花了六个礼拜来调试它。这段时间我当然还在做别的事,但我时不时就会回来调试一下这个 bug,大概每天会花几个小时。让人崩溃。
问题是这样的。你要存储你的游戏进度,所以你会访问记忆卡。大部分时间它都挺正常的……但是每隔一段时间,你就会遇到一次读写超时的错误,毫无缘由。一次快速写入(short write)常常会把记忆卡搞崩:玩家存档的时候我们不仅没办法正确保存,我们还会把玩家的记忆卡整个清除。D’OH!
过了一阵,我们在索尼的制作人, Connie Booth [译注],开始慌了。很显然我们不能带着这个 bug 把游戏推出去,而且六个礼拜过去了,我还是一点头绪都没有。通过 Connie 我们和 PS 1 的开发者搭上了话 —— 「你们谁见过类似这样的事情吗?」结果是没有。没有任何人在记忆卡的存储系统上遇到过任何问题。
在你束手无策的时候,你唯一能做的就是分解代码然后逐个击破:一点一点移除可疑的代码,直到你只剩下一段相对短小但是依然会导致 bug 的代码。排除所有无关的部分,剩下的就是 bug 所在。 在电子游戏的场景下这事就有点棘手:你很难移除代码片段。比如说,如果你移除了重力模拟的代码,或者移除了渲染角色的代码,你还怎么运行游戏?你能做的只能是移除一些模块,然后用一些接口代码(stubs)作为代替。这些接口代码会假装工作,但实际上它们只会做一些绝对不会引发 bug 的事情。你必须重新写一些框架(scaffolding code) 来让游戏足以运行。这是一个漫长而痛苦的过程。
长话短说:我这么干了。我移除了大块大块的代码,直到最后,我基本上除了启动代码什么都没剩下了。这些代码就只是启动系统,运行游戏,初始化渲染硬件,诸如此类。当然,这时候我无法调出加载/存储的菜单,因为我把图像相关的代码都移除了。但是我可以模拟用户使用了那个(不可见)的加载/存储界面,选择保存,然后写入记忆卡。
终于,我把会引发问题的代码缩小到了一个非常小的范围——但是依然会随机触发!大部分时间它都能正常工作,但是每过一段时间它就会失败。几乎所有真正和《古惑狼》相关的代码都被移除了,但是错误还在。事情变得令人费解:剩下的代码其实什么都没干。
在某个时刻——可能是凌晨三点——我冒出了一个想法。读写操作(I/O)涉及到精确的时序。当你和硬件打交道的时候——一块集成闪存,或者一个蓝牙发信器,随便什么东西——那些读写相关的底层代码需要依照时钟来执行。时钟可以让没有和 CPU 直接连接的硬件设备与 CPU 保持同步。时钟决定波特率——数据从一边发送到另一边的速率。如果时序被弄乱了,硬件或者软件,或者二者皆有,会被迷惑。这非常非常糟糕,通常会导致数据丢失。
万一由于某种原因我们的设定代码里面有什么东西把时序给搞乱了呢?我又看了一遍测试程序中和时序有关的代码,我注意到我们把 PlayStation 1 的可编程定时器设置在了 1 kHz。这是一个相对比较高的频率,PlayStation 1 启动的时候默认频率是 100 Hz。Andy,这游戏的主程(也是除我之外唯一开发者),把它设置成了 1 kHz,以使《古惑狼》的运动计算更加精确。Andy 喜欢这种追求极致的风格,既然我们要模拟重力,那我们就要尽可能地准确。
但是,如果增加这个定时器的频率会莫名其妙地影响到整个程序的时序,进而影响记忆卡的波特率呢?
我把设置定时器的注释掉,然后我就没办法复现那个错误了。但是这并不能说明我修好了它,因为这个问题是会随机出现的。万一我只是运气好呢?
接下来的几天,我一直摆弄我的测试程序,那个 bug 再也没出现过。我回到《古惑狼》完整代码,修改了加载/存储的代码,在访问记忆卡之前把可编程定时器设置成默认的 100 Hz,在访问过后再把它调回 1 kHz。于是我们再也没见到过那个问题。
但是为什么?
我反复地调试测试程序,尝试在定时器被设置在 1 kHz 时找到一些错误的共有模式。最终我注意到,当有人在用 PlayStation 1 的手柄的时候就会出错。由于我很少这么干——我在测试加载/存储的代码,怎么会去碰手柄呢——所以我一直没发现这事。但是有一天,一位美术在等我完成测试——我当时肯定在骂骂咧咧的——于是他就在小心翼翼地摆弄手柄。然后就出错了。「等会,怎么回事?嘿,你再做一次!」
一旦我观察到了二者的关联性,复现就变得很容易了:开始写入记忆卡,摇动手柄,记忆卡崩溃。我觉得这应该是一个硬件 bug。
我去找了 Connie,告诉她我的发现,她又把这些转述给了开发了 PlayStation 1 的一位硬件工程师。「不可能,」对方跟她说,「这不可能是一个硬件的问题。」我问她能不能让我直接和对方讲。
于是他打电话过来,我们用他蹩脚的英文和我(极其)蹩脚的日语争论起来。最终我告诉他,「让我发给你一段 30 行的测试程序,你摇摇杆的时候就会触发这个问题。」他接受了。这只会是浪费时间,他向我保证,而且他当时正在忙于另一个项目,但他还是勉为其难地接受了,因为对索尼来说我们是非常重要的开发者。我整理了一下我的测试小程序,发给了他。
第二天晚上(我们当时在洛杉矶,而他在东京,所以他的白天刚好是我的晚上)他打电话给我,非常难为情地向我致歉。这确实是一个硬件问题。
直到最后我也不是很清楚具体的原因是什么,但我印象中从索尼总部听到的说法是,当那个可编程定时器被设定在一个比较高的时钟频率,它会影响主板上谐振器附近的一些东西。其中一样就是记忆卡的波特率控制器,它同时也会设置手柄的波特率。硬件不是我的专长,所以我不是很清楚那些细节。重点是主板上的通讯干扰,还有在定时器被设置在 1 kHz 时同时发往手柄端口和记忆卡端口的数据传输,会导致一些比特的数据丢失,于是数据失效,于是记忆卡崩溃。
[译注]: Connie Booth 参与了很多知名游戏的制作和发行,包括《小龙史派罗》、《古惑狼》、《瑞奇与叮当》、《蜘蛛侠》、《神秘海域》、《最后生还者》、《往日不再》等。2020 年,她以索尼互娱的产品开发部门副总裁入选了 2020 年电子游戏名人堂。