存储
D、A、M
Hack 机器包含两个16位寄存器——D 和 A,分别是 Data 和 Address 的缩写。他们本质上是一样的,用途的不同是分配出来的。
如当访问 M 这个符号的时候,总是以 A 寄存器的值作为地址去操作内存。即 M 解释为 Memory[A]
。
又如 goto 指令,跳转到的位置也是指令存储器的 A 地址,即 goto 跳到 InstructionMemory[A]
。
最后,A 寄存器也可以像 D 寄存器一样用于存储数据值,而到底它存的是什么含义,其实取决于上下文的解释。
指令
从寄存器 A 的用法上可以看出,Hack 程序的很大一部分指令应该都是在操作地址。写一个地址,操作一下,写一个地址,操作一下这样。因此他的指令也分为两类:
A 指令
A 指令的第一位是 0,后面跟 15 位值。表示将 A 寄存器设为这个值。
它的意义首先是允许了输入常数(15位),这是在将 A 寄存器的值做数字处理的时候;其次为操作内存、跳转指令 提供地址。
因此也可以推导 Hack 机器的内存地址空间是 15 位的。
C 指令
C 指令第一位是 1,之后两位没有使用。后面的 13 位被分为三个域:7 位的计算域(Comp)、3 位的目标域(Dest)、3 位的跳转域(Jump)。
为什么空出两位没有使用,而目标域和跳转域都是 3 位呢?
目标域是因为 C 指令操作的存储对象只有三个:A 、D 、M。每个对象都需要表达 “存入” 或 “不存入” 两种选择,因此需要三个位的域宽。
因为计数器的存在,程序的默认执行流程是顺序执行的,即执行完 ROM[N]
后执行 ROM[N+1]
。跳转域需要表达下一步指令是否需要跳转的问题,且跳转的本质是实现流程控制,故该域还需要能够对计算域输出的值做出反应。计算域的输出值范围有 16 位宽,判断条件则是一个确定的数,比如 10086。那么输出值和判断条件之间的关系就应该定义三种:小于、等于和大于。这三种关系的输出值之间互相独立,故每种都需要表达 “跳” 或 “不跳” 两种选择,因此需要三个位的域宽。
另外两点:1. 至于说输出值和判断条件之间的关系为什么定义三种,是出于效率考虑的,两个数字之间的关系也可以表达为 两种:等于或不等于。但这样在做大于或小于判断的时候就会非常麻烦。 2. 因为指令已经没有空间存放判断条件的比较数,因此跳转域实际使用的条件值是 0,即你需要先减一次比较数。
计算域有 7 位,用于控制 ALU。记得 A 寄存器有两种意义吗,它既可以解释为值,也可以解释为地址。这个解释操作其实就是用计算域的第一位控制的,通常管他叫 a-bits,为 0 时表示输入值,为 1 时表示输入 M[A] 的值。剩余 6 为称为 c-bits ,用于控制 ALU 函数的选择。具体对应需要参考表 4-3。但如果你看的是中文版,注意该表右半部助记符有个错误,应该是 (当a=1)。
说到勘误,4.1.1 的寄存器段还有一个错误,寄存器的宽度与内存相等,都是 16 位。因此那里应该是 “只存储 1 个值”,而不是书中的 “只存储 1 位”.以及本章错误真鸡儿多,感觉跟前三章不是同一个人翻的。
汇编
本章和附录都没有一个很完善的汇编参考或教程,因此在做题的时候虽然思路是有的但不知道应该怎么写,或者不应该怎么写。于是把章内的 sum(1..100)
程序抄了一遍,并且用编译器和CPU模拟器尝试跑了一下。
原始的 asm 是这样的:(OSC 的 markdown 渲染有点傻逼)
[@i](https://my.oschina.net/izhuchao) // i refers to some mem. location M=1 [@sum](https://my.oschina.net/Asum) M=0(LOOP) [@i](https://my.oschina.net/izhuchao) D=M [@100](https://my.oschina.net/laoka) D=D-A [@END](https://my.oschina.net/u/567204) D;JGT // if (i-100)>0 goto END @i D=M @sum M=D+M @i M=M+1 @LOOP 0;JMP // goto LOOP(END) @END 0;JMP // infinit loop
我疑惑的点在于,这些 i
啊 sum
啊的变量到底是怎么分配内存的?还有 (LOOP)
这种标记算什么?为什么有缩进,他们有具体的含义吗?当把它编译之后,得到的文件是这样的:
0 @161 M=12 @173 M=04 @165 D=M6 @1007 D=D-A8 @189 D;JGT10 @1611 D=M12 @1713 M=D+M14 @1615 M=M+116 @417 0;JMP18 @1819 0;JMP
程序自动为 i 分配了 16(10000),而 sum 分配了 17(10001)。之后不论我怎么修改程序,发现总是按顺序从 16 开始赋值,即使你的程序写成有冲突的样子,比如:
@aM=1@16M=2
它依然会编译成:
0 @161 M=02 @163 M=1
这个故事告诉我们,不要随便硬编码内存地址,尽量都通过符号来分配。
后记:眼瞎如我,其实有一段(4.2.4)讲解符号的。
套路
把题做完后总结了一些套路:
- 所有变量存在内存里,使用符号指定
- 只有一个A寄存器,它不能既用来存地址,又用来存值。这意味着:当使用跳转命令时,A 的值只能是跳转的目标地址,因此表达式只能是 D 或常数。即当使用 C 指令时,总是要先把值存入 D 或使用常数。
- 流程控制全靠跳转,跳转全靠比较。看到有流控的需求的时候,第一反应把程序分几段。
- 因为 hack 机器只有一个 D 和 一个 A 寄存器,写汇编就像玩汉诺塔,总想着怎么倒数据
- 将 M 的值赋给 A 要使用 C 指令而不是 A 指令,即
A=M
, 而不是@A