1980年7月,《Recreational Computing》杂志刊登了一款给Exidy Sorcerer电脑写的BASIC游戏《The Wizard's Castle》。程序第一行是这样的:
10 REM"_(C2SLFF4
按BASIC语法,这只是一行注释,给人看的,解释器不会执行它。但四十多年后,有人翻出这行字符较真起来,发现它根本不是排版事故或输入错误——它是一段能被处理器直接执行的Z80机器码,伪装成了注释文本的样子。
一行"注释",其实是一个跳转地址
线索藏在后面几行代码里。程序用POKE把两个字节写进260和261地址,再调用USR(0)——这是BASIC里调用机器码子程序的标准写法。把这两个字节按小端序拼起来,得到的地址是474。
问题是,474这个地址上到底有什么?
Sorcerer的BASIC解释器把每一行代码存成链表节点:2字节"下一行"指针,2字节行号,1字节REM的token值,紧接着才是注释正文的第一个字符。逐字节数下来,469到474正好落在这行代码上——474恰好是REM文本的第一个字符。
- 结论.
USR(0)跳转的地址,精确对齐在这行"注释"的第一个字节上,这不是巧合,是作者故意算好的内存偏移。
也就是说,这行REM从来不是给人看的说明文字,它是一段被"伪装"成合法BASIC语法、实际上会被当成可执行指令读取的机器码。屏幕上显示的"_(C2SLFF4只是这些字节碰巧对应的ASCII字形,不是它们的真实含义。
随机数,是从屏幕角落"偷"来的
跳到474之后,真正执行的指令是这样一套逻辑:读取Z80的R寄存器(每执行一条指令就自增一次的计数器),如果结果是0就重来一次,不为0就写进内存地址0xF7FF——这个地址恰好是屏幕右下角显示的那个字符。
写完之后,RET返回BASIC,程序马上用PEEK(-2049)(也就是0xF7FF的有符号写法)把这个值读出来,喂给RND()当种子。整个过程,是把处理器一个几乎"顺手"的计时副作用,伪装成屏幕上一闪而过的字符,再假装是从屏幕上读出来的随机数。
但R寄存器只有低7位持续变化,也就是最多128种取值,再排除会导致种子退化的0,这台机器上真正能生成的随机地牢,理论上限只有127种。
真正让我觉得有意思的,不是这段代码有多聪明,而是它暴露的一个断层:杂志印出来的字符,不等于能跑起来的字节。研究者第一次照着ASCII字形硬翻译这些字符,得到的是一堆语无伦次的指令,根本跑不通——因为屏幕显示的字形和内存里真实的字节,从一开始就不是一一对应的关系。这也解释了为什么原始杂志只留下一句提示——"第一条注释是模拟随机函数的机器语言例程"——却没有说清楚读者照着字面敲进去到底能不能用。很可能当时的读者还需要额外手动POKE几个字节才能让它真正工作,这一步杂志压根没写。
80年代的型录式游戏靠读者逐字敲代码复活,后来杂志普遍改用校验和(比如后来流行的MLX式机器码校验)来防止这种"敲对了字形、敲错了字节"的问题,某种程度上就是被这类事故逼出来的补救机制。
印在纸上的代码,只是这台机器的一种投影,不是它本身。
这行REM真正值得记住的,不是某个作者秀了一手把戏,而是那个年代的软件和硬件贴得有多近——解释器的内存布局、屏幕显示的字节、处理器的计时副作用,全部拧成一根绳,少一环都不转。至于杂志上反复出现的那几个"F4"到底是笔误还是另有玄机,原始的调查记录到这里也没给出答案,只能留在原地。
