The Mystery of the Wizard's Castle REM Statement
The Mystery of the Wizard's Castle REM Statement
The Mystery of the REM Statement
在 1980 年的微型计算机游戏 The Wizard's Castle(为 Exidy Sorcerer 编写)中,第一行源代码显示为 10 REM"_(C2SLFF4。虽然它看起来像是一个拼写错误或乱码,但这一行实际上是一个伪装成 BASIC 注释的 Z80 机器码例程,用于模拟 RANDOMIZE 函数,为游戏的伪随机数生成器 (PRNG) 提供种子。
How the Machine Code was Hidden
在 Exidy Sorcerer 上,BASIC 解释器会对命令进行分词(tokenize),并将其作为链表存储在从地址 469 开始的 RAM 中。程序节点的结构包括下一个指针、行号、分词后的命令以及该行的文本。
对于程序的第一行:
- 地址 469-470:下一个指针
- 地址 471-472:行号 (10)
- 地址 473:
REM标记 (0xC3) - 地址 474:
REM文本的开始
通过使用 USR() 函数——该函数通过在地址 260 和 261 处的 POKE 命令跳转到指定的机器码地址——游戏将 REM 语句的文本作为实际的 Z80 指令执行。具体来说,代码 POKE 260,218: POKE 261,1 告诉 USR() 函数跳转到地址 474,而这正是 REM 文本开始的地方。
Dissecting the Z80 Routine
最初尝试将 REM 文本作为 ASCII 进行反汇编的尝试失败了,因为原始杂志列表中的字符并非 ASCII 编码。当对磁带镜像中的实际字节进行分析时,该例程被揭示为:
LD A,R ; Copy R register to accumulator
JR Z,-4 ; If result is 0, jump back to previous instruction
LD (F7FF),A ; Copy accumulator into address F7FFh
RET ; Return to BASIC
The PRNG Seeding Process
- Entropy Source: 该例程读取
R寄存器,该寄存器在指令获取期间频繁递增,根据用户开始游戏的准确时间提供一个半随机值。 - Zero-Check: 它确保值不为零,以避免糟糕的 PRNG 种子。
- Memory Mapping: 该值存储在地址
0xF7FF,对应于内存映射屏幕文本的右下角字符位置。 - BASIC Retrieval: 然后 BASIC 代码使用
T = PEEK(-2049)(其中 -2049 是 0xF7FF)来读取此值,并使用Q = RND(-(2*T+1))来为RND()函数设置种子。
由于 R 寄存器仅递增其低 7 位,这种技术限制了游戏只能生成 127 个可能的随机地牢。
The "Typing-In" Paradox
在 20 世纪 80 年代,程序员直接从杂志上输入代码。然而,如果将 The Wizard's Castle 中的 REM 语句作为字面 ASCII 字符输入,它是不可能起作用的。对于原始用户是如何实现这一点的,有两种可能的解释:
- Direct POKING: 作者可能输入了一个占位符
REM,然后使用POKE命令手动将非 ASCII 字节插入内存。 - Special Key Combinations: 研究表明,Exidy Sorcerer 拥有
Graphic+Shift组合键,允许用户输入 0x80 到 0xFF 的标记和数值,这些数值会映射到列表中看到的字符。
Technical Insights from the Community
对该技术的分析突显了早期家用计算机的确定性特征,即固定的 RAM 位置和缺乏多处理能力使得 BASIC 和机器码能够无缝融合。
"Most type-in hybrid BASIC-machine language programs would have numerous DATA statements with opcodes and a loop to READ and POKE them... Commodore 64 BASIC's LOAD"WHATEVER",8 command would load a file of any size in the BASIC text area without validation." — @RiverCrochet
类似的技术也被用于其他平台,例如 Sinclair ZX81,在缺乏专用内存块或 DATA 语句的情况下,使用 REM 语句或其他被忽略的行来存储机器码。