


不知道上面这几张图片有没有勾起大家的童年记忆。在那段游戏机被禁售的年代,不知是哪位大能造出了一款可以在背后插手柄的 VCD,再搭配一张写着"中文游戏 300 合 1"的光盘,让小时候的我如痴如醉。后来我才知道,这些盗版游戏甚至漂洋过海,远销东欧,壮哉我大天朝……
但这段独特的记忆里,也伴随着一些挥之不去的"影子"。下面有两段音乐,大家可以先听一下,看是否能秒懂我的意思:
还有一段——到结尾处会稍微"正常"一点的版本:
这就是(我不知道它是否著名,反正你小时候要是听过,这辈子都很难忘记的)盗版超级马里奥城堡关底的胜利音乐。不知道你听到的是哪个版本?我听的是第一种。

这也让我很长一段时间里都以为,超级马里奥城堡关底的音乐本来就长这样——阴森、诡异,倒也莫名契合城堡关卡与之前关卡截然不同的独特氛围。直到有一天,我偶然听到了真正的关底音乐,当时我的第一反应竟然是:"这个人玩的版本怎么这么正常、这么好听?"这件事也成了我少年时代的未解之谜之一。不管怎么说,这段诡异的音乐有没有给我留下童年阴影暂且不论,它肯定已经成为我童年记忆中独特的一部分。
那么,这段怪异音乐到底是怎么来的呢?今天我们就来浅浅地探究一下。
ROM比对
我们可以直接把盗版和正版的超级马里奥一代 ROM 拿来比对:




比对下来,大致能发现上面四处差异。前三处位于 PRGROM,也就是代码和数据部分;第四处位于 CHRROM,也就是素材部分。本文只关注 PRGROM 的差异,我们逐一来看这些修改都造成了什么影响。
反编译
光盯着这些十六进制数据肯定看不出门道,我们需要把它们翻译成对应的汇编代码或数据结构。这些工作前人已经做过,我们可以直接参考:
https://6502disassembly.com/nes-smb/SuperMarioBros.html
来看到这些数据代码部分的反编译结果。
需要注意的是,PRGROM 会被加载到内存 8000–FFFF 区域,上面的反汇编也是按照这个加载地址来标注的,所以要小心 ROM 文件偏移和内存地址之间的映射关系。这里直接给出结论:
加载地址 = ROM 文件偏移 + 0x8000 − 0x10
其中 0x10 是 ROM 文件头的大小,这部分不会被加载进内存。
第一段

先来看第一处差异,直接对照反汇编代码:


先对上面的图片做下说明,左边的是正版的反编译代码,右边的是盗版的。
上面高亮的两处修改都位于 NMI 处理程序内部。NMI 处理程序可以理解为定时中断处理例程,更通俗地说,就是一个每隔一段固定周期就会被执行一次的函数。它通常用来刷新屏幕、管理缓冲区、维护定时器等等。
这两处修改具体做了什么呢?
第一处,把"将 PPUMASK 对应的值存入 A 寄存器"的指令替换成了"跳转到 $FE5D 并执行那里的代码"。而 $FE5D 正好落在被修改过的那段 ROM 数据范围内——我们先把这个地址记下来,后面再谈。
第二处,把跳转的目标地址从原本处理"声音开关"的逻辑,偏移到了原地址 +1 的位置。细节不展开,结果就是:原本在标题画面被禁用的游戏音效,被强行启用了。
第三处修改对应下面这个标签:

也就是说,原本指向 GameMenuRoutine 的调用,全部被 HOOK 到了 $FE00。GameMenuRoutine 本来负责处理游戏开始菜单的逻辑,而盗版厂商就是在这里"偷天换日",把选关的逻辑塞了进去。$FE00 同样落在被修改过的 ROM 数据区域内,我们也先记住它。
第二段

第二处其实只是把游戏画面顶部分数上方显示的 "MARIO" 换成了 "SCORE" 这几个字符:

第三段

第三处差异,才是破坏水下关卡和城堡胜利音乐的罪魁祸首。我们可以看到,这里修改了 7E10–7ED3 的数据,对应内存加载地址 $FE00–$FEC3:

上图两个红框分别对应被破坏区域的起点和终点。可以看到,这段被覆盖的范围,正好吃掉了水下关卡音乐的下半部分,以及城堡胜利音乐的大部分。

