本 Wiki 开启了 HTTPS。但由于同 IP 的 Blog 也开启了 HTTPS,因此本站必须要支持 SNI 的浏览器才能浏览。为了兼容一部分浏览器,本站保留了 HTTP 作为兼容。如果您的浏览器支持 SNI,请尽量通过 HTTPS 访问本站,谢谢!
这是本文档旧的修订版!
Week 4 notes
为了更方便的讨论机器语言,人们通常将指定的代码段与相应的含义(Mnemonic Form)对应起来。下面的例子中,ADD
就是 0100010
对应的 Mnemonic Form。可以观察到的是,这种替代不仅可以代表 operation,也可以代表地址。
需要注意的是:
ADD
这样的形式不是真正存在的
Symbols 主要应用于简化内存寻址的过程。比如维护一个 index
,将指定的内存区域分为不同的片区,当需要对指定区域进行操作时,我们可以通过对应的 index
对其进行快速访问。这种将 symbol 与实际内存地址建立映射关系的方式,会大大增强程序的易读性和可维护性。该映射关系也是由 assmbler 来翻译的。
不同的机器语言之间的实现可能并不相同,取决于其复杂度。
内存寻址是一项非常昂贵的操作。通常来说,有两个问题:
解决方案是提供内存的级联:将内存按速度进行划分,不同速度的内存区域对应不同的任务。需要注意的是,速度越快,对应的内存就越小。
寄存器是访问速度最快的内存,通常由 CPU 内置。寄存器分为几个类型:
add r1 r2
,r1
和 r2
就是该加法操作中,对应数据所在的寄存器名称。
# Register mode R2 <- R2 + R1
Add R1, R2
# Direct mode Mem[200] <- Mem[200] + R1
Add R1, M[200]
# Indirect Mem[A] <- Mem[A] + R1
Add R1, @A
# Immediate R1 <- R1 + 73
Add 73, R1
外设(键盘鼠标)通常通过寄存器链接,并通过一定的协议(驱动)来使其工作。
RAM[0]
ROM[0]
整个 program 是一系列的 Hack 16bit 指令组成的序列。
reset
唤起该序列,之后整个程序开始运行A
对应的寄存器
A指令(寻址类)的作用:存储一个值到某个 A 类寄存器中。该值将作为 RAM 的 index,自动关联到 M 寄存器。比如 @21
,之后对 M 类寄存器进行访问,实际上就是访问的 RAM[21]
。
# 对 A 类寄存器赋值
# 语法:@value
# value 值:non-negative decimal constant / symbol referring to such a constant
# side effects: RAM[21] becomes the selected RAM register(M 寄存器)
@21
# usage
@100 # A=100
M=-1 # RAM[100] = -1
C指令(计算类)由下面的结构组成:
# dest & jump are optional
dest:comp ; jump
其中:
整个流程:
==
0,那么执行 jump。跳转的位置与之前指定的 A 值相关该部分指定计算结果应该存储到哪个寄存器。比如:
# set the D resgister to -1
D=-1
# set the RAM[300] resgister to D - 1
# when use A register, it must be assigned a value first
@300 # A = 300
M = D - 1 # RAM[300] = D - 1
jump 是 C 指令的一个可选操作。通常来说,当 jump 是 C 指令的一部分时,整个 C 指令实际上就是为了 jump 而准备的。比如下面的例子:
# if (D-1 == 0), jump to execute the instruction sotred in ROM[56]
@56
D-1;JEQ
也就是说,这个 A = 56
是一个目的性很强的操作,它的主要功能就是告诉计算机当条件成立时,我们应该执行 ROM[56]
对应的指令。换句话说,jump 的意义就是我们希望在某种条件成立的情况下,执行特定的指令(如果没有 jump,ROM 里的指令会按次序进行)。
Type | explanation |
---|---|
null | 不跳转(默认情况) |
JGT | 如果 comp > 0,跳转 |
JEQ | 如果 comp == 0,跳转 |
JGE | 如果 comp >= 0,跳转 |
JLT | 如果 comp < 0,跳转 |
JNE | 如果 comp ≠ 0,跳转 |
JLE | 如果 comp ⇐ 0,跳转 |
JMP | 无条件跳转(always jump |
两种写指令的方式:
两种指令的对比如下:
# set the A register to value
# symbolic @ + value
# Binary 0 + value(in binary)
@21
0 000000000010101
C-insturction 的主要难点在于如何用二进制表示 C-instruction:
1
1
需要注意的是,上述的映射是以 screen 所在的内存区域为基准的。当 screen 作为整个内存的一部分时,需要将之前的量加上,比如:
word = Screen[32*row + col/16] #without base addres
word = RAM[16384 + 32*row + col/16] #with base address
当得到具体寄存器所在地址后,如果我们要改变该寄存器中任意一个像素点,可以使用 col % 16
来找到该像素点并赋值
k
对应的 75)就会以二进制的形式写入该寄存器RAM[24576]
。如果其值为 0
,代表没有按键按下。可以使用课程提供的 assambler 或者 cpu emulator 来进行 symbolic 语言到机器语言的转化。
Hack 编程语言需要的功能有:
下面是一些基础的例子:
# D = 10
@10
D = A
# D++
D = D + 1
# D = RAM[17]
@17
D = M
# RAM[17] = 0
# 注意这里的 0 是表达式!而不是常量,HACK 里常量是不能直接写入 M 的
@17
M = 0
# RAM[17] = 10
# 注意,Hack 的指令集并不能支持将常量写入 M 的操作
# 所有的操作都必须放到寄存器里去过一遍
@10 # 将常量 10 写入到 A
D = A # 之后 A 需要复用(写入 17,关联 M),因此这里将当前 A 代表的常量写入到 D
@17 # 使用 A 取地址,准备关联 M
M = D # 关联 M 到 RAM[17],并写入 D 中的常量 10
# RAM[5] = RAM[3]
@3
D = M
@5
M = D
指令在转换过程中会忽略所有的 white space。
# adds up two numbers
# RAM[2] = RAM[0] + RAM[1]
@0
D = M # D = RAM[0]
@1
D = D + M # D = D + RAM[1]
@2
M = D # RAM[2] = D
需要特别注意的是,计算机并不会自动终结程序。一种终止程序的解决方案是,在指令序列的末尾添加一个无限循环,让整个程序不会再继续执行下去:
@6
0;JMP
这些 symbols 主要作为虚拟寄存器(Virutal register),来提高可读性。考虑下面的程序:
@15
D=A
@5
M=D
这段程序中,A 既充当了数据寄存器,又充当了地址寄存器。如果单看带 @
这一段,我们根本不知道 A 到底使用的哪个寄存器。R0-R15 代表着我们指定了将要使用的寄存器是 0-15 号寄存器,而不是别的:
@R5 # case sensitive! no r5
M=D
机器语言(汇编语言)中,只存在着一种 branching 的方式:goto:
# sinnum.asm
# Computes: if R0>0, R1 = 1, else R1 = 0
@R0
D = M
# ROM[8] 会针对另外一个条件做处理
# 如果 D > 0, 则跳转到 ROM[8]
@8
D;JGT
# 否则做另外一个分支
# 两个分支都会使用 R1 寄存器
@R1
M = 0
# 无限循环,结束程序(在分支 R0 <= 0 上)
# 入口是 ROM[6],但会跳转到 ROM[10],无限循环最终会稳定在 ROM[10]
@10
0;JMP
# ROM[8], D > 0 分支
@R1
D = M
# 做 D > 0 分支的结束
@10
0;JMP
我们可以使用一些 label 代替具体的位置,来提高可读性:
# jump to POSITIVE branch
@POSITIVE
D; JGT
(POSITIVE)
@R1
M = 1
# infinite loop
# Jump to the @end
(END)
@END
0;JMP
括号中的内容可以被视作注释,不会被转写;但引用(@END
)这个将在转写中被替代为对应的 ROM 入口。
Hack 中使用 16bits 的寄存器作为 variable 的唯一标识方式。在Hack 语言中,我们可以通过 @varName
的方式来定义一个变量。这个变量存储于 RAM 中,其起始位置为 RAM[16]
(0-15 被 R0-R15 占了)。下面是一个具体的例子:
# exhange two values
# temp = R1, R1 = R0, R0 = temp
@R1
D = M
@temp #此时 M 对应的是变量所关联的寄存器
M = D
@R2
D = M
@R1
M = D # R1 = R0
@temp
D = M # 再次关联 temp 的值到 M,并存储到 D 中
@R0 # 最后将该值存放到 R0 中,交换完毕
M = D
(END)
@END
0;JMP
需要注意的是,变量被识别的前提是在程序中无法找到与其对应的 label 注释。这个是与 branch 的根本不同:
@name + (name)
的形式,分配位置与具体 label 所在位置有关@name
的形式,分配位置从 RAM[16]
开始在 Hack 中写循环一般要确定两个入点:
下面是一个循环累加的例子:
# compute RAM[1] + RAM[2] ... + RAM[n]
# 初始化
@R0
D=M
@n
M=D # 将 R0 的值用于 n 的初始化
@i
M = 1 # i = 1
@sum
M = 0 # sum = 0
(LOOP)
@i
D = M # 获取当前 i 值
@n
D = D - M # 获取 n 值,计算 i-n 的结果
@STOP
D;JGT # 如果 i-n > 0,则循环超过上限,停止
@sum
D = M
@i
D = D+M # 否则,sum += i
@sum # 覆写 sum
M = D
@i
M = M + 1 # ++i
@LOOP # 再次循环
0;JMP
(STOP)
@sum
D = M
@R1 # 将 sum 值存储到 R1 中
M = D
(END)
@END
0;JMP