MIPS汇编指导
MIPS汇编指导
在阅读本指导书前,需要你先下载Mars软件并配置java运行环境,具体方式可以查看pdf版指导书《Mars使用教程》。如果你对于mips汇编是完完全全的零基础,那么,欢迎阅读本指导书,我会用最为朴实的语言,最为细致的样例,只讲方法,不讲原理,手把手教你写汇编程序,保证你奶奶来了都能学会。
入门篇
赋值
在C语言中,我们最先用到的便是给变量赋值,这个在mips中该如何实现呢?
下面这条指令的意思,就是将t2寄存器的值赋值为100,其实际含义是给$t2赋值为 100或上0后的结果
ori $t2,100
当然了,我们也可以使用伪指令,下面这条指令的意思,便是将$t1寄存器赋值为100,该指令与上一条指令的效果完全一致。
li $t1,100
是不是很简单呢?
传值
学过C语言的同学马上想到,在我给a复制后,想把a的值传给b,我们常用b=a的方法进行传值,那么在mips中该如何实现呢?
move $t1,$t2
这条指令的意思,就是将t2寄存器的值传给t1寄存器,同时t2寄存器中的值不变,与C语言中b=a的含义基本一致。
运算
在mips中,加减乘除便更为简单,如下公式的含义为t1寄存器的值为t2寄存器的值加上t3寄存器的值
add $t1,$t2,$t3
addi $t1,$t2,100
减法、乘法使用方法相似,可以查看mips指令集进行学习,这里不再赘述。
而除法却不太一样,在C语言中,两个整数的除法是整除,而在mips中,除法是带有余数的,我们来看mips指令集中对于除法的描述:

商存放在LO寄存器,而余数存放在HI寄存器,我们用mfhi指令来读取HI寄存器,用mflo来读取lo寄存器,完整代码如下所示:
li $t1,5 #被除数为5
li $t2,2 #除数为2
div $t1,$t2 #计算5/2,其中商存放在LO寄存器,而余数存放在HI寄存器
mfhi $t3 #将hi寄存器中的值取出放到t3寄存器(值为1)
mflo $t4 #将lo寄存器中的值取出放到t4寄存器(值为2)
输入输出
学习了最简单的几种指令后,我们来学习输入输出(scanf和printf)的方式,这一块是很重要的内容,也是一个小小的难点
众所周知,mips中有很多的的寄存器,虽然大部分寄存器可以乱用(不是),但是有一些寄存器是有着自己独特的含义的,比如接下来要学习的v0寄存器。
v0寄存器的值与syscall(系统调用)是紧密联系在一起的,如果用非常非常非常大白话的方式来讲,就是v0的值决定了syscall是输入还是输出,输入(输出)字符串还是数字,如下代码
li $v0,5
syscall
这个代码的表面含义是,把v0寄存器的值赋值为5,然后进行系统调用,而其实际含义是读入一个整数,该整数的值被存储在v0寄存器中。或者你可以这么理解:v0寄存器为5决定了syscall为读入一个整数,并将读入的值存储在V0寄存器中。
有同学会问,那我怎么知道v0的值与syscall的值的对应关系呢?其实mars上都有写,如图,点击help

点击syscall,其中就有v0值对应syscall的方法,比如第一排我可以这么理解:v0是1,对应syscall是输出整数(print integer),输出的值是a0寄存器内的值。是不是很简单?

第一个程序
学会了这些,我们就可以写一个最最简单的程序。
li $v0,5
syscall #读入一个整数
addi $v0,$v0,1 #加1
move $a0,$v0 #把v0寄存器的值移到a0
li $v0,1
syscall #输出a0寄存器的值
在保存后,我们点击run-assemble

点击最大的绿色按钮

随便输入一个数20,系统自动输出21,是不是和我们预计的结果一样?

恭喜你,你已经学会了mips的基本使用方法了,奖励自己休息一下
基础篇
判断
在c语言中,常用if-else结构,在mips中如何实现呢?我们不得不提一个叫做ble的指令,如下图,其含义是若t1寄存器的值小于等于6,则跳转到lable1处,否则,执行ble下一条指令
ble $t1,6,lable1
为了便于理解,我们可以看以下代码
li $t1,5
li $v0,1 #确立syscall为输出a0的值
ble $t1,5,lable1
li $a0,5 #若t1不是小于等于5,继续执行该语句
syscall #输出5
lable1: #若t1小于等于5,跳转至此处
li $a0,6
syscall #输出6
我们把t1的值赋值为了5,按照道理来说,应该跳转到lable1继续执行,最后输出6,我们看一下输出结果

是不是和预计的完全一致?
如果我们把上述代码第三行改成下图所示,你猜猜会输出什么,是5吗?
ble $t1,3,lable1
(我写指导书,从来都不问没有答案的问题)输出结果如下图:

输出结果是56,而不是我们预计的5,这是因为在输出5后,程序并没有结束,他会继续向下运行label1的内容,如果想要阻止这种行为,可以添加终止程序代码
li $v0,10
syscall
所以完整代码如下
li $t1,5
li $v0,1
ble $t1,3,lable1
li $a0,5
syscall
li $v0,10
syscall
lable1:
li $a0,6
syscall
相应的,还有一些等于、大于等于、大于、小于这些指令,使用方法与ble完全一致,可以在mips指令手册中查找,这里不再一一讲解。
循环
循环超级简单,可用判断语句实现
li $t1,0
li $a0,0
loop:
addi $a0,$a0,1
addi $t1,$t1,1
ble $t1,3,loop
li $v0,1
syscall
当执行到ble时,若t1寄存器的值小于等于3,则会重新跳转至loop,只有大于3时,才会跳出循环,mips通过这种方式模拟循环(for or while)
输出结果与预期一致