还有一个有趣的发现:一部分盗版厂商的卡带,从加载地址 $FEB1 开始会多出一段"根本不会被执行"的代码(因为函数在执行到这里之前就已经返回了)。我猜测是有的厂商发现了这段冗余代码,于是把它还原了回去(对应上图中的绿框)。而被还原的这部分,恰好是城堡关卡其中一个声部的一半音乐数据。
这就解释了,为什么这段诡异的胜利音乐会有两个版本,以及为什么其中一个版本听起来会有"某一个声部的一小段是正常的"。
题外话:声部
如果你仔细观察,会发现 EndOfCastleMusData 被分成了三部分,分别对应方波通道 2(主音)、三角波通道(低音)和方波通道 1(辅音,用来构成简单的和弦)。被"还原"回来的那部分属于辅音,所以即便数据是正常的,由于它不是主旋律,听起来依然怪怪的。
对于上面提到的两段 D 商插入的例程,我不太熟悉 6502 汇编,就交给 AI 帮我们加上注释,方便理解:
FE00-FE5C: 通过手柄控制选关
LDA $06FC ; 读取本帧手柄1的按键状态(SavedJoypad1Bits)
AND #$C0 ; 只保留 A(bit7) 和 B(bit6) 两个键
BEQ NoABPress ; A、B 都没按下 → 跳过递增逻辑
LDX $01E6 ; 检查防连发锁
BNE SetLock ; 锁已生效(上一帧已处理)→ 不重复递增
AND #$80 ; 单独检测 A 键
BEQ CheckBBtn ; 没按 A → 去检测 B
INC $075C ; 按了 A:世界选择计数器 +1
LDA $075C ;
AND #$03 ; 限定在 0~3 范围内循环(共 4 档)
STA $075C ;
CheckBBtn: LDA $06FC ; 重新读原始按键状态
AND #$40 ; 单独检测 B 键
BEQ SetLock ; 没按 B → 直接去加锁
INC $075F ; 按了 B:关卡选择计数器 +1
LDA $075F ;
AND #$07 ; 限定在 0~7 范围内循环(共 8 档)
STA $075F ;
SetLock: LDA #$FF ;
STA $01E6 ; 置防连发锁,按键松开前不再响应
NoABPress: LDY $075C ; 取出世界选择计数器到 Y
BNE ChkLevelSel ; 非 0 → 进入关卡号白名单过滤
STY $0760 ; 为 0 → 清零 WorldNumber(默认 World 1)
JMP $8245 ; 跳回被劫持的原初始化入口
ChkLevelSel: LDA $075F ; 取关卡选择计数器
CMP #$01 ;
NOP ; ← 补丁抹掉(原本是 BEQ UseLevelSel)
NOP ; 值 1 档位已被屏蔽
CMP #$02 ;
BEQ UseLevelSel ; 2 号档位合法
CMP #$04 ;
BEQ UseLevelSel ; 4 号档位合法
CMP #$05 ;
BEQ UseLevelSel ; 5 号档位合法
CMP #$07 ;
BEQ UseLevelSel ; 7 号档位合法
INY ; 此处 Y 原本是 $075C(1~3),INY 后必然非 0
BNE UseLevelSel ; → 实际是一条无条件跳转
AND #$02 ; —— 以下为死代码 ——
BNE UseLevelSel ;
BEQ RejectSel ; (永远执行不到,补丁前版本遗留残骸)
FE5D-FEB0: 选关屏幕反馈
PHA ; 先保存调用方准备写入 PpuMask_2001 的值
LDA $01E6 ; 选关菜单激活标志(由输入钩子置位)
BEQ Exit ; 未激活 → 跳过 HUD 绘制,直接返回
LDA $0314 ; 备份 VRAM_Buffer1+0(原值暂存到栈)
PHA ;
LDA $0316 ; 备份 VRAM_Buffer1+2
PHA ;
LDY $075F ; 取关卡计数器(0~7)
INY ; 显示时 +1,变为 1~8
STY $0314 ; 暂存关卡数字
LDY $075C ; 取世界计数器(0~3)
INY ; 显示时 +1,变为 1~4
STY $0316 ; 暂存世界数字
LDA #$10 ;
STA PpuControl_2000 ; 关 NMI,背景图案表选 $1000,VRAM 自增 +1
LDA #$00 ;
STA PpuMask_2001 ; 强制消隐(熄屏后才能安全直写 VRAM)
LDX PpuStatus_2002 ; 读 $2002 复位 PPU 地址锁存器
LDA #$20 ;
STA PpuAddr_2006 ; 写 VRAM 目标地址高字节
LDA #$73 ;
STA PpuAddr_2006 ; 写低字节 → $2073(状态栏顶部区域)
LDA $0314 ; 取出关卡数字(1~8)
STA PpuData_2007 ; 写入第一个 tile
LDA #$28 ;
STA PpuData_2007 ; 写入分隔符 tile(CHR 中的 "-" 字形)
NOP ; ← 补丁抹掉(原本应是 STA PpuData_2007,
NOP ; 多写一个 tile,例如前导的 "W" 字符,
NOP ; 现在被砍掉,HUD 少显示一位)
LDA $0316 ; 取出世界数字(1~4)
STA PpuData_2007 ; 写入最后一个 tile
PLA ;
STA $0316 ; 还原 VRAM_Buffer1+2
PLA ;
STA $0314 ; 还原 VRAM_Buffer1+0
Exit: PLA ; 取回最初保存的 PpuMask_2001 值
STA PpuMask_2001 ; 按调用方原本的意图恢复渲染
RTS
一句话总结
盗版超玛比正版多了一段"选小关"的逻辑,而当年写这段补丁的程序员在挑选代码存放位置时,不偏不倚地砸在了存放音乐数据的那段内存上。结果就是:APU 在播放音乐时,把这段程序代码当成了乐谱,于是便有了那段让无数人童年难忘的诡异关底音乐。

参与讨论
(Participate in the discussion)
参与讨论
没有发现评论
暂无评论