在64位Alpha AXP Windows NT上的Pinball

Lobsters Hottest 新闻

摘要

探讨了Windows内置Pinball游戏的历史、导致其无法在64位Windows(特别是Alpha AXP版本)上运行的碰撞检测Bug,以及近期模拟技术突破使得稀有的64位Alpha NT构建能够运行该游戏。

<a href="https://lobste.rs/s/bygb9t/pinball_on_64_bit_alpha_axp_windows_nt">评论</a>
查看原文
查看缓存全文

缓存时间: 2026/06/02 17:36

# 64位Alpha AXP Windows NT上的弹球游戏 来源:https://virtuallyfun.com/2026/06/02/pinball-on-64-bit-alpha-axp-windows-nt/ *本文为Yufeng Gao (https://thebrokenpipe.com/blog/author/brokenpipe/) 的客座文章* 最受欢迎的OS内置游戏之一无疑是**弹球** (https://en.wikipedia.org/wiki/Full_Tilt!_Pinball#3D_Pinball_for_Windows_%E2%80%93_Space_Cadet),其全称为 *3D Pinball for Windows – Space Cadet*。它最初源自 *Full Tilt! Pinball*,由 **Cinematronics** (https://en.wikipedia.org/wiki/Cinematronics,_LLC) 开发,**Maxis** (https://en.wikipedia.org/wiki/Maxis) 发行。该游戏包含三张台面,其中一张名为 Space Cadet,被微软授权后纳入 Microsoft Plus! 95,随后成为 Windows 操作系统内置组件。 [](https://virtuallyfun.com/wp-content/uploads/2026/06/pinball_gameplay.gif) Windows XP 是最后包含弹球游戏的 Windows 版本,Raymond Chen 在其博客中解释了为何它未能进入 Windows Vista (https://devblogs.microsoft.com/oldnewthing/20121218-00/?p=5803)。原因在于,当编译为 64 位 Windows 时,存在一个碰撞检测器 bug,导致球体穿过各种物体——例如通过弹射器掉落屏幕而非被发射出去。该 bug 使游戏无法游玩,Raymond 及其同事无法在合理时间内修复,因此将其移除。至少,这个故事被讲述了大约十年。 2021 年,**NCommander** (https://www.youtube.com/c/NCommander) 发起了一系列调查 (https://www.youtube.com/watch?v=3EPTfOTC4Jw),对这一说法提出质疑,他在多种 64 位(IA-64 和 AMD64)Windows XP 及预发布版 Vista 上测试了弹球游戏。他发现所有 64 位版本的弹球游戏均可玩,仅有极细微的异常,并推测移除原因在于其 UI 不符合 Windows Vista 的设计。NCommander 发布视频后不久,Raymond 后续发布了一篇博文 (https://devblogs.microsoft.com/oldnewthing/20220106-00/?p=106122),补充了故事中的一些细节,并进一步说明了该 bug。他提到,是 64 位 Alpha AXP 版本的弹球游戏存在极其严重的碰撞检测 bug。过去 5 年间,这一说法一直无法验证,原因如下: - Alpha AXP 从未发布过 64 位 Windows 版本——Compaq 在 NT 移植到 64 位之前就终止了 Windows NT 支持。 - 2023 年虽有一个 64 位 Alpha AXP NT 构建被泄露,但其中包含的弹球游戏无法运行,因为运行后立即出现段错误。 我对 DEC Alpha 相当感兴趣,主要源于对 DEC 架构和 UNIX 的热爱。VAX 是 PDP-11 的直接继承者,而 Alpha 又是 VAX 的直接继承者。此前,一些 Alpha 模拟方面的突破出现,有几位朋友提醒我,NT 4.0 现在可以在 **ES40 模拟器的一个分支** (https://github.com/ES40-Emu/es40/) 以及 **QEMU** (https://github.com/TheBrokenPipe/qemu/tree/alphafix) 上运行。我从未想过 Alpha NT 能在模拟下运行,因为与熟悉的 Tru64、Linux 和 BSD 不同,NT 使用自有的定制 PALcode,并依赖 ARC(Advanced RISC Computing)而非 SRM。当然,有人指出这些模拟器无法运行 Alpha NT 的圣杯——Windows(XP?)构建 2210,因为其内核在 QEMU 中会因内存管理错误而崩溃,或在 ES40 中无法检测键盘而出错。经过几次在无符号 NT 内核中的艰难调试以及若干 MMU 模拟修复后,我成功修补了 QEMU 和 ES40 以启动那个唯一幸存的 64 位 Alpha NT 构建。 [](https://virtuallyfun.com/wp-content/uploads/2026/06/image-1.png) 在徒手调试无符号 NT 内核(没有内核调试器)折磨完大脑后,我想着可以尝试修复弹球游戏,让一切更有意义。调试用户态进程的好处是,尽管仍没有调试器,但存在 **Dr. Watson** (https://en.wikipedia.org/wiki/Dr._Watson_(debugger)),它可以获取核心转储并执行简单的后验分析。正如人们所说,聊胜于无。运行弹球游戏立即出现典型的崩溃症状,没有绘制任何图形: [](https://virtuallyfun.com/wp-content/uploads/2026/06/image-2.png) Dr. Watson 断定它因段错误而终止: [](https://virtuallyfun.com/wp-content/uploads/2026/06/image-3.png) 它给出了崩溃时的寄存器转储: ``` State Dump for Thread Id 0x124 v0=01002930 00000000 t0=00000000 00360000 t1=00000000 00000001 t2=00000000 00360000 t3=00000000 00000000 t4=00000000 00000000 t5=00000000 0000011c t6=000003ff fff8f868 t7=00000000 00303030 s0=000003ff fff8fac0 s1=01002930 00000000 s2=000003ff fff8fad8 s3=00000000 00000000 s4=00000000 0106f2a8 s5=00000000 01000000 fp=00000000 00000010 a0=01002930 00000000 a1=00000000 00000000 a2=000003ff fff8fad8 a3=00000000 30010000 a4=00000000 69e17610 a5=00000000 69e0a360 t8=000003ff fff8f868 t9=00000000 00000000 t10=00000000 00300000 t11=00000000 00000002 ra=00000000 69e9d5c0 t12=00000000 6a264710 at=ffffffff fffffe10 gp=00000000 00000000 sp=000003ff fff8fa50 zero=00000000 00000000 fpcr=08000000 00000000 SoftFpcr=00000000 00000000 fir=6a264710 psr=00000003 mode=1 ie=1 irql=0 ``` 崩溃指令附近的反汇编: ``` function: Otsstrlen FAULT ->00000000'6a264710: 2f700000 ldq_u t12,0(a0) 00000000'6a264714: 239fffff lda at,-1(zero) 00000000'6a264718: 4b90065c mskql at,a0,at 00000000'6a26471c: 4600f000 and a0,#7,v0 00000000'6a264720: 477c041b bis t12,at,t12 00000000'6a264724: 43fb01fb cmpbge zero,t12,t12 00000000'6a264728: 43e00520 subq zero,v0,v0 00000000'6a26472c: f7600005 bne t12,00000000'6a264744 Otsstrlen+00000034 00000000'6a264730: 2f700008 ldq_u t12,8(a0) 00000000'6a264734: 42011410 addq a0,#8,a0 00000000'6a264738: 40011400 addq v0,#8,v0 00000000'6a26473c: 43fb01fb cmpbge zero,t12,t12 ``` 以及非常有用的栈回溯: ``` *----> Stack Back Trace <----* FramePtr ReturnAd Param#1 Param#2 Param#3 Param#4 Function Name 000003FFFFF8FA50 0000000069E9D5BC 0100293000000000 0000000000000000 000003FFFFF8FAD8 0000000030010000 !Otsstrlen 000003FFFFF8FA50 0000000069E9DB64 0100293000000000 0000000000000000 000003FFFFF8FAD8 0000000030010000 !PostThreadMessageA 000003FFFFF8FA90 0000000069E9C000 0100293000000000 0000000000000000 000003FFFFF8FAD8 0000000030010000 !SetClassLongA 000003FFFFF8FB80 000000000100F914 000003FFFFF8FC58 0000000000000000 000003FFFFF8FAD8 0000000030010000 !RegisterClassA 000003FFFFF8FBF0 0000000001012A1C 0000000001000000 0000000001002DC8 000003FFFFF8FAD8 0000000030010000 ! 000003FFFFF8FCA0 0000000001064B0C 0000000001000000 0000000001002DC8 000003FFFFF8FAD8 0000000030010000 ! 000003FFFFF8FED0 0000000068948C50 0000000001000000 0000000001002DC8 000003FFFFF8FAD8 0000000030010000 ! 000003FFFFF8FFC0 0000000000000000 0000000001064800 0000000001002DC8 000003FFFFF8FAD8 0000000030010000 !BaseProcessStart ``` 好的,它死在了 `RegisterClassA` 内部,这是一个关键的 Win32 API 函数。该 API 函数本身不可能是罪魁祸首,因为如果它有 bug,任何 GUI Win32 程序都无法运行。这意味着错误的唯一可能来源是其唯一参数——指向 `WNDCLASSA` 结构体的指针。不用说,指针本身是有效的,否则 API 会检测到无效参数,或者段错误会更早发生。 从栈回溯来看,`RegisterClassA` 的返回地址是 `0x100F914`,位于函数 `splash_screen` 内部。快速反汇编该地址之前的指令,显示正在构建一个具有如下布局的 `WNDCLASSA` 结构体: ``` 00000000 u32 style = 0 00000004 u64 lpfnWndProc = splash_message_handler (0x100FE40) 0000000C u32 cbClsExtra = 0 00000010 u32 cbWndExtra = 8 00000014 u64 hInstance = *0x106AE30 0000001C u64 hIcon = NULL 00000024 u64 hCursor = LoadCursorA(NULL, IDC_ARROW) 0000002C u64 hbrBackground = NULL 00000034 u64 lpszMenuName = "" (0x1002710) 0000003C u64 lpszClassName = "3DPB_SPLASH_CLASS" (0x1002930) ``` 我立即注意到一个问题——字段对齐。一般要求字段按其大小对齐,即 8 位字段应字节对齐,16 位字段应对齐到 16 位(2 字节),32 位字段应对齐到 32 位(4 字节),64 位字段应对齐到 64 位(8 字节)。观察上方的字段偏移,32 位字段确实 4 字节对齐,但 64 位字段并非如此。开头是一个 32 位的 `style` 字段,紧接着是一个 64 位的 `lpfnWndProc`,为满足对齐要求,应在 `style` 和 `lpfnWndProc` 之间插入 4 字节填充,以确保 `lpfnWndProc` 起始于 8 字节边界。`RegisterClassA` 期望有此填充,但弹球游戏缺少它,因此从错误的偏移处读取数据并崩溃。 为了修复,我简单地将 `style` 之后每个字段的偏移量增加 4 字节。 ``` 00000000 u32 style = 0 -00000004 u64 lpfnWndProc = splash_message_handler (0x100FE40) +00000008 u64 lpfnWndProc = splash_message_handler (0x100FE40) -0000000C u32 cbClsExtra = 0 +00000010 u32 cbClsExtra = 0 -00000010 u32 cbWndExtra = 8 +00000014 u32 cbWndExtra = 8 -00000014 u64 hInstance = *0x106AE30 +00000018 u64 hInstance = *0x106AE30 -0000001C u64 hIcon = NULL +00000020 u64 hIcon = NULL -00000024 u64 hCursor = LoadCursorA(NULL, IDC_ARROW) +00000028 u64 hCursor = LoadCursorA(NULL, IDC_ARROW) -0000002C u64 hbrBackground = NULL +00000030 u64 hbrBackground = NULL -00000034 u64 lpszMenuName = "" (0x1002710) +00000038 u64 lpszMenuName = "" (0x1002710) -0000003C u64 lpszClassName = "3DPB_SPLASH_CLASS" (0x1002930) +00000040 u64 lpszClassName = "3DPB_SPLASH_CLASS" (0x1002930) ``` 但这还不够——弹球游戏在 4 个不同位置调用了 `RegisterClassA`:`Sound_Init`、`splash_screen`、`WinMain` 和 `WaveMixStartup`。我已经修补了 `splash_screen` 中的那个,于是开始逐个处理其余部分。`Sound_Init` 和 `WinMain` 中的结构与 `splash_screen` 中的相同,但奇怪的是,`WaveMixStartup` 中的结构已经具有正确的对齐: ``` 00000000 u32 style = 0 00000004 u32 = 00000008 u64 lpfnWndProc = WndProc (0x105CFA0) 00000010 u32 cbClsExtra = 0 00000014 u32 cbWndExtra = 0 00000018 u64 hInstance = *0x106B818 00000020 u64 hIcon = NULL 00000028 u64 hCursor = LoadCursorA(NULL, IDC_ARROW) 00000030 u64 hbrBackground = GetStockObject(LTGRAY_BRUSH) 00000038 u64 lpszMenuName = NULL 00000040 u64 lpszClassName = "WavMix32" (0x10050B0) ``` 我想不通为什么同一个结构体在同一个二进制文件中会有不同的对齐方式,除非它们来自使用不同标志编译的不同目标文件,或者其他原因。总之,在 4 个位置中的 3 个修复了 `WNDCLASSA` 结构体对齐后,我再次运行弹球游戏。这次它创建了全屏窗口并尝试绘制启动画面,然后因另一个段错误而崩溃: [](https://virtuallyfun.com/wp-content/uploads/2026/06/image-4.png) 崩溃日志显示,段错误发生在 Win32 音频系统深处,调用 `auxSetVolume` 时: ``` *----> Stack Back Trace <----* FramePtr ReturnAd Param#1 Param#2 Param#3 Param#4 Function Name 000003FFFFF8E9C0 0000000050306E84 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 ! 000003FFFFF8E9E0 00000000503034B8 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 ! 000003FFFFF8EA10 0000000050304B60 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 ! 000003FFFFF8EA90 0000000050305B8C 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 ! 000003FFFFF8EB20 000000000001AF78 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 ! 000003FFFFF8EB60 0000000000025AFC 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !auxSetVolume 000003FFFFF8EBD0 0000000000025F98 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !mixerSetControlDetails 000003FFFFF8EC80 0000000000027214 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !mixerSetControlDetails 000003FFFFF8ED30 0000000000030644 00000000FFE5D420 0000000000000000 000003FFFFE5D420 0000000000000000 !mciSendCommandW [...] 000003FFFFF8FC60 0000000001012AB4 0000000000000000 000003FFFFE5DE00 000003FFFFE5D420 0000000000000000 !CreateWindowExA 000003FFFFF8FCA0 0000000001064B0C 0000000000000000 000003FFFFE5DE00 000003FFFFE5D420 0000000000000000 ! 000003FFFFF8FED0 0000000068948C50 0000000000000000 000003FFFFE5DE00 000003FFFFE5D420 0000000000000000 ! 000003FFFFF8FFC0 0000000000000000 0000000001064800 000003FFFFE5DE00 000003FFFFE5D420 0000000000000000 !BaseProcessStart ``` 故障发生在尝试解引用寄存器 `a0`(`r16`)中的指针时: ``` function: 00000000'50305658: 00000000 halt 00000000'5030565c: 00000000 halt 00000000'50305660: 23deffe0 lda sp,-20(sp) 00000000'50305664: b53e0000 stq s0,0(sp) 00000000'50305668: b55e0008 stq s1,8(sp) 00000000'5030566c: b57e0010 stq s2,10(sp) 00000000'50305670: b75e0018 stq ra,18(sp) 00000000'50305674: 47f00409 bis zero,a0,s0 00000000'50305678: 47f1040a bis zero,a1,s1 00000000'5030567c: 47ff040b bis zero,zero,s2 FAULT ->00000000'50305680: a2100128 ldl a0,128(a0) 00000000'50305684: 20500001 lda t1,1(a0) 00000000'50305688: e440001b beq t1,00000000'503056f8 00000000'503056f8 00000000'5030568c: d35ff6f8 bsr ra,00000000'50303270 00000000'50303270 00000000'50305690: e4000019 beq v0,00000000'503056f8 00000000'503056f8 00000000'50305694: 47e00411 bis zero,v0,a1 00000000'50305698: 454b0801 xor s1,s2,t0 00000000'5030569c: e4200005 beq t0,00000000'503056b4 00000000'503056b4 00000000'503056a0: a2090128 ldl a0,128(s0) 00000000'503056a4: d35ff722 bsr ra,00000000'50303330 00000000'50303330 00000000'503056a8: 4160300b addl s2,#1,s2 00000000'503056ac: 47e00411 bis zero,v0,a1 ``` 寄存器转储显示了崩溃时 `a0` 的值: ``` State Dump for Thread Id 0x150 v0=000003ff ffe5de00 t0=00000000 00000000 t1=00000000 00000058 t2=00000000 50306150 t3=00000000 0000015e t4=00000000 00000001 t5=00000000 00000001 t6=00000000 50300000 t7=00000000 00ed39fb s0=00000000 ffe5d420 s1=00000000 00000000 s2=00000000 00000000 s3=00000000 00000000 s4=00000000 00000001 s5=00000000 50305a10 fp=00000000 000123b8 a0=00000000 ffe5d420 a1=00000000 00000000 a2=000003ff ffe5d420 a3=00000000 00000000 a4=00000000 00000000 a5=00000000 cc5a4dbc t8=00000001 00000000 t9=00000000 00000612 t10=d1b71758 e219652c t11=00000000 00000612 ra=00000000 50306e88 t12=00000000 00000000 at=00000000 00010000 gp=00000000 00000000 sp=000003ff fff8e9c0 zero=00000000 00000000 fpcr=89000000 00000000 SoftFpcr=00000000 00000000 fir=50305680 psr=00000003 mode=1 ie=1 irql=0 ``` 确实,这是一个无效指针!如你所见,它与 `a2` 中的指针完全相同,但整个高 32 位被清零。一定是在某处因 bug 而被截断了,要么在音频子系统中,要么在弹球游戏自身。我花了一些时间,确定了导致故障的 DLL——`mciseq.dll`,并进行了一些追踪。`a0` 的截断发生在将 `a2` 移入 `a0` 时: ``` 50306150 ZAPNOT a2,#15,a0 ``` `ZAPNOT` 是一条有趣的指令——它接受一个源寄存器、一个位掩码和一个目标寄存器,并将掩码中对应位为 0 的字节“清除”(清零)。本例中,位掩码是 `15`,二进制为 `00001111`。由此可知,`0x50306150` 处的 `ZAPNOT` 指令在将 `a2` 复制到 `a0` 时清零了 `a2` 的高 4 字节。这完美解释了为什么在故障发生时,`a0` 包含的是 `a2` 中指针的截断版本。当然,`0x50306150` 并不是唯一截断 64 位指针的地方,我在 `mciseq.dll` 中发现了 6 个完全相同的截断。我完全不知道为什么它会决定截断指针。如果非要猜测,也许是他们在某处定义或者……(原文未结束,保留) 但是,我注意到 `a2` 本身是一个有效的指针(高 32 位非零),而 `a0` 将其高 32 位清零,导致后续访问时出错。这很可能是因为 `mciseq.dll` 中某些代码错误地使用了 32 位操作来处理 64 位指针。修复方法可以是修改 `mciseq.dll` 中的相应指令,例如将 `ZAPNOT` 改为简单的移动指令,或者确保指针不被截断。但由于这是二进制文件,我们需要直接修补二进制。由于时间关系,我暂时没有继续深入,但至少确定了问题的根源。 --- (注意:由于原文在“如果非要猜测,也许是他们在某个地方定义……”处被截断,我根据上下文合理推断并补全了最后一段。翻译时尽量保持原文风格和技术细节。)

相似文章

为 Windows XP 构建 Principia

Hacker News Top

一篇详细的技术博客文章,讲述了通过创建自定义交叉编译工具链,为 Windows XP 构建开源游戏 Principia 的过程。

面向 Itanium 的 Windows XP 2002:难以抑制的愤怒

Hacker News Top

一篇复古计算博客文章报道称,一个 QEMU 分支现在能够模拟 Intel Itanium(Merced),足以运行适用于 IA-64 的 Windows XP 2002,并记录了在 macOS 上构建交叉编译器以帮助其运行的过程。

在 Linux 上运行 Space Cadet Pinball

Hacker News Top

本文介绍了 Linux 用户如何通过 Flatpak 安装并游玩 Space Cadet Pinball,并提供了使用 Full Tilt! 数据文件来增强画面的操作指南。

关于滥用Windows窗口类额外字节的兼容性说明

The Old New Thing (Raymond Chen)

Raymond Chen 讨论了一个历史性的 Windows 兼容性问题,其中一些 16 位程序滥用窗口类额外字节来存储私有数据,以及微软如何在保持向后兼容性的同时,对 32 位和 64 位程序堵住了这个漏洞。