数组
在mips代码中,一般会分为两部分,.data部分与.text部分,我们前面写的代码都属于.text部分,但是当我们需要使用数组时,则要使用.data段。如下图,下图在.data定义一个起始地址为array,空间为400的数组,这就是我们需要用到的部分,然后我们在.text部分写代码。
.data
array:.space 400
.text
xxxx
我们想要使用数组,首先需要将数组首地址赋值给一个寄存器,用la指令
la $t1,array
然后就可以使用数组啦,因为int有4个字节,所以每个数字占用四个空间,如下图,便是将array[0]赋值为3,array[1]赋值为4,其中 4(t1)可以理解为偏离t1寄存器中的地址4个空间,值得注意的是,在使用这种表示方法时,必须使用sw(存)和lw(取)来赋值与取值。
li $t2,3
sw $t2,0($t1) #array[0]=3
li $t2,4
sw $t2,4($t1) #array[1]=4
全代码如下
.data
array:.space 400
.text
la $t1,array #t1内存的是数组首地址
li $t2,3 #array[0]=3
sw $t2,0($t1)
li $t2,4 #array[1]=4
sw $t2,4($t1)
li $v0,1
lw $a0,0($t1) #输出array[0]
syscall
lw $a0,4($t1) #输出array[1]
syscall
输出结果为34,符合预期

小tips:
有同学想要在3和4之间输出一个逗号该怎么办呢,我们可以在.data段中加一句话,意思是a代表字符串","
a: .asciiz","
然后在想要输出逗号的地方加上这么一句话(v0为4时输出以a0为起始地址字符串)
la $a0,a
li $v0,4
syscall
全代码如下(在上方代码基础上添加)
.data
array:.space 400
a: .asciiz"," #新增
.text
la $t1,array
li $t2,3
sw $t2,0($t1)
li $t2,4
sw $t2,4($t1)
li $v0,1
lw $a0,0($t1)
syscall
la $a0,a #新增
li $v0,4
syscall
li $v0,1
lw $a0,4($t1)
syscall
输出结果如下:

数组结束了吗?还有一点小小的遗漏,
想要给array[1]赋值为4,除了上面那种写法外,我们也可以这样写:
.data
array:.space 400
.text
la $t1,array #t1内存的是数组首地址
li $t2,4
addi $t3,$t1,4
sw $t2,0($t3) #array[1]=4
想想看,这么写的好处是什么?
揭晓答案:当我遇到以下代码时:
int a[20]
for(int i=1;i<=10;i++){
a[i]=i;
}
如果采用刚才我们所讲述的赋值方法,我们需要写十次赋值函数,因为sw $t2,4($t1) 中的4,不能用一个变量来代替。而如果我们采用现在所讲的赋值方法,仅需如此便可解决:
.data
array:.space 400
.text
la $t1,array #t1内存的是数组首地址
li $t2,1 #t2内存的是代码中的i值
loop:
sw $t2,0($t1) #a[i]=i
addi $t1,$t1,4 #下一个数组元素地址(加4个单位空间)
addi $t2,$t2,1 #i++
ble $t2,10,loop
进阶篇
递归
大的来了,这个超难
第一步,我们来学习jal指令,jal指令的意思是跳转到相应位置,并将现在的指令位置存储在ra寄存器中,当跳转结束后可由jr $ra指令返回,返回值则存储在v0寄存器中。
如下图,在执行到2处时会跳转到4处,当4执行到5处时,则会返回至3处。
li $a0,1 --1
jal label --2
li $v0,1 --3
syscall
li $v0,10
syscall
label: --4
addi $a0,$a0,1
jr $ra --5
输出结果为2,符合预期

这是递归程序中非常重要的一个指令之一
第二步,我们来了解sp指针。本教程只讲方法,不讲原理,因此对于sp指针详细原理,可以参考百度或是前面的指导书,在这里,你只需要了解sp指针是一个可以分配空间的玩意儿,当我们执行以下代码
addi $sp,$sp,-8
你可以理解为我们分配了8个单位的空间,可以用来存储数据
sw $t1,0($sp)
sw $t2,4($sp)
通过这种方式,可以将t1,t2的值存储在相应空间内,此时,无论你如何改变t1,t2寄存器的值,只要你执行以下代码
lw $t1,0($sp)
lw $t2,4($sp)
t1和t2寄存器就会恢复到原有的值
当然,在使用结束后,别忘了把sp指针加回来
addi $sp,$sp,8
这就是sp指针的使用方式。
我们来看如下递归代码:
输出:f(3)
int f(x){
if(x==1) return 1;
return f(x-1)+1;
}
其对应汇编代码为:
li $a1,3
jal label
move $a0,$v0 #输出
li $v0,1
syscall
li $v0,10
syscall
label:
beq $a1,1,ret #对应如果x==1,则xxx
addi $sp,$sp,-8
sw $a1,0($sp)
sw $ra,4($sp) #将返回地址与x的值存储起来
addi $a1,$a1,-1 #x-1
jal label #对应执行递归函数f(x-1)
lw $a1,0($sp)
lw $ra,4($sp)
addi $sp,$sp,8 #取出返回地址与x的值
move $s1,$v0
addi $v0,$s1,1 #对应f(x-1)+1
jr $ra #返回
ret:
li $v0,1 #返回值存储在v0寄存器中
jr $ra
写在最后
如果发现了本指导书的错误,或是对某些地方产生疑问,欢迎各位同学在计算机硬件基础群中添加助教的微信反馈。