学完前一篇的机器语言规范,仍然是懵圈的,对于作业无从下手,还好视频中有一些示例,摘抄到这里,并附上了详细注释:
操作寄存器和内存
从前一篇可以知道,HACK机器语言中有三个寄存器:
- D:data register,数据寄存器
- A:address register/data register,地址与数据寄存器
- M:M=RAM[A]
A指令作用:
@17之后A=17,M=RAM[17]
HACK的所有功能都通过对这三个寄存器的操作来实现。
基本用法:
// D=10
@10 // A指令引入常量
D=A // 将常量传递给D寄存器
// D++
D=D+1 // 可以直接对D+1
// D=RAM[17]
@17 // A指令保存17到A寄存器,此时M=RAM[A]就代表RAM[17]
D=M // 将RAM[17]赋给D
// RAM[17]=10
@10 // A指令取常量10
D=A // 将常量10传递给D
@17 // A指令取常量17,此时M=RAM[A]就代表RAM[17]
M=D // 将常量10从D中传递到RAM[17]
// RAM[5]=RAM[3]
@3 // A指令取常量3,此时M=RAM[A]就代表RAM[3]
D=M // D=RAM[3]
@5 // M=RAM[5]
M=D // RAM[5]=RAM[3]
小试牛刀,计算RAM[2]=RAM[0]+RAM[1]:
// Add2.asm
@0
D=M // D=RAM[0]
@1
D=D+M // D=D+RAM[1]
@2
M=D //RAM[2] = D
分支
完成基本的if-else结构:
if R0>0
R1=1
else
R1=0
汇编代码:
0: @R0 //rR0表示0,因此表示@0,下面的@R1同理
1: D=M // D=RAM[0]
2: @8
3: 0;JGT // IF R0>0 goto line 8
4: @R1
5: M=0 // RAM[1] = 0
6: @10
7: 0;JMP // 程序结束
8: @R1
9: M=1 // R1=1
10: @10
11: 0;JMP
上面代码特意标上了行号,是为了引出标签:
@R0
D=M // D=RAM[0]
@POSITIVE
0;JGT // IF R0>0 goto line 8
@R1
M=0
@END
0;JMP
(POSITIVE)
@R1
M=1
(END)
@END
0;JMP
行号在开发过程中是时时改变的,如果要记行号,那就是灾难,使用标签替代行号,就友好多了。
变量
完成两个内存单元,RAM[0]和RAM[1]数值的交换,需要声明一个中间变量temp:
// temp=R0
// R0=R1
// R1=temp
@R0
D=M //将RAM[0]保存到D
@temp // 没有声明的标签视为对内存地址的引用,因为R0-R15已经有特殊声明,所以地址从16开始。
M=D // 将RAM[0]保存到temp内存
@R0
D=M // D=RAM[R0], R0保存到D
@R1
M=D // R1=R0
@temp
D=M // D=temp
@R0
M=D // R0=temp
(END)
@END
0;JMP
如果有多个变量,内存地址会从16开始一直递增:16,17,18…
循环
计算:1+2+…+n
如果直接编写汇编代码,会比较痛苦。
按课程中提倡的方法,先列伪代码,伪代码逻辑没问题,再实现为汇编代码。
伪代码:
// 计算 RAM[1] = 1+2+...+RAM[0]
n=R0
I=1
sum=0
LOOP:
if i>n goto STOP
sum=sum+i
i=i+1
goto LOOP
STOP:
R1=sum
伪代码逻辑ok后,将伪代码用汇编实现即可,这时伪代码可作为代码注释:
@R0
D=M //D=RAM[R0]
@n //@16,M=RAM[16],取RAM[16]
M=D //n=R0
@i //@17
M=1 //i=1
@sum //@18
M=0 //sum=0
(LOOP)
@i //@17,M=RAM[17],取RAM[17]
D=M //D=i
@n //@16,M=RAM[16],取RAM[16]
D=D-M //D=D-RAM[16],比较i和n的大小
@STOP
D;JGT // if i>n goto STOP,循环出口
@sum //@18,M=RAM[18],取RAM[18]
D=M //D=sum
@i //@17,M=RAM[17],取RAM[17]
D=D+M //D=sum+i
@sum
M=D // sum=sum+i
@i
M=M+1 //i=i+1
@LOOP
0;JMP // 无条件跳转到LOOP位置
(STOP)
@sum // 循环退出后跳到这里,保存sum到RAM[1]
D=M // sum地址的值保存到D
@R1
M=D //RAM[1]=sum
(END)
@END
0;JMP
最后可以推演一下在循环中变量的变化过程:
指针
在汇编语言中,只有内存片段和寄存器,没有数组这样的概念,因此以下的操作需要借助额外的技术:
for (i=0;i<n;i++){
arr[i]=-1
}
汇编语言中只有内存地址,在汇编中对数组支持是通过数组名称arr和下标i共同实现的
将arr[i]看成两部分:
- arr,表示一个内存基地址,是一个内存单元
- i表示相对基地址的偏移量,arr[i]表示离基地址arr距离为i的内存单元
伪代码:
arr=100 //假设基地址为100
n=10 //假设循环10次
i=0 //偏移从0开始
LOOP
if (i==n) goto END
RAM[arr+i]=-1
i++
END
因此可以实现如下:
//arr=100
@100
D=A
@arr
M=D // 假设基地址为100
//n=10
@10
D=A
@n
M=D
//i=0
@i
M=0
(LOOP)
//if (i==n) goto END
@i
D=M
@n
D=D-M
@END
0;JEQ
//RAM[arr+i]=-1
@arr
D=M
@i
A=D+M
M=-1
//i++
@i
M=M+1
@LOOP
0;JMP
(END)
@END
0;JMP
经过三次循环后内存状态:
输入输出
输入是键盘KBD
输出是屏幕SCREEN
关于HACK中屏幕和键盘的操作可参考输入输出处理
视频中只提到了屏幕操作的例子,例子任务是在屏幕左上角画一个宽为16的矩形,也即每行只操作第一个寄存器。
伪代码:
addr=SCREEN
n=RAM[0]
i=0
LOOP:
if i>n goto END // 画n行后结束
RAM[addr]=-1 //1111111111111111,表示将RAM[addr]内存对应的屏幕上16个像素改成黑色
// 到屏幕下一行
addr = addr+32 // 每行有32个寄存器,+32正好落到下一行的第一个寄存器上
i=i+1
goto LOOP
END
goto END
汇编:
@SCREEN
D=A
@addr
M=D //addr=16384,屏幕基地址
@0
D=M
@n
M=D //n=RAM[0]
@i
M=0 //i=0
(LOOP)
@i
D=M
@n
D=D-M
@END
D;JGT //if i>n goto END
@addr
A=M
M=-1 //RAM[addr]=1111111111111111
@i
M=M+1 //i=i+1
@32
D=A
@addr
M=D+M //addr=addr+32
@LOOP
0;JMP //goto LOOP
(END)
@END
0;JMP