前言
本系列文章将梳理《计算机组成原理》这门课的相关知识点,教材使用的是《计算机组成原理:硬件/软件接口》MIPS版,原书第五版。
本文所属章节为MIPS汇编指令章节,重点内容为MIPS汇编指令的格式、五种寻址方式以及MIPS汇编对过程的支持。
一. MISP32指令集概述
MIPS是一种RISC指令集,即精简指令集,MISP指令都是32位长。
常见的指令集:MIPS,ARMv7, ARMv8,Intel x86
MISP中的运算操作数必须来自寄存器或者指令本身,不像X86。
MISP一共有32个32位的寄存器,一共128B。
- 保存寄存器:
$s0
-$s7
- 临时寄存器:
$t0
-$t7
- 零寄存器:
$zero
二. MISP三类汇编指令
1. MISP的三种指令格式
MISP有三种指令格式,分别是R型,I型和J型。
下文将逐一进行解释。
1.1 机器码字段和寄存器号
- op:操作码
- rs:源寄存器
- rt:第二个源寄存器
- rd:目的寄存器
- shamt:shift amount 偏移量
- funct:功能码
$t0
-$t7
是8-15号寄存器$s0
-$s7
是16-23号寄存器
1.2 指令格式
- R型指令格式:op(6) + rs(5) + rt(5)+ rd(5) + shamt(5) + funct(6)
- 常见指令:add、sub、sll、srl、slt、jr寄存器跳转指令
- funct:add的功能码是32,sub的功能码是34
- 移位指令sll、srl的rs字段为0(之前说错了,说成了rt字段!)
- I型指令格式:op(6) + rs(5) + rt(5) + address(16)
- 常见指令:
- 立即数系列:addi、ori。
- 数据传送指令:sw、lw。
- 决策指令:beq
- 注意:
- 在
addi
和ori
中,目的寄存器变成了rt
,address字段存立即数。 lw
的op是35,sw
的op是43。- rs永远是内存地址,所以lw和sw中会形成“交叉现象”。例如,
lw $s0,20($s1)
指令中,rs存放的是s1,rt存放的是s0,address存放20;sw $t0,8($s1)
指令中,rs存放的是s1,rt存放的是t0。 - 解释3:beq指令中,address存标签的偏移量。
- 在
- 常见指令:
- J型指令格式:op(6) + address(26)
2. 运算指令
- 加法:
add t0,t1,t2
,R型指令,将t1和t2结果相加放到t0。 - 减法:
sub t0,t1,t2
,R型指令,将t1减去t2的结果存到t0。 - 加/减立即数:
addi t0,t1,100
,I型指令,(无减立即数指令) - 逻辑按位操作:
- 按位与:
and t0,t1,t2
- 按位或:
or t0,t1,t2
- 取反指令:MISP中用或非指令nor代替NOT指令,而根据离散数学,NOT(A OR 0) = NOT A,故
nor $t1,$t0,$zero
,表示将t0的值取非。 - 立即数与:
andi $s1,$s2,100
,I型,s1为目的寄存器 - 立即数或:
ori $s1,$s2,100
,I型
- 按位与:
- 逻辑移位:空出来的位置补0。
- 逻辑左移:
sll s0,s2,2
,2是shamt字段,s0是rd字段,s2是rt字段,rs字段为空。 - 逻辑右移:
srl s0,s2,2
- 逻辑左移:
- 算术右移:空出来的位用符号位填充。
sra s0,s2,2
例题:翻译下面C语言代码:B[8] = A[i-j],假设i和j分别赋值给寄存器
$s3
和$s4
,数组A和B的基地址分别在寄存器$s6
和$s7
中。
解答:
这下面的解答有几个注意的点:
①MISP中每次进行数组访问,都要将偏移左移两位。
②进行lw的时候,一般直接加上基地址。
sub $t0, $s3,$s4 # i-j
sll $t1,$t0,2 # MISP中数组访问一定要 << 2 转为字节地址
add $t1,$s0,$t1 # &A[i-j]
lw $t2, (0)$t1 # tmp = A[i-j]
sw $t2, (32)$s7 # B[8] = tmp
3. 数据传送指令
- 寄存器数据送到内存:
lw $s0,20($s1)
- 作用:将
a[5]
送到s0。 - 内存是按照字节编址,而
a[5]
实际上是5个字!
- 作用:将
- 寄存器间的数据传送:
- 写法1:
addi s1,t0,zero
—也可以用于将立即数放入寄存器。 - 写法2:伪指令。
move s1, t0
- 写法1:
- 取立即数:
li $s2,10
,伪指令
3.1 装载32位立即数
addi的address字段用来存放立即数,但是这个字段只有16位。
那么我们要如何装载32位的立即数到寄存器中呢?
一般有两种做法:
- 方法①:lui + ori。
- 第一步,使用lui将立即数的高16位放入寄存器。
lui $s2,10A2
,这个指令将立即数放到寄存器的高16位,并且把低16位设置为0。 - 第二步,将立即数的低16位和寄存器进行或运算。
ori $s2,7FFF
- 第一步,使用lui将立即数的高16位放入寄存器。
- 方法②:直接使用li指令
li $s2,10A27FFFF
思考题:能否使用addi指令替代ori指令?
解答:不能。
首先我们需要理解addi的实现原理。
addi会将立即数进行符号扩展为32位,然后和源操作数相加,将结果送入目标操作数。
而且,addi指令的立即数必须是一个16位的有符号整数。
所以,如果是低16位最高位是1,那么符号扩展后的这个数的高16位都是1,那么相加就会影响原来的高16位。故不能用addi替换ori。
4. 决策分支指令
- 相等则分支:
beq $s0,$s1,Label
,I型 - 不等则分支:
bne $s0,$s1,Label
,I型 - 小于则置位 :
slt t0,s0,s2
,R型。如果s0 < s2,将t0设为1.- 立即数版本:
slti $t0,$t2,10
,I型 - 无符号版本:
sltu $t0,$s3,$s4
,R型
- 立即数版本:
- 寄存器跳转指令:
jr $ra
,跳转到目标寄存器,R型 - 跳转指令:
j Label
,跳转到指定标签
4.1 if-else型翻译
例题:将C代码中的if-else翻译为汇编代码,假设f ~ j ---- $s0 ~ $s4
if ( i = = j ) f = g + h ; else f = g - h ;
解答:
①一般相等我们反而用bne,不等反而用beq。
bne $s3,$s4,Else
add $s0,$s1,$s2 # f = g + h
j Exit
Else:
sub $s0,$s1,$s2
Exit:
...
4.2 while型翻译
例题:( Assume: i ~ k---- $s3 ~ $s5 base of save ---- $s6 )
while ( save[i] = = k )
i = i + j ;
解答:
loop:
move $t0,$s3 # i
sll $t0 ,$t0 ,2 # i << 2
add $t1, $t0, $s6 # &save[i]
lw $t2, 0($t1)
bne $t2, $s5, Exit
add $s3,$s3,$s4
j loop
exit:
...
5. 指令小结
三. MISP中的五种寻址方式
1. 立即数寻址和寄存器寻址
立即数寻址,例如,i型指令 addi、ori、lui等的立即数字段。
寄存器寻址,例如,R型指令的add。
2. 基址偏移寻址:lw/sw
例如,使用数据传送指令lw和sw来访问数组的时候,都是将基地址和偏移量相加后得到内存地址。
3. PC相对寻址:beq/bne
分支指令中的16位偏移地址是字地址,并且是补码,可正可负。
实际上,分支32位地址 = PC + 4 + 字地址偏移量左移两位形成的字节地址
那么,beq的寻址范围:16位的字地址偏移量,扩展后是18位,那么一共有 2 17 − 4 2^{17}-4 217−4个正字节,以及 2 17 2^{17} 217个负字节,即前后大约128KB的范围。
4. 伪直接寻址:j/jal
伪地址寻址包括:j指令、jal指令
为什么说是伪直接寻址?因为直接寻址是直接给出32位的目标地址,而J指令只有26位的地址。
J指令执行逻辑:先将26位地址左移两位形成28位字节地址,然后和PC+4的高32位进行拼接。
所以J指令的寻址范围:26位字地址,扩展28位的字节地址,所以是 2 8 = 256 M B 2^8=256MB 28=256MB,是一块的,不是上下前后的关系。
思考:如何扩大分支跳转指令的范围
扩大beq的跳转范围
我们以beq $s0,$s1,L1
为例。
先将分支指令取反:bne $s0,$s1,Exit
,再来一条J指令:J L1
,这样寻址范围就被扩大了。
扩大J指令的跳转范围
对于J指令,如何跳到32位的范围?
可以先将32位地址用lui + ori
组合装入寄存器,然后使用jr
指令跳转。
四. 过程调用
1. 支持过程的寄存器
- 参数寄存器:
$a0-$a3
- 返回值寄存器:
$v0-$v1
- 返回地址寄存器:
$ra
- 程序计数器:
PC
,保存当前指令的运行地址。
2. jal-jr指令对
- jal:跳转并链接。执行原理:第一步,无条件跳转到下一个标签;第二步,将下一条指令保存到
$ra
中,方便回来。 - jr:跳到寄存器存放的32位地址去。一般配和
$ra
做返回用。
3. 过程调用的C代码翻译
3.1 简单叶过程翻译
题目:翻译下面C代码。
int leaf_example(int g, int h, int i, int j)
{
int f;
f = (g + h) - (i + j);
return f;
}
解答:
addi $sp, $sp,-12
sw $t1, 8($sp)
sw $t0, 4($sp)
sw $s0, 0($sp)
add $t0, $a0, $a1
add $t1, $a2, $a3
sub $s0, $t0, $t1
add $v0, $s0, $zero # 结果放到返回值寄存器
lw $s0, 0($sp)
lw $t0, 4($sp)
lw $t1, 8($sp)
addi $sp, $sp, 12
jr $ra
3.2 嵌套过程的翻译
题目:翻译下面C代码。
int fact(int n)
{
if (n < 1)
return (1);
else
return (n * fact(n - 1));
}
解答:
fact: addi $sp, $sp, -8
sw $ra, 4($sp)
sw $a0, 0($sp)
slti $t0, $a0, 1 # test for n < 1
beq $t0, $zero, L1 # if n >= 1, go to L1(else)
addi $v0, $zero, 1 # return if n <1
addi $sp, $sp, 8 # Recover $sp (Why not recover $ra and $a0 ?)
jr $ra # return to after jal
L1: addi $a0, $a0, -1 # n >= 1: argument gets ( n - 1 )
jal fact # call fact with ( n - 1 )
lw $a0, 0($sp) # return from jal: restore argument n
lw $ra, 4($sp) # restore the return address
addi $sp, $sp, 8 # adjust stack pointer to pop 2 items
mul $v0, $a0, $v0 # return n*fact ( n - 1 )
jr $ra # return to the caller
4. 堆栈操作
需要压栈保存的寄存器:
$s0-$s7
存放主程序变量。$ra
:嵌套过程中需要压栈。
入栈:把$sp
减去保存寄存器个数的4倍,再用sw指令将数据入栈。
帧指针$fp
:指向高地址。$fp
和$sp
之间的空间叫做过程帧。
全局指针:$gp
。主程序使用的变量以及static变量都放在固定的区域,全局指针$gp
指向这个区域。内存从高到低依次是:栈、堆(栈向下生长,堆向上生长),静态数据、正文、保留。
题目:翻译C代码为MISP汇编。
int cal(int g, int h , int i , int j){
int f;
f = (g+h) - (i+j);
return f;
}
解答:假设f保存到
$s0
中。
cal:
addi sp, sp, -4 # 入栈
sw s0, 0(sp) # 只需要保存s0
add t0,a0,a1
add t1,a2,a3
sub s0,t0,t1
add v0,s0,zero # 寄存器之间数据传输
lw s0,0(sp)
addi sp, sp,4 # 出栈
五. 总结
1. 寄存器
2. 指令