Hotdry.

Article

逆向解析 Zork 的 Z-machine:文件格式与虚拟机指令集重建

解析 Z-code 文件格式结构、Z-machine 虚拟机架构与指令执行模型,重建经典文本冒险游戏的解析与执行管线。

2026-05-23systems

引言:跨越四十年的技术考古

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 将地址空间划分为三个区域:

  1. 动态内存(Dynamic Memory):地址 0x0000 起,包含头部、全局变量、对象树。运行时可修改,需保存到存档文件。
  2. 静态内存(Static Memory):从头部指定地址开始,包含静态字符串、常量表。只读,可置于 ROM。
  3. 高内存(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 解释器需处理版本差异与平台限制。

核心循环结构

典型解释器的主循环如下:

  1. 从 PC 读取指令首字节,解码操作码类型
  2. 根据类型读取操作数(0-8 个)
  3. 执行对应操作(算术、逻辑、内存访问、IO)
  4. 更新 PC,处理分支或调用 / 返回
  5. 重复直到遇到 quit 指令或非法操作码

IO 抽象层

Z-machine 定义了标准化的 IO 接口:

  • 屏幕输出:支持窗口分割、状态行、文本样式(粗体 / 斜体 / 等宽)
  • 键盘输入:支持单行输入、字符输入、超时读取
  • 文件 IO:保存 / 恢复游戏状态、命令脚本录制

现代解释器(如 Frotz、Bocfel)将这些操作映射到目标平台的图形库或终端 API,实现跨平台兼容。

版本检测与降级

解释器启动时应:

  1. 读取首字节确定版本
  2. 验证文件大小是否符合该版本规范
  3. 检查标志位确认特性支持
  4. 根据版本选择对应的解码表和内存布局

V6 文件包含图形资源,解释器可选择性忽略或渲染占位符,保持游戏可玩性。

结语:逆向工程的技术遗产

Z-machine 的逆向工程是软件考古学的经典案例。通过分析二进制文件、对比不同版本的行为差异,社区重建了一套完整的虚拟机规范。这一过程不仅保存了《Zork》等经典游戏,也为现代交互式小说平台(如 Inform 7)奠定了基础。

对于今天的开发者,Z-machine 提供了宝贵的启示:在资源受限环境中,精心设计的抽象层可以实现惊人的可移植性。其文件格式的紧凑编码、虚拟机的栈式执行、对象树的层级建模,至今仍是嵌入式系统与游戏引擎设计的参考范式。


参考来源

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com