背景
在前面采用了结构化的方式来描述计算机硬件的构造过程,接下来本章采用软件方式,即机器语言的方式来抽象的描述计算机的功能。
机器语言是一种约定的形式,用于对底层程序进行编码,从而形成一系列机器指令,使处理器进行算术和逻辑操作,在内存中进行存取操作等等。如果说高级语言的设计目标是通用,那么机器语言的设计目标就是能够在指定的硬件平台上运行,并且对该硬件平台全面的操控。
机器
机器语言是种约定的形式,利用处理器和寄存器来操作内存。
内存
前面所构建的能够存储数据的芯片RAM就属于内存。内存单元是一个连续固定宽度的单元序列,具有唯一的地址,可以通过提供的地址来确定位置。
处理器
CPU,是执行一组固定基本操作的设备,通常包括算术运算和逻辑运算,内存存取和控制操作。 这些操作的对象是二进制数,来自于寄存器或者指定的内存单元。操作结果也会被存储在寄存器或者指定的内存单元。
寄存器
在前面构建的RAM可知,内存访问需要一长串地址,比较缓慢,基于此原因,处理器都有集成一些寄存器,用于快速访问,相当于处理器的一个本地高速内存,使得处理器能够快速地操纵数据和指令。
语言
机器语言程序是一系列的编码指令,本质上是二进制数。
为了便于识别和理解,在机器语言中使用了二进制码和助记符的方式便于理解。
举例来说,可以定义操作码 1010 用 Add 来表示,寄存器可以使用符号R0、R1、R3来表示,那么 1010 0011 0001 1001使用助记符来表示的话,就可以写作
Add R3,R1,R9
进一步说的话,我们可以直接使用这种助记符的方式来编写程序,只要有一个专门的程序来进行助记符到机器语言的翻译即可。那么助记符便是汇编语言,翻译器就是汇编编译器。
命令
算术操作和逻辑操作
计算机执行的基本算数操作,包括加法,减法,基本的布尔运算等。
汇编 | 含义 |
---|---|
ADD R2,R1,R3 | R2 <——R1+R3 把寄存器R1和R3里的值相加,结果存放到R2 |
ADD R2,R1,foo | R2<——R1+foo 把寄存器R1和自定义foo指向内存单元的值相加,结果存放到R2 |
AND R1,R1,R2 | R1<——R1&R2 把寄存器R1和R2的值按位与,结果存放到R1 |
内存访问
-
直接寻址
最常用的寻址方式,直接表示一个指定内存单元地址,或者使用一个符号表示
汇编 含义 LOAD R1,67 R1<——Memory[67] 内存67内存放的数存到R1中 LOAD R1,bar R1<——bar bar指向的内存单元存放的数存到R1中 -
立即寻址
通常被用于加载常数,直接把指令数据域中的内容当做要操作的数装入寄存器中
汇编 含义 LOADI R1,67 R1<——67 把67装入寄存器R1中 -
间接寻址
指定了地址存放的位置,根据存放的位置再去寻找地址,再通过地址来寻找操作数。
汇编 含义 LOAD* R2,R1 R2<——Memory[R1] 取出R1内的数作为地址,再根据地址寻找到操作数,存到R2内
控制流程
程序通常是线性方式执行,但也会有分支执行。
包括:循环,条件执行,子程序调用,无条件跳转
Hack机器语言
Hack是最终要构造的冯诺依曼结构的16位计算机,包括部件:1个CPU,2个互相独立的内存模块(指令内存和数据内存),2个内存映射I/O设备(显示器和键盘)。
内存地址空间
分为两个不同的地址空间(指令地址空间,数据地址空间),都是16位宽,32K大小,这意味着地址长度是15位。
寄存器
Hack的CPU集成了两个寄存器便于快速存取,它们均是16-位寄存器。
-
D寄存器:仅用于存取数据值。
-
A寄存器:可以做数据寄存器,也可以做地址寄存器。
- 可以发现地址长度只有15位,而指令长度最长为16位,所以在一条指令内把操作码和地址同时写明是不可能的,因此我们规定:
当A寄存器作为地址寄存器时,地址是被隐式表明的。举例来说,如果要执行D=Memory[516]-1,就必须先必须取出 寄存器[516] 内的值,因此需要有一条指令使A寄存器的值设为516,再通过A寄存器访问到寄存器[516],再执行D=M-1。
- A寄存器用来访问指令存储器。同样的,如果你想要使用jump指令跳转到某个地址,就需要把这个地址存入到A寄存器内,然后再紧接着跳转到该地址,最后再把该地址存放的指令取出,存放到A寄存器内。
指令
A-指令
地址指令,用来为A寄存器设置15-位的值。
二进制表示:0vvv vvvv vvvv vvvv
汇编表示:@value
value应当是一个非负的十进制数,或者代表非负十进制数的符号,这条指令代表将value值存入到A寄存器内。
有三种用途:
- 在程序控制下,提供了唯一一种输入常数到计算机的方法。
- 通过把目标数据的地址存放到A寄存器内,提供对该内存单元操作的C指令的必要条件。
- 通过把要跳转的目标之地存放到A寄存器内,为执行跳转的C指令提供条件。
C-指令
计算指令。
二进制表示:111a c1c2c3c4 c5c6d1d2 d3j1j2j3
汇编表示:dest = comp;jump //dest或者jump可以为空,为空时,对应的符号(= ;)可以省略
dest指明计算后的结果存放到什么位置,comp指明ALU计算什么,jump描述转移条件,即下一条应该执行哪一条命令。
dest 规范
d1 d2 d3 | 助记符 | 目的地 |
---|---|---|
0 0 0 | Null | 空,该值不会被存储 |
0 0 1 | M | Memory[A],地址由寄存器A给出 |
0 1 0 | D | D寄存器 |
0 1 1 | MD | Memory[A]和D寄存器 |
1 0 0 | A | A寄存器 |
1 0 1 | AM | A寄存器和Memory[A] |
1 1 0 | AD | A寄存器和D寄存器 |
1 1 1 | AMD | A寄存器,D寄存器和Memory[A] |
comp规范
a=0时 助记符 | c1 c2 c3 c4 c5 c6 | a=1时 助记符 |
---|---|---|
0 | 1 0 1 0 1 0 | |
1 | 1 1 1 1 1 1 | |
-1 | 1 1 1 0 1 0 | |
D | 0 0 1 1 0 0 | |
A | 1 1 0 0 0 0 | M |
!D | 0 0 1 1 0 1 | |
!A | 1 1 0 0 0 1 | !M |
-D | 0 0 1 1 1 1 | |
-A | 1 1 0 0 1 1 | -M |
D+1 | 0 1 1 1 1 1 | |
A+1 | 1 1 0 1 1 1 | M+! |
D-1 | 0 0 1 1 1 0 | |
A-1 | 1 1 0 0 1 0 | M-1 |
D+A | 0 0 0 0 1 0 | D+M |
D-A | 0 1 0 0 1 1 | D-M |
A-D | 0 0 0 1 1 1 | M-D |
D&A | 0 0 0 0 0 0 | D&M |
D|A | 0 1 0 1 0 1 | D|M |
jump规范
j1 j2 j3 (out < 0) (out = 0)(out > 0) | 助记符 | 作用 |
---|---|---|
0 0 0 | null | 不跳转 |
0 0 1 | JGT | out>0时跳转 |
0 1 0 | JEQ | out=0时跳转 |
0 1 1 | JGE | out>=0时跳转 |
1 0 0 | JLT | out<0时跳转 |
1 0 1 | JNE | out!=0时跳转 |
1 1 0 | JLE | out<=0时跳转 |
1 1 1 | JMP | 无条件跳转 |
由于我们可以使用A寄存器为“包含M的C-指令”指定数据内存中的地址,也可以为“包含jump的C-指令”指定指令地址中的内存,两者会引发冲突,所以在可能引发跳转的指令中不能引用M,在引用M的指令中也不准引发跳转。
符号
汇编命令可以使用常数或符号来表示内存单元地址。
- 预定义符号
- 虚拟寄存器:R0到R15分别代表0到15号RAM地址。
- 预定义指针:SP、LCL、ARG、THIS、THAT分别预定义为0到4号RAM地址。
- I/O指针:SCREEN和KBD预定义为屏幕和键盘内存映像的基地址(16384/0x4000和24576/0x6000)。
- 标签符号:用户自定义符号,用于标记goto命令跳转的目标地址,由“(Xxx)”来声明,一个标签只能定义一次,在定义前也可以使用。
- 变量符号:用户自定义符号,变量,有独立的内存地址(从RAM地址16往后开始)。
输入/输出处理
屏幕
Hack计算机的屏幕是256X512像素,通过RAM基地址为16384(0x4000)的8K内存映射表示,每一行从左到右,在RAM中用32个连续的16-位字表示。
对于第r行第c个像素,它的位置被映射到**RAM[16384+r*32+c/16]**的字的c%16位。其中,1表示黑,0表示白。
键盘
Hack计算机中的键盘是单字内存映射到RAM基地址为24576(0x6000)。只要在键盘上输入一个键,其对应的ASCII码就会出现在RAM[24576],没有输入时,该内存单元值便是0。除了ASCII码外,还有以下键可以被识别。
语言规约和文件格式
二进制文件
拓展名为“hack”,文件中的每一行都是16个连续的1和0,在机器语言程序被加载到计算机指令内存中时,约定文件中的第n行的二进制码会被存储到指令内地址为n的单元(从0开始计数)。
汇编语言文件
拓展名为“asm”,文件中每一行是一条指令或者一个符号定义。
- 指令:一条A-指令或者C-指令。
- 符号:让编译器把该定义的符号标签分配给程序中下一条命令被存储的内存单元。
- 语法规范:
- 常数:必须非负且使用十进制表示。
- 符号:不能以数字开头,仅可以包括字母、数字、_、.、$、: 符号。
- 注释:以双斜线// 开头。
- 空格:空格和空行会被忽略。
- 大小写:所有汇编助记符都必须大写。其他的自定义标签和变量一般要求标签大写,变量名小写。
项目构建
Mult.asm(乘法程序)
输入值存储在R0和R1内,要求计算R0*R1,并存放到R2内。
直接构造一个乘法程序比较困难,而乘法可以看成加法的累加,我们可以先构建一个加法程序,然后在加法程序的基础上改造后构造乘法程序。
要构造的加法程序:输入值存储在R0和R1内,要求计算R0+R1,并存放到R2内。
思考流程:
我们构建的是个加法程序,核心就是加法这一步,所以先把加法需要的C-指令这一步构造好。
在C-指令中,并没有支持直接两个数据寄存器相加的功能,所以需要先把其中的一个数取出到A寄存器或者D寄存器内,再进行加法操作。
但这样会出现一个问题,我们在将R0数据存放到D寄存器后,再通过A寄存器获得R1的地址,然后D+M获得结果,这个结果应该存放的位置R2,没有办法获取到(因为A寄存器此刻存放的是R1的地址)。
所以我们的加法应该分为三步:
- 把R2初始化置零。
- R2=R2+R0。
- R2=R2+R1。
//初始化,把R2的数据清空
(START)
@R2
M = 0
//R2 = R2 + R0 ,也可以直接R2 = R0
@R0 //R0的地址存到A
D = M //把M[0]的数据存到D寄存器中
@R2 //R2的地址存到A
M = D + M // R2 = R0 + R2
//R2 = R2 + R1
@R1
D = M
@R2
M = D + M
@START
0;JMP
现在写出来了加法程序,再接着构造乘法程序。
R0*R1可以看做R1个R0相加,那么我们需要一个LOOP来完成这个累加过程,边界条件便是R1<0是进行跳转,循环内容是R2 = R2 + R0,因为R1=0时是最后一次累加。
要注意的是,如果一开始R1就为0,便可以直接返回结果到R2。
//初始化,R2置空
(START)
@R2
M = 0
//条件判断 R1是否为0
D = 0 //常数0存到寄存器D中
@R1
A = M
@END
D|A;JEQ //由于在JMP命令内不能引用M,所以上一步需要把M的值赋给A,再进行比较
(LOOP)
//循环体,R2=R2+R0,然后R1-1,在M>0时,跳转到上面LOOP位置
@R0
D = M
@R2
M = D + M
@R1
M = M - 1
D = M //把R1-1后的值取出赋给D,然后根据D进行跳转
@LOOP
D;JGT
(END)
@START
0;JMP
这样做确实实现了乘法,但是也破坏了数据的存储,R1内的数据递减,最后归0,如果要保护R1内的数据,需要一个新寄存器R3用于存储临时数据,这里不再构造。
Fill(I/O处理程序)
这是一个无限循环程序,检测键盘的输入,当任一键被按下时,键盘将变黑,没有间按下时,屏幕被清屏。键盘变黑的顺序和清屏的顺序不做要求。
首先需要构造的是LOOP,在这个LOOP内,检测键盘映射空间的输入,当RAM[24576]不为0时,便会进行黑屏操作,为0时,便会进行清屏操作。
由于顺序不做要求,我们自然从屏幕左上角RAM[16384]开始变黑和清屏操作,终点是RAM[24575]。
如果想要一个像素一个像素的变黑,那么就是做 或位操作,显然太慢而且复杂。为了更方便,直接按字(16位)来进行黑屏操作,这样只要直接赋值即可。
下面给出一种方法。
(START)
//需要两个额外的寄存器R0和R1,R0存储当前黑屏位置,R1存储屏幕的最后地址
@R0
M = 0 //循环前置零
@8192
D = A
@R1
M = D //从0开始计数,所以说屏幕的最后地址应该为8191,但为了方便判断,设为8192
(LOOP)
//检测键盘是否有输入
@KBD
D = M
@CLEAR
D;JEQ
(BLACK) //黑屏操作
@R0
D = M //获得当前黑屏位置和屏幕初始位置的偏差值
@R1
A = M //获得8192,并存放到A内
D = A - D
@LOOP
D;JLE //判断是否为屏幕的最后地址,如果是的话,直接跳回循环开始
@R0
D = M
M = M + 1 // 偏差值+1
@SCREEN
A = D + A //获取屏幕初始地址,再加上偏差值
M = -1 //黑屏,即全位置1
@LOOP
0;JMP
(CLEAR) //清屏操作,和黑屏操作的逻辑相同,不过顺序是反过来的
@R0
D = M //获取当前黑屏位置和屏幕初始位置的偏差值
@SCREEN
A = D + A
M = 0
@LOOP
D;JEQ //判断是否为屏幕的开始地址,如果是的话,直接跳回循环开始
@R0
M = D - 1 //偏差值-1
@LOOP
0;JMP
总结
- 这一节进行了汇编程序的简单构造,属于软件方面,下一节将再次构造硬件——将以往构造的芯片组合起来,集成为一台计算机。
- 相比于硬件的构造,汇编程序的编写还是比较符合常规思维逻辑的,就是前置需要的知识点比较多。
- 我已经仔细检查了我的程序,大概率是没用问题的,当然也可能有些BUG,还望指正。