引言:跨越四十年的技术考古
1979 年,Infocom 团队在开发《Zork》时面临一个核心问题:如何让同一款游戏在 TRS-80、Apple II、Commodore 64 等差异巨大的硬件上运行?他们的解决方案是创造 Z-machine—— 一个抽象的虚拟机架构,将游戏逻辑编译为与平台无关的字节码。这一设计比 Java 虚拟机早了十五年,堪称早期跨平台技术的典范。
然而,Infocom 从未公开 Z-machine 的完整规范。现代开发者能够运行《Zork》三部曲,全靠社区对游戏文件的逆向工程。本文将解析 Z-code 文件格式结构、虚拟机指令集与执行模型,为重建这一经典引擎提供技术路径。
Z-code 文件格式:紧凑的二进制容器
Z-code 文件是 Z-machine 的「故事文件」(story file),扩展名从 .z1 到 .z8 对应不同版本。文件首字节即为版本标识,直接决定了解析策略。
头部结构(Header)
文件前 64 字节构成固定头部,包含关键元数据:
- 字节 0:Z-machine 版本(0x03 表示 V3,0x05 表示 V5)
- 字节 1-2:标志位,指示状态行、分屏等特性支持
- 字节 4-5:程序计数器(PC)初始值
- 字节 6-7:全局变量表起始地址
- 字节 8-9:静态内存起始地址(动态内存上限)
版本差异显著影响文件布局。V3(ZIP)限制最多 255 个游戏对象,V5(XZIP)扩展至 65535 个,V6(YZIP)更引入图形与音效支持。这种演进反映了 Infocom 从「内存受限的 8 位机」向「功能丰富的 16 位机」的适应过程。
内存布局模型
Z-machine 将地址空间划分为三个区域:
- 动态内存(Dynamic Memory):地址 0x0000 起,包含头部、全局变量、对象树。运行时可修改,需保存到存档文件。
- 静态内存(Static Memory):从头部指定地址开始,包含静态字符串、常量表。只读,可置于 ROM。
- 高内存(High Memory):存放可执行代码,从底部向上增长。
这种分层设计允许解释器将只读数据映射到 ROM,显著降低 RAM 占用 —— 在 1980 年代,这是让《Zork》能在 16KB 内存机器上运行的关键。
虚拟机架构:栈式执行与对象树
Z-machine 采用基于栈的指令集架构,核心组件包括程序计数器、求值栈、局部变量帧和对象树。
指令编码与操作数
Z-machine 指令采用变长编码。首字节的高两位区分操作数类型:
- 0b00:2OP,双操作数指令
- 0b01:1OP,单操作数指令
- 0b10:0OP,零操作数指令
- 0b11:VAR,变长操作数指令
操作数本身支持三种寻址模式:
- Large constant(16 位立即数)
- Small constant(8 位立即数)
- Variable(从变量编号读取,0x00 表示栈顶,0x01-0x0F 表示局部变量,0x10 起表示全局变量)
这种灵活的编码方式在代码密度与执行效率间取得平衡。
对象树与属性系统
游戏世界中的房间、物品、NPC 均以「对象」形式存在。V3 的对象结构包含:
- 属性标志:32 个布尔属性(如「可移动」「发光」)
- 属性值:变长存储的数值属性
- 关系指针:父、兄弟、子对象链接,构成树形结构
- 属性地址:指向对象名称与描述文本
对象树支持高效的层级查询。例如「取出背包中的灯」涉及:找到玩家对象→遍历子对象链→匹配「灯」对象→验证「可移动」属性。
分支与例程调用
Z-machine 支持条件分支和子例程调用。分支指令编码跳转偏移量,支持正向或向后跳转。例程调用采用栈帧机制:调用时压入返回地址和局部变量,返回时恢复先前状态。
这种设计与现代 CPU 的调用约定异曲同工,只是实现更为精简。
ZSCII 文本压缩:在字节中塞入更多字符
Z-machine 的文本存储采用 ZSCII(Zork Standard Code for Information Interchange)编码,这是一种针对英文文本优化的压缩方案。
三字符编码机制
ZSCII 将每 2 字节(16 位)划分为三个 5 位块,可编码 3 个「字符」。第 16 位作为「字符串结束」标志。5 位可表示 32 种字符,覆盖常用字母和标点。
对于高频字符(空格、句号、逗号),ZSCII 使用 5 位直接编码;低频字符则通过「转义序列」扩展:一个 5 位块指示「切换字符集」或「输出缩写」,后续块提供索引。
缩写表与字符串缓存
V3 支持 96 个缩写词(如「the」「and」「you」),存储于文件头部的缩写表中。编码时,2 位指示缩写集,5 位指示表索引,单个 5 位块可展开为完整单词。
这种压缩使 Z-code 文件的文本密度达到约 1.7 字符 / 字节,在当时显著降低了磁盘占用。
现代实现:解释器架构与兼容策略
重建 Z-machine 解释器需处理版本差异与平台限制。
核心循环结构
典型解释器的主循环如下:
- 从 PC 读取指令首字节,解码操作码类型
- 根据类型读取操作数(0-8 个)
- 执行对应操作(算术、逻辑、内存访问、IO)
- 更新 PC,处理分支或调用 / 返回
- 重复直到遇到 quit 指令或非法操作码
IO 抽象层
Z-machine 定义了标准化的 IO 接口:
- 屏幕输出:支持窗口分割、状态行、文本样式(粗体 / 斜体 / 等宽)
- 键盘输入:支持单行输入、字符输入、超时读取
- 文件 IO:保存 / 恢复游戏状态、命令脚本录制
现代解释器(如 Frotz、Bocfel)将这些操作映射到目标平台的图形库或终端 API,实现跨平台兼容。
版本检测与降级
解释器启动时应:
- 读取首字节确定版本
- 验证文件大小是否符合该版本规范
- 检查标志位确认特性支持
- 根据版本选择对应的解码表和内存布局
V6 文件包含图形资源,解释器可选择性忽略或渲染占位符,保持游戏可玩性。
结语:逆向工程的技术遗产
Z-machine 的逆向工程是软件考古学的经典案例。通过分析二进制文件、对比不同版本的行为差异,社区重建了一套完整的虚拟机规范。这一过程不仅保存了《Zork》等经典游戏,也为现代交互式小说平台(如 Inform 7)奠定了基础。
对于今天的开发者,Z-machine 提供了宝贵的启示:在资源受限环境中,精心设计的抽象层可以实现惊人的可移植性。其文件格式的紧凑编码、虚拟机的栈式执行、对象树的层级建模,至今仍是嵌入式系统与游戏引擎设计的参考范式。
参考来源
- IFWiki - Z-machine:虚拟机架构与版本差异概述
- Zarfhome Blog - Compiling for the Z-machine version 3:版本演进与编译器实现细节
- Graham Nelson, The Z-Machine Standards Document:Z-machine 规范标准文档
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。