第四章,02-HACK机器语言规范
概述(Overview)
Hack是书中用于教学的一个基于冯·诺伊曼架构的16位计算机,组成:
CPU
两个独立的内存模块(instruction memory,指令内存和data memory,数据内存)
两个内存映射I/O设备:显示器、键盘
内存地址空间(Memory Address Space)
- 指令地址空间
- 数据地址空间
两个空间都是16位宽,15位地址空间,即可寻址最大空间为32K的16bit word。
寄存器(Registers)
Hack含有两个16位寄存器:D和A,这些寄存器能被算术和逻辑指令显式操纵。
- D寄存器
存储数据值 - A寄存器
存储:数值,或数据存储器中的地址,或指令存储器中的地址。
由于Hack为16位计算机,而地址需要占到15位,因此操作码和地址必须分开两条指令描述,就需要借助A寄存器。
- A寄存器的作用:
- 存储数据寄存器地址
借助一个隐式的内存地址”M“,M总是代表Memory[A]。
@516 // A将A寄存器的值置为516,此时M=Memory[516]
D=M-1 // 由于M=Memory[516],则这条指令将Memory[516]-1的值保存到D寄存器
- 存储指令存储器地址
在jmp或goto指令前将A寄存器置为目的地址,跳转指令就会跳转到该地址。
这样jmp或goto指令中就不需要再指定地址。
- A寄存器内容的修改:
使用”@“命令即下面要介绍的A-指令:@value,其中value可以是数值或是代表数值的符号。
如,或sum代表内存地址17,那么@17和@sum都将A置为17:A ← \leftarrow ← 17。
@sum // A指令指定目的地为LOOP,LOOP为程序中使用的标记
0;JMP // 跳到该目的地
涉及内存地址的操作需要两条Hack命令:
1.确定内存单元地址 – 地址指令,也称为A-指令;
2.描述要进行的操作 – 计算指令,也称为C-指令;
两种指令都有二进制表示和符号表示。
A-指令(The A-Instruction)
A-指令用来修改A寄存器的值
上述”@“命令就是A指令的符号表示
@17的二进制表示:0000000000010001
@5的二进制表示:0000000000000101
分别将17和5储存到A寄存器中
A-指令的三种用途:
- 在程序控制下,唯一一种”将常数输入计算机“的方法;
- 提供C-指令操作所需目标数据内存单元;
- 为执行跳转的C-指令提供跳转的目的地址。
这三个用途的示例可见下面jump指令介绍
用途参考:
C语言:
// 1 + ... + 100的累加
int i = 1;
int sum = 0;
while (i <= 100){
sum += i;
i+=;
}
Hack机器语言:
// 1 + ... + 100的累加
@i // A指令指定i内存单元
M=1 // i=1
@sum // A指令指定sum内存单元
M=0 // sum=0
(LOOP)
@i // A指令指定i内存单元
D=M // D=i
@100 // A指令指定常数100
D=D-A // D=i-100
@END // A指令指定END标记
D;JGT // If (i-100)>0 goto END
@i // A指令指定i内存单元
D=M // D=i
@sum // A指令指定sum内存单元
M=D+M //sum=sum+i
@i // A指令指定i内存单元
M=M+1 //i=i+1
@LOOP // A指令指定LOOP标记
0;JMP //goto LOOP
(END)
0;JMP //无限循环
C-指令(The C-Instruction)
有了A-指令指定地址,C-指令的功能聚焦到计算和存储,和逻辑跳转上:
指令编码的最高位为1,表示该指令是C-指令。接着两位为闲置位,没有使用
接下来分成三个功能域:dest=comp;jump,分别解释如下:
Computation规范(计算规范)
Hack的ALU执行一组固定的函数集,实现对寄存器D、A、M(表示Memory[A])的操作。
comp域由一个a位域(a-bits)和6个c位域(c-bits,对应ALU的六个控制位)组成。这个7位模式可以编码128个不同的函数,但只有28个函数在机器语言规范中列出。
注:图上右上角应该是(当a=1),为书中印刷错误
计算示例:
111 0 0011 10 00 0000,D-1,D寄存器当前值减1
111 1 0101 01 00 0000,D|M
111 0 1110 10 00 0000,A-1
Destination规范(目的地规范)
指定计算结果输出的地址:
d1,d2,d3分别表示A,D,M三个位置,对应位置为1表示存入,为0表示不存入。
示例:
0000 0000 0000 0111 // @7,选中地址为7的寄存器(也就是M)
1111 1101 1101 1000 // MD=M+1,计算MM+1的值,并存入M和D中
Jump规范(跳转规范)
计算机可能继续执行指令内存中的下一条指令,也可能获取并执行位于其他地址的一条指令,第2种情况需要跳转。
假定A寄存器已经装载了跳转的目的地址。
Jump操作的执行取决于jump域的三个j-位域和ALU的输出值out(根据comp域计算得出):
jump示例:
逻辑
if Memory[3]==5 then goto 100
else goto 200
实现
@3 // A指令指定数据内存地址
D=M //D=Memory[3]
@5 // A指令指定常数
D=D-A //D=D-5
@100 // A指令指定指令内存地址,即跳转地址
D;JEQ //If D=0 goto 100
@200
0;JMP // Goto 200
注1: 这个例子是A指令三个用途的极佳展示
注2: 最后一个0;JMP执行一个无条件的跳转,实现无限循环,防止计算机继续往下读取,进入包含未知代码的区域。若进入未知区域,有可能被恶意攻击利用,执行被恶意插入到该区域的代码。
使用A寄存器的冲突
由于A寄存器可能包含数据内存的地址,也可能包含指令内存的地址,因此在可能引发jump的C-指令中不能引用M,在引用M的C-指令中不能引发jump,防止发生冲突。
符号
汇编命令可以使用常数或符号来表示内存单元位置(地址)。通过以下三种方式应用到汇编语言中:
预定义符号(Predefined symbols)
RAM地址的一个特殊子集,可以通过如下预定义符号来被所有汇编程序引用:
- 虚拟寄存器(Virtual registers)
R0-R15表示0到15号RAM地址。 - 预定义指针(Predefined pointers)
符号SP、LCL、ARG、THIS和THAT表示0到4号RAM地址。这四个内存位置都有两种符号表示。这种表示方法在7、8章中讨论的虚拟机实现中使用到。 - I/O指针
预定义符号SCREEN和KBD分别表示RAM地址16384(0x4000)和24576(0x6000),分别是屏幕和键盘内存映像的基地址。
标签符号(Label symbols)
自定义符号用来标记goto命令跳转的目的地址,由伪指令"(Xxx)"来声明。其意义是:Xxx代表程序中下一条命令的指令内存位置。一个标签只能被定义一次,可以在汇编程序中的任何地方使用,即使是在其定义之前也可以使用。
变量符号(Variable symbols)
任何自定义符号Xxx,如果不是预定义符号,也不是标签符号,那么就看作是变量,并被赋予独立的内存地址(R15以上,即从0x0010开始)。
输入/输出处理(Input/Output Handling)
计算机使用一组输入输出(I/O)设备与外部环境交互,这些设备多种多样,每个设备都是一个独立的机器,需要相关的工程知识,不可能对每个设备都了解它本身的构造。
处理方式是设计不同的方案将这些不同外设的物理细节进行封装,让计算机能以相同的方式对它们进行操作,其中最简单的实现技巧就是I/O映像(memory-mapped I/O)。
I/O映像
基本思想是创建I/O设备的二进制仿真,让CPU像处理内存段一样和外设交流。每个I/O设备都在内存中分配了独立的区域,作为它的“内存映像”。
- 输入设备,内存映像连续不断地反映设备的物理状态;
- 输出设备,内存映像连续地驱动设备的物理状态。
- 硬件角度,所有I/O设备都提供类似于记忆单元(memory unit,或称内存单元)的接口
- 软件角度,对每个I/O设备定义交互协议,使程序能够正确地访问。所需要做的只是为硬件分配一个新的内存映像并记录基地址(操作系统完成)
- 大量计算机平台和I/O设备同时存在时,就需要定义各类标准规范(standards),使其能够正确访问而不致冲突
Hack的外设硬件
Hack能够连接两个外部设备:屏幕和键盘。
对它们的操作都是通过内存映像(memory maps)实现的。
即屏幕上描绘像素是通过将二进制值写入与屏幕相关的内存段来实现的。
键盘的输入是通过读取与键盘相关的内存单元来实现的。
物理I/O设备和它们对应的内存映像通过连续的循环刷新来同步。
屏幕
Hack计算机包括一个256×512像素的黑白屏幕。
屏幕的内容由RAM基地址为16384(0x4000)的8K内存映射来表示。
物理屏幕的每一行从屏幕的左上角开始,在RAM中用32个连续的16位字表示。
因此至顶部r行、至左边c列的像素映射到位置为RAM[16394+r·32+c/16]的字的c%16位(从LSB到MSB)。
为了读写屏幕上的一个像素,可以对RAM内存映射的相关位进行读写操作(1=黑,0=白)。如:
// 在屏幕的左上角画一个黑点
@SCREEN // 将A寄存器的值置为内存映射区的映射到屏幕第一行的16个最左边的像素的内存字。
M = 1 // 将最左边的像素变黑。
键盘
内存映像在RAM的基地址为24576(0x6000)。
键盘上敲击一个键,其对应的16位ASCII码值就发送到RAM[24576]:
没有击键时,该内存单元的值就为0。
除了常用的ASCII码外,Hack键盘还可以识别以下的一些键:
按键 | 编码 | 按键 | 编码 |
---|---|---|---|
newline | 128 | end | 135 |
backspace | 129 | page up | 136 |
left arrow | 130 | page down | 137 |
up arrow | 131 | insert | 138 |
right arrow | 132 | delete | 139 |
down arrow | 133 | esc | 140 |
home | 134 | f1-f12 | 141-152 |
语法规约和文件格式(Syntax Conventions and File Format)
二进制文件
每一行都是16个0 1 ASCII字符,对应一条单独的机器语言指令。
文本中的所有行组成了机器语言程序。
当机器语言程序被加载到计算机指令内存中,约定文件中第n行所表示的二进制码加载到指令内存地址为n的单元中。
约定机器语言程序文件扩展名为“hack”,如Prog.hack。
汇编语言文件
习惯上,汇编语言程序存储在以“asm”为扩展名的文本文件中,如Prog.asm。
汇编语言文件由文本行组成,每一行代表一条指令或者一个符号定义:
- 指令(Instruction):一条A-指令或C-指令
- 符号定义(Symbol):这条伪指令让编译器中把Symbol标签分配给程序中下一条命令被存储的内存单元地址。
- 不会生成任何机器代码,所以称为“伪命令(pseudo-command)”。
常数(constants)和符号(symbols)
常数必须是非负的十进制整数。
自定义符号可以是任何字母、数字、下划线(_)、点(.)、美元符号($)、冒号(:)构成的字符串,但不能以数字开头。
注释
双斜线(//)开头
空格
空格与空行也被程序忽略
大小写
所有汇编助记符必须大写。
自定义标签(label)和变量名区分大小写,一般标签大写,变量名小写。
吐槽
ALU只有8个输入,其中两个是16位的计算数据,6个对应C指令的comp域,而A指令和C指令都有16位,剩下10位连接到哪里了?这个细节书中没有提到,看后面会不会再提到吧。。。