进阶篇
进阶篇
函数
虽然前面有宏定义,但是 MIPS 本身还是有函数的使用的。
宏定义和函数的关系就像是 C 语言当中 #define 关键字 和 函数 的关系一样。
首先,我们来学习 jal(jump and link)指令。
jal 指令的意思是跳转到对应标签的位置,并将当前执行的指令位置存储在 $ra 寄存器中。
前面说过 $a0 - $a3 寄存器的作用是函数传参,意味着如果在 jal 指令调用之前,往 $a0 - $a3 寄存器当中存放了数据,那么 jal 指令调用之后,这几个寄存器的值是可以直接拿出来用的。
既然 $ra 寄存器当中存放了调用之前的执行的位置,当函数调用结束后可使用 jr(jump register)指令:jr $ra ,返回(return)这个函数,函数的返回值存储在 $v0 和 $v1 寄存器中。(MIPS 甚至可以有两个返回值)
考虑下面的 MIPS 汇编代码,在执行到 2 处时会跳转到 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,符合预期。

考虑下面的 C 代码:
int add(int a, int b) {
return a + b;
}
int main() {
int x = 1, y = 2;
printf("%d", add(x + y));
return 0;
}
转换成 MIPS 代码如下所示:
.macro PI(%n)
move $a0, %n
li $v0, 1
syscall
.end_macro
li $a0, 1
li $a1, 2
jal add
PI($v0)
end:
li $v0, 10
syscall
add:
add $v0, $a0, $a1
jr $ra
有的同学可能会问了 那 $a0 - $a3 和起来也就只有 4 个寄存器啊,我函数传参难道最多只传 4 个参数吗?
要想解决这个问题,我们还需要了解 $sp 指针。
在这里,你只需要了解 sp 指针是一个可以分配空间的玩意儿,当我们执行以下代码后:
addi $sp, $sp, -8
可以理解为我们分配了 8 个单位的空间,可以用来存储数据。
sw $t1, 0($sp)
sw $t2, 4($sp)
通过这种方式,可以将 $t1 和 $t2 的值存储在相应内存空间内,此时,无论你如何改变 $t1 和 $t2 寄存器的值,只要你重新把原来的值 load 出来,就可以重新获得之前存进去的值。
lw $t1, 0($sp)
lw $t2, 4($sp)
$t1 和 $t2 寄存器就会恢复到原有的值。
当然,在使用结束后,别忘了把 $sp 指针加回来。
addi $sp, $sp, 8
这就是 $sp 指针的使用方式。
递归
大的来了,这个超难。在这里我们只讲方法,不讲原理。
不过递归部分,本次实验不做要求,仅供学有余力的同学参考。如果掌握的话,对后续学习《编译技术》理解递归可能会有帮助。
我们来看如下递归代码:
int f(x) {
if (x == 1) return 1;
return f(x - 1) + 1;
}
递归函数的编写,主要解决的问题就是下面两点:
传参:因为需要进行递归,没有办法把所有的传参都放在 $a0 - $a3、$v0 和 $v1 寄存器。
返回:包括每次返回时函数的返回值、返回之后要执行的下一条指令。
这就不得不让前面介绍的、强大的 $sp 指针出面解决了。
现在我们想求 f(3) 的值,其对应汇编代码为:
li $a1, 3
jal label
move $a0, $v0 # 输出
li $v0, 1
syscall
li $v0, 10 # 结束程序
syscall
label:
beq $a1, 1, ret # 对应如果 x==1,跳转到 ret
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