Hack 指令集架构(ISA)
← 入门教程 · Hack 概览 · English · 汇编器内部原理 →
本文只讲 Hack ISA 本身:软件能够看到什么状态、机器指令怎样编码,以及一条指令怎样改变状态。详细位图以默认 hack16 profile 为基线,profile 差异会显式标出。运行命令、断言和 .hack 元数据等工具规则见 Hack 包参考手册。
ISA 到底是什么
ISA(Instruction Set Architecture,指令集架构)是软件与处理器实现之间的契约。它规定:
- 处理器有哪些对程序可见的寄存器和内存;
- 每个机器字怎样解码;
- 每条指令读取什么、计算什么、写回什么;
- 程序计数器怎样前进或跳转。
ISA 不规定加法器由多少个 NAND 门组成、信号需要多久稳定,也不规定流水线或缓存怎样实现。这些属于微架构或电路实现。只要两个处理器对相同机器状态和机器指令产生相同的架构结果,它们就可以实现同一个 ISA。
本项目涉及的几个层次不要混淆:
最终送给模型 CPU 的始终只有所选 profile 的 A 指令或 C 指令。命令默认选择 hack16,--profile hack32 选择扩展。
体系结构概览
两个 profile 都采用分离的指令存储和数据存储:程序从 ROM 取指,数据从 RAM 读写;这通常称为 Harvard 结构。它们共享 15 位 PC/地址空间,但机器字宽度不同。
架构状态
汇编语言中的 M 不是独立寄存器,而是当前 A 地址指向的内存:
标准汇编器符号 R0..R15 是 RAM[0]..RAM[15] 的别名,不是额外的架构寄存器。例如,@R3 把地址 3 装入 A,后续 D=M 读取的就是 RAM[3]。
hack16 的 A 为 16 位,A 指令携带 15 位立即数;hack32 的 A 为 32 位,A 指令携带 31 位立即数。两个 profile 的 RAM 地址与跳转目标都只使用 A[14:0];hack32 的 A 高位仍参与 32 位 ALU 数据运算。
Hack 平台的内存映射
nand2tetris 的完整 Hack 平台通常把数据地址空间解释为:
Sail 模型把全部 0..32767 地址实现为普通 RAM。指令寻址规则属于模型范围,屏幕刷新和键盘输入不属于模型范围。
机器指令格式
每个 profile 都只有 A、C 两类指令。下面位图展示 canonical 16 位 hack16 布局;hack32 在更宽的编码中复用同一组 C 字段。
A 指令
标准汇编写作:
执行语义:
例如 @42 编码为:
A 指令中的值可以是十进制数或符号,符号解析由汇编器完成。hack16 编码 0 加 15 位立即数;hack32 编码 0 加 31 位立即数。
C 指令
标准汇编形式为:
三个字段分别决定:
a + comp:ALU 计算什么;dest:结果写入A、D、M中的哪些位置;jump:是否把PC改为旧A指定的地址。
dest 和 jump 可以省略,但 comp 必须存在。hack32 把上面的 16 位 C 布局放在机器字低半部,高半部固定为 0xFFFF,完整编码为 1111111111111111 111accccccdddjjj。
comp:ALU 运算
ALU 的固定输入 x 是 D。a=0 时 y=A,a=1 时 y=M=RAM[A]。
下表列出汇编器接受的 canonical mnemonic。— 表示该组合没有规范汇编写法。门级 control ALU 对 cccccc 的全部 64 个值都有总函数语义,因此合法 C 机器字可以解码并执行汇编器从不生成的 control 值。
位运算和算术结果按所选 profile 的机器字宽度截断:hack16 按模 2^16 回绕,hack32 按模 2^32 回绕。Hack 没有独立状态标志寄存器;跳转条件直接检查本次 ALU 输出是否为零以及最高位是否为一。
dest:写回目标
ddd 从高到低分别是 A、D、M 的写使能:
多个目标在架构上视为同时写回。尤其是 AM=... 或 AMD=...:虽然 A 会得到新值,M 仍写入指令开始时 old_A 指向的位置。
jump:条件跳转
跳转把 ALU 输出 out 当作所选 profile 宽度的二进制补码值判断:
条件成立时,目标是指令开始时 A 的低 15 位,而不是 ALU 输出,也不是写回后的新 A。
一条指令如何执行
可以把 Sail 中的 execute 理解为下面的架构伪代码。
A 指令
C 指令
保存 old_A 是最关键的细节。例如:
如果条件成立,这条指令会同时:
- 计算
D+1; - 把结果写入
A; - 把结果写入旧
A指向的 RAM; - 跳转到旧
A指向的 ROM 地址。
这正是共享 model/core.sail 在 C 指令路径先保留旧 A 的原因。
标准汇编怎样变成机器码
汇编器使用两遍处理:
- 解析源码,并先把 Hack+ 伪指令展开为标准 A/C 指令;
- 第一遍记录每个
(LABEL)对应的 ROM 地址;标签自身不占 ROM; - 第二遍解析 A 指令符号并编码每条机器指令;
- 未预定义的变量符号从 RAM 地址
16开始依次分配。
预定义符号包括:
R0..R15;SP=0、LCL=1、ARG=2、THIS=3、THAT=4;SCREEN=16384、KBD=24576。
对于 hack16,A 指令编码为 0 加 15 位值,C 指令拼接为:
对于 hack32,A 指令编码为 0 加 31 位值;C 指令编码为 0xFFFF 加同一条 canonical 16 位 C 编码。
例如:
对应 comp=D、dest=M、无跳转,因此机器码为:
Hack+ 如何降级为正式指令
Hack+ 的展开在标签解析和机器码编码之前完成。展开后的每一行使用 canonical nand2tetris A/C 汇编语法,最终机器字则采用所选 profile 的编码。
几个容易忽略的点:
SET R0, R1把符号R1的地址值1写入R0;复制RAM[R1]应使用MOV R0, R1;- 二元内存运算采用目标在前的顺序:
SUB R0, R1表示RAM[R0] -= RAM[R1]; MOV、二元内存运算和条件伪指令都会覆盖D;JNZ与JNE等价,后者与正式 Hack 跳转助记符一致;HALT不是 Hack 指令。汇编器为每个HALT生成唯一的__HACKPLUS_HALT_n标签和两指令自循环,并把该 ROM 地址记录为执行元数据;本项目 executor 到达该地址时结束,而实现所选 profile 的机器会停留在自循环中。
完整展开示例
源码:
概念上的标准 Hack 汇编结果:
随后第一遍汇编才计算 DONE 和私有 HALT 标签的 ROM 地址,第二遍再生成所选 profile 的机器字。因此,伪指令展开产生的额外指令会真实占用 ROM 地址,标签地址也以展开后的程序为准。
这份 ISA 如何映射到 Sail
当前模型把共享语义与 profile 位布局分开:
C 指令字段使用命名位向量类型,而不是匿名的 bit / bits(n) 位置。这使 decoded instruction、alu 和 should_jump 的接口能够直接表达字段角色,同时不改变原始编码域。
decode_hack 先检查合法性,再调用 profile mapping。hack16 中最高位为 0 的机器字是 A 指令,只有 111 前缀是 C 指令,100、101、110 非法。hack32 中最高位为 0 的机器字是 A 指令;C 指令必须同时满足高 16 位为 0xFFFF、低半字以 111 开头,其他最高位为 1 的机器字全部非法。非法机器字会在进入 execute 前抛出 HackIllegalInstruction(word),既保留原始字,也不提交架构状态。
C 前缀合法后,六位 ALU control、a、dest、jump 的所有组合都属于机器编码域。共享 alu 按所选机器字宽度定义全部 64 种 control 结果;汇编器则有意只生成上表的 canonical mnemonic 子集。汇编语言词表不能替代机器码合法性定义。
兼容性边界
hack16 是 canonical nand2tetris 基线。hack32 不是普通 nand2tetris .hack 格式:机器字为 32 位,A 立即数为 31 位,C 指令带额外的 0xFFFF 高半字。汇编 mnemonic 相同不代表二进制可互换;必须显式选择 hack32,并使用理解 profile manifest 的 loader。
模型边界
理解测试结果时要区分完整 Hack 平台与本可执行模型:
- ROM 由所选 profile composition 声明;生成的 driver 初始化 profile 宽度的原始机器字,
fetch_hack和hack_step负责取指与分派; - RAM 没有 Screen、Keyboard 的设备副作用;
HALT、Hack+、.assert和.max_steps都属于工具层,不属于 Hack ISA;- 模型描述架构状态转换,不模拟门延迟、时钟边沿细节或 nand2tetris HDL;
- 模型通过 Sail C 后端执行,不声明已经完成形式化等价证明。
对照源码学习
- 先看本文的 A/C 编码和执行伪代码;
打开一个
model/profiles/文件,沿 include 进入model/core.sail,再回到 profile 的 mapping clauses;编码直接使用encdec(instruction),执行路径依次阅读fetch_hack→decode_hack→encdec(word)→execute→hack_step; - 查看
programs/basic_alu.asm,对照标准汇编与 Hack+; - 执行
pixi run just hack assemble basic_alu,查看isa/hack/.build/hack16/asm/basic_alu/basic_alu.hack;加--profile hack32可查看.build/hack32/asm/basic_alu/basic_alu.hack; - 阅读
tests/sail/hack16/conformance.sail与tests/sail/hack32/conformance.sail,观察 profile 规则如何直接变成测试。
规范来源
- nand2tetris Chapter 4 / Project 04:Hack 机器语言;
- nand2tetris Project 05:Hack CPU、Memory 与 Computer;
- nand2tetris Project 06:Hack 汇编器;
- Sail Language Reference:Sail 语言与后端。
下一步:选择一个扩展,进化你自己的 Hack。