摘要:这款 1993 年 12 月发布的 FPS 开山之作,不仅掀起了玩家对“第一人称射击”的狂热,还影响了此后几十年的游戏文化。自那时起近 32 年时间里,数不清的玩家投入了数百万、甚至数十亿小时沉浸其中,更别说社区也贡献了大量模组与自制关卡。可即便如此,它的底层
整理 | 屠敏
出品 | CSDN(ID:CSDNnews)
如果说游戏史上有哪款作品最经得起时间考验,《DOOM》一定榜上有名。
这款 1993 年 12 月发布的 FPS 开山之作,不仅掀起了玩家对“第一人称射击”的狂热,还影响了此后几十年的游戏文化。自那时起近 32 年时间里,数不清的玩家投入了数百万、甚至数十亿小时沉浸其中,更别说社区也贡献了大量模组与自制关卡。可即便如此,它的底层代码里依旧埋着一些奇怪的遗留问题。
为了验证其中一个鲜有人注意的 32 位整数溢出 Bug 是否真的会在现实中触发,一名极客决定做个实验:把《DOOM》在一款老旧的硬件上开着跑上几年,看看会发生什么。结果是——在两年半之后,游戏真的崩溃了。
极客实验:掌机 + UPS + “忘掉它”,让 DOOM 跑到崩溃
这名极客叫 Minki,谈及此次实验的初衷,其表示是源于他当时偶然间看到了一篇讲 DOOM 引擎的文章,发现有个用来记录 demo 播放的变量,每次都会一直往上加,甚至在下一个 demo 开始之后也还在加。程序里会把这个变量和另一个保存“前一个值”的变量做比较。问题在于,这个变量每次加一,迟早会接近溢出。虽然在正常情况下几乎不可能跑到那一步,但他就特别好奇:要是一直运行下去,多久会因为这个溢出而把游戏搞崩?
为了搞明白这个问题,他在自己的网站 LenOwO 上分享了这次“实验”过程——将移植版的 WinDOOM 装进一台 2003 年的华硕 MyPal A620 掌机里。这台掌机搭载 Windows Mobile 系统和 Intel XScale ARMv5 处理器,性能早已落伍。为了确保设备长时间不断电,Minki 还动手 DIY 改装了一块 18650 锂电池 UPS,并通过路由器的 USB 接口稳定供电,保证能一直有 5V 电压。
然后,这台掌机就被丢在角落里,绝大多数时间无人问津,直到两年半后,Minki 才发现屏幕上弹出了熟悉的“应用程序崩溃”提示。
崩溃幕后:gametic 的“死亡倒计时”
问题的根源其实埋在 DOOM 的代码里。和很多老牌商业软件一样,即便到了 1.9 最终版本,游戏依旧遗留了不少 bug。这里最关键的就是一个名叫 gametic的计时器变量。
这个数值用于追踪游戏内的时间流逝,并且以 35Hz(每秒 35 次)的固定频率递增,而不依赖游戏的渲染循环。即使不需要多复杂的数学知识,也能看出如果 gametic 永远不清零,时间一长它就会变成一个天文数字。
作为 DOOM 引擎的主要开发者,John Carmack 当年其实肯定知道这里的设计有点小问题。但他大概觉得没什么关系。为什么?因为这个数是存在有符号的 32 位整数里的。
这种整数能表示的范围大概是:从 -2,147,483,647到+2,147,483,647。所以只要你从 0 一直往上加,加到2,147,483,647这个天花板,下一次再加,就会溢出。
在 C 语言的标准里,“整数溢出”属于“未定义行为”。但在 x86 PC 这种常见的硬件上,几乎总是会出现一个很直观的结果:数值会绕一圈回到负数的最小值。也就是说,它会从最大正数跳到-2,147,483,647。
换句话说,Carmack 知道这个变量理论上会溢出,但考虑到要数二十多亿次才会触发,在正常游戏流程里压根没人能跑到这一步,所以他就没当回事。
经过 2.5 年后,游戏崩了
正如上文所述,按照每秒 35 次计数来算,大约 1.95 年就会让 gametic 数值溢出。当时 Minki 自己算过一遍,具体怎么算的已经记不清了,但他记得结果大概是——差不多能跑两年半才会溢出。
如今的实验证明,这款游戏真的经过了 2.5 年后崩了。所以,别把 DOOM 持续开着两年——或者说,任何游戏都别这么做,除非它是专用的服务器而不是客户端。
一场实验引发的思考
这场实验虽然看起来毫无实用意义,但却透露出几个耐人寻味的地方。首先,很多开发者在写代码时会假设用户不会让程序运行那么久,于是一些看似微不足道的边界条件就被忽略了。其次,C 语言中的未定义行为总是让人摸不透——有时表现一致,有时却完全不可预测。
随着 Minki 的帖子登上 Hacker News 热榜,也引发了不少讨论。许多人分享了类似经历:
一位叫 jbreckmckye的用户分享说,他在研究《古惑狼 3》的计时系统时发现,它同样有一个不断递增的 int32 计数器,只有在角色死亡时才会归零。如果一直运行下去,2.26 年后也会溢出。
jsheard则提到,《最终幻想 9》有一把隐藏武器,必须在游戏时间不到 12 小时的情况下赶到后期地图才能拿到。PAL 版本因为疏忽更狠,只给 10 小时。听上去几乎不可能,但还有另一条路:把游戏放着别管它,两年多之后计时器自己溢出归零,你就能拿到这武器了。慢慢来,照样能赢。
其实游戏中看似不可能触发的边界条件,不仅真的会发生,还能在不同游戏里演变成一种另类的“隐藏彩蛋”。
来源:CSDN一点号