寄存器
32个通用寄存器(GPR)
编号从00000到11111
$zero
的值永远是0,无法改变
$1
保留给汇编器,一般不使用此寄存器
三个特殊寄存器
- PC:它用于存储当前 CPU 正在执行的指令在内存中的地址。需要注意的是,这个寄存器的值不能用常规的指令进行取值和赋值,但这并不意味着不能做到,只是麻烦一些。那么,怎样对其取值、赋值呢?这可以作为一个思考的题目,在之后的学习中自行探索。(提示:跳转)
- HI:这个寄存器用于乘除法。它被用来存放每次乘法结果的高 32 位,也被用来存放除法结果的余数。
- LO:HI 的孪生兄弟。它被用来存放每次乘法结果的低 32 位,也被用来存放除法结果的商。
CP0寄存器
CP0 是一个系统控制协处理器,而 CP0 寄存器则是该协处理器工作时需要用到的一些寄存器。在我们的实验中,只会用到其中的 4 个寄存器:SR、Cause、EPC 和 PRId。
- **SR:**用于系统控制,决定是否允许异常和中断
- **Cause:**记录异常和中断的类型
- **EPC:**保存异常或中断发生时的 PC 值,也就是发送异常或中断时 CPU 正在执行的那条指令的地址。当处理完成之后,CPU 会根据这个地址返回到正常程序中继续往下执行。
- **PRId:**处理器 ID,用于实现个性的寄存器。
MIPS汇编指令
指令,即是由处理器指令集架构(Instruction Set Architecture,可以理解为计算机体系结构中对程序相关的部分所做的定义)定义的处理器的独立操作,这个操作一般是运算、存储、读取等。一个指令在 CPU 中真正的存在形式是高低电平,也可以理解为由 01 序列组成的机器码。
为了便于人类理解,指令一般由汇编语言来表示。汇编指令只是指令的一种表示形式而已,其实质是一样的。
指令的格式
在 MIPS 汇编语言中,指令一般由一个指令名作为开头,后跟该指令的操作数,中间由空格或逗号隔开。
指令的操作数一般为0~3个,每个指令有其固定的操作数个数。一般格式如下两种:
指令名 操作数 1, 操作数 2, 操作数 3
指令名 操作数 1, 操作数 3(操作数 2)//一般用于存取
所谓操作数,就是指令操作所作用的实体,可以是寄存器、立即数或标签,每个指令有其固定的对操作数形式的要求。标签最终会由汇编器转换为立即数。
立即数:在指令中设定好的常数,可以直接参与运算,一般为16位二进制
标签:表示一个地址,以供指令来引用。一般用于表示一个数据存取的地址(类似于数组名)、或者一个程序跳转的地址(类似于函数名,或者 C 语言中
goto
的跳转目标)。标签用如下的方式写出:name:
name代表这个标签的名称,可以自行取名,这一行不占用内存
add $s0, $a0, $a1
addi $s0, $a0, 12
mult $s1, $s2
beq $a1, $a2, -2
blez $s1, -2
jr $ra
j 0x00003014
可写成如下形式
beq $a1, $a2, loop
j loop
这里的loop
就是一个标签,代表一段代码的起始地址。在进行汇编时,汇编器会自动把标签转换为我们所需要的立即数,这样就不用我们自己去计算这些地址偏移量,简化了编程难度。
注意:标签标记的是一个位置,而不是某一行!跳转后会从标签的下一行开始逐行执行
注意:在 MARS 中,跳转指令只能使用标签来进行跳转,不能使用立即数!
由此可以看出,操作数的形式并非严格固定,而是具有一定的灵活度。虽然在 MIPS 标准指令集中,一条机器码指令的格式是固定的,但汇编器可以将多种形式的汇编指令转换为同样意思的机器码指令。因此,许多指令有比标准写法简单的写法。
机器码指令
计算机只能理解二进制形式的数据,而我们前面所说的汇编语言,最终就会转化为机器语言——也就是机器码指令, CPU 可以直接识别这种机器语言,从而去完成相应的操作。
在我们学习的 MIPS 汇编中,所有的指令长度均为 32位 ,即 4 字节,或者说 1 字。以16进制来表示就是8位16进制数。例如下面这段汇编指令:
.data
.text
.global main
main:
addi $t0, $0, 100
ori $t1, $0, 200
add $t2, $t1, $t2
sub $t3, $t2, $t1
lui $t4, 233
ori $v0, 1
ori $a0, 2333
mthi $t1
syscall
nop
loop:
j loop
nop
转换后的结果为(16进制)
20080064
340900c8
012a5020
01495822
3c0c00e9
34420001
3484091d
01200011
0000000c
00000000
08000c0a
00000000
机器码指令格式
在 MIPS 指令集中,指令分为三种格式:R 型、I 型和 J 型。
以下的32位指的是二进制数。
R型指令
操作数最多,一般用于运算指令。例如
add
、sub
、sll
等。其格式如下:I型指令
I 型指令的特点是有 16 位的立即数。一般用于
addi
、subi
、ori
等与立即数相运算的指令。(这里需要注意:在写汇编语言的时候,需要使用负号来标记负数,而不要和机器码一样认为首位的 1 就代表负数),或beq
、bgtz
等比较跳转指令,因为它们要让两个寄存器的值相比并让 PC 偏移 offset 这么多,刚好利用了全部的字段。还有存取指令,例如sw
、lw
,它们在使用时需要对地址指定一个偏移值,也会用到立即数字段。J型指令
很少,常见的为
j
和jal
。他们需要直接跳转至某个地址,而非利用当前的 PC 值加上偏移量计算出新的地址,因此需要的位数较多。
严格来说,并非所有的指令都严格遵守上面三种格式,有的如 eret
、syscall
指令一样没有操作数;有的如 jalr
指令一样某些字段被固定为某个值。不过,就大部分指令而言,都可按上面三种格式进行解释,某些字段被固定也可以按照格式来识别为 R、I、J 中的一种,因此这三种格式要着重理解。
解读:
**op:**也称 opcode、操作码,用于标识指令的功能。CPU 需要通过这个字段来识别这是一条什么指令。不过,由于 op 只有 6 位,不足以表示所有的 MIPS 指令,因此在 R 型指令中,有 func 字段来辅助它的功能。
func: 用于辅助 op 来识别指令。
rs、rt、**rd:**通用寄存器的代号,并不特指某一寄存器。范围是
$0~$31
,用机器码表示就是 00000~11111(将指令翻译成机器码时会用到)。**shamt:**移位值,用于移位指令。
**offset:**地址偏移量。在跳转中,它的值就等于:从该条指令的下一条指令开始数,到目标
label
一共经过的指令条数(字数)。为了程序员的便利,MIPS只允许使用标签来跳转,不过我们仍然有手段计算offset的具体值,在此之前,我们需要先了解指令集中为什么是这样计算的。仔细观察跳转指令,以beq为例,
目标地址 = 当前指令地址 + 4 + (offset × 4)
当前指令地址 + 4
就是下条指令指令地址当前指令地址 + 4 + (offset × 4)
是目标地址,故offset
就等于下条指令到目标lable
经过的指令条数(字数)。为什么要拓展到32位?因为MIPS使用的是32位地址空间,这意味着内存地址都是32位的数字。而offset常为16位的数字,因此需要转换。
题外话,在将
offset × 4
时,使用的手段是将它左移两位,这使得地址的最后两位永远是00
。这吻合了MIPS里的存储方式,每条指令是4字节对齐的,每条指令的地址一定是4的倍数,最低两位只能是00
。(这会影响到跳转有效地址的范围)**immediate:**立即数
**address:**跳转目标地址,用于跳转指令。
扩展指令和伪指令
扩展指令
汇编器将一些常用、但标准指令集不提供的功能封装为一条指令;或者改变现有指令的操作数的形式或个数,使其以新的形式出现。需要注意的是,它们只是形式上是一条新指令,而实际上,在汇编器将其汇编之后,还是使用标准指令来实现的。
li
指令:用来为某个寄存器赋值,如li $a0, 100
就是将100赋给$a0
寄存器la
指令:使用标签给寄存器赋值,标签本质上对应一个32位地址,la $t0, fibs
就是将fibs
这个标签的地址存入$t0
中
伪指令
伪指令是用来指导汇编器如何处理程序的语句,有点类似于其他语言中的预处理命令。伪指令不是指令,它并不会被编译为机器码,但他却能影响其他指令的汇编结果。
伪指令的主要用途是标识数据段和代码段的位置,并为声明的数据分配空间,以及初始化常量
.data
格式:
.data[address]
说明:
- 定义程序的数据段,初始地址为address,若无address参数,初始地址为设置的默认地址
- 需要声明的程序变量需要紧跟着该指令
.text
格式:
.text[address]
说明:
- 定义程序的代码段,初始地址为address,若无address参数,初始地址为设置的默认地址
- 该指令后面就是程序代码
- 在 MARS 中如果前面没有使用
.data
伪指令,可以不使用.text
直接编写程序代码,代码将放置在前面设置的代码段默认地址中,但如果前面使用了.data
伪指令,务必在代码段开始前使用.text
进行标注。
.space
格式:
[name]: .space [n]
说明:
- 申请n个字节未初始化的内存空间,类似于其他语言中的数组声明
- 这段数据的初始地址保存在标签
name
中 name
的地址是由.data
段的初始地址加上前面所申请的空间大小计算得出的。由于前面申请的空间大小不定,有可能会出现后来申请的空间没有字对齐的情况,从而在使用sw
,lw
一类指令时出现错误,所以在申请空间时尽可能让 n 为 4 的倍数,防止在数据存取过程中出现问题。
用法:用来声明一个可能用到的空数组
.word
格式:
[name]: .word [data1],[data2]...
说明:
- 在内存数据段中以字为单位连续存储数据
data1
,data2
,…即,将datax写入对应1个字的空间 - 这段数据的初始地址保存在标签
name
中
用法:用来存储一个或多个常量,以供以后使用
- 在内存数据段中以字为单位连续存储数据
.asciiz
格式:
[name]: .asciiz "[content]"
说明:
- 以字节为单位存储字符串,末尾以NULL结尾
- 这个字符串在内存数据区的初始地址保存在标签
name
中 - 注意与
.ascii
伪指令的区别 .asciiz
由于是按字节存储,可能会导致之后分配的空间首地址无法字对齐的情况发生
用法:用来存储一个字符串,以供以后使用
宏
可以把宏理解为浓缩的代码段,汇编器会把所有的宏语句替换为相应的代码段
不带参数的宏
.macro macro_name
# 代码段
.end_macro
如:
.macro done
li $v0, 10
syscall
.end_macro
在需要程序停止运行的地方,使用 done 语句,就可以让程序在那里退出。
带参数的宏
.macro macro_name(%parameter1, %parameter2, ...)
# 代码段
.end_macro
带参数的宏在 macro_name 后面有若干个用括号括起来的形式参数,每两个形式参数中间用逗号隔开,参数名前面有一个百分号。
如:
.macro getindex(%ans, %i, %j)
sll %ans, %i, 3
add %ans, %ans, %j
sll %ans, %ans, 2
.end_macro
宏定义
与C语言中类似,一般用于常量的定义,是.eqv
用法:.eqv EQV_NAME string
汇编器会把所有 EQV_NAME 的地方替换成 string,这可以用来定义一些常量。
条件语句
.text
li $t1, 100 #t1 = 100
li $t2, 200 #t2 = 200
slt $t3, $t1, $t2 #if(t1 < t2) t3 = 1
beq $t3, $0, if_1_else #if($t3 == 0) ->if_1_else
nop
#do something
j if_1_end #jump to end
nop
if_1_else:
#do something else
if_1_end:
li $v0, 10
syscall
循环语句
.text
li $t1, 100 #n
li $t2, 0 #i
for_begin1: #for (int i = 0; i < n; i++)
beq $t2, $t1, for_end1
#do something
addi $t2, $t2, 1 #i++
j for_begin1
for_end1:
li $v0, 10
syscall
一些细节
小端存储和大端存储
小端存储是指最低有效字节放在内存的最低地址,最高有效字节放在内存的最高地址
eg.假设某条指令的机器码为
0x00852020
,在小端存储格式中,这4个字节在内存中的存储顺序应该是:地址 值 0x1000 0x20 0x1001 0x20 0x1002 0x85 0x1003 0x00 值得一提的是,小端存储仅指某条指令的机器码内部存储方式,多条指令之间依旧是递增存储的,如下:
add $s0, $a0, $a1 #机器码为0x00852020 addi $s0, $a0, 12 #机器码为0x2080000C
地址 值 0x1000 0x20 0x1001 0x20 0x1002 0x85 0x1003 0x00 0x1004 0x0c 0x1005 0x00 0x1006 0x80 0x1007 0x20 slt
调用函数时配套使用
jal
和jr
,jr
能带你回到原函数编写程序时,十进制表示的立即数用负号区分正负,十六进制表示的立即数会被视为一个补码表示的数
一个数的补码已知,它的相反数的补码:取反加1
在测试中,
ori
指令可以当作li
和move
来使用lui
指令的全称应该是:将立即数加载到高位,且将低位置0
指令 | Opcode | RegWrite | RegDst | ALUSrc | Branch | MemWrite | MemtoReg | ExtOp | ALUControl | 类型 |
---|---|---|---|---|---|---|---|---|---|---|
add | 000000 | 1 | 1 | 0 | 0 | 0 | 0 | X | 010 | R |
sub | 000000 | 1 | 1 | 0 | 0 | 0 | 0 | X | 110 | R |
ori | 001101 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 001 | I |
lw | 100011 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | 010 | I |
sw | 101011 | 0 | X | 1 | 0 | 1 | X | 1 | 010 | I |
beq | 000100 | 0 | X | 0 | 1 | 0 | X | X | X | I |
lui | 001111 | 1 | 0 | 1 | 0 | 0 | 0 | X | 011 | I |
nop |
拓展元件:0是无符号拓展,1是符号拓展