汇编语言中的过程就是高级编程语言中的函数。一个过程调用另一个过程,前者称为调用过程,后者称为被调用过程。
过程调用的基本流程如下:
- 输入参数。
- 执行跳转指令jal。
- 保存被调用过程中使用过但仍需在调用过程中使用的寄存器中的数值。
- 执行被调用过程,计算返回值。
- 恢复被调用过程中使用过但仍需在调用过程中使用的寄存器中的数值。
- 执行跳转指令jr跳转回调用过程中。
参数与返回值
过程的输入与输出分别称为参数与返回值。
为了让不同的调用过程与被调用过程都知道参数与返回值所在地,参数会固定存在$a0
, $a1
, $a2
, $a3
四个寄存器,如果参数数目更多,会存在被调用过程的栈中,也称作栈框架。
返回值会保存在$v0
, $v1
两个寄存器中,一般情况下过程只会有一个返回值,大部分情况下只使用一个返回值寄存器。当返回64位的数值时,比如双精度浮点数,会同时用到两个寄存器。
jal与jr指令
执行完被调用过程后,处理器必须知道自己要返回到哪里,从而继续执行调用过程。所以我们需要将这个返回地址保存起来,$ra
就是专门用来保存这个返回地址的寄存器。
执行jal指令时,处理器会首先将返回地址保存到$ra
中,再执行跳转,跳转到被调用过程所在地址。
执行jr指令返回时,会使用指令jr $ra
,将跳转到$ra
中存放的地址,也就是jal保存的地址。
这样处理器就知道自己调用过程时跳转到哪里,又返回哪里。
栈
被调用过程不能对调用过程造成破坏,它不能改变调用过程后续需要用到的数值,即不能改变保存这些值的寄存器。但是被调用过程对数值的处理也需要用到寄存器。于是我们需要保存这些数值中需要被被调用过程使用的一部分,并最后在返回到调用过程时把它们恢复到所在的寄存器中。
这里用到了栈,每个被调用过程都有一个自己的栈,称作栈框架。这个栈会保存被调用过程修改过但最终需要恢复以便于调用过程使用的数值,也会存放参数大于4的被调用过程中的多余参数,被调用过程中的数组局部变量也存放在栈里。
除了数组,被调用过程的局部变量一般存放在$s0
-$s7
中,当除数组之外的局部变量多于8时,多余的局部变量也会存放在栈中。
MIPS中的栈在存储器中是一个倒置的栈,它向地址逐渐变低的方向扩展。也就是说,栈底的地址最大,栈顶的地址最小。MIPS用一个栈顶指针$sp
(也是一个寄存器),存放栈顶的地址,用于指向栈顶元素。通过改变$sp
的值,可以存入数值并扩展栈空间或者释放数值并缩小栈空间。
在保存和恢复那些寄存器时,同时也需要指令对$sp
加减,从而对栈进行空间扩展与释放。
由于有些在调用前用到的寄存器里存储的是之后不可能再用到的值,所以对这些寄存器进行保存和恢复是无效的。寄存器分为受保护寄存器与不受保护寄存器,受保护的寄存器在被被调用过程使用时需要进行保存和恢复,不受保护寄存器则不一定用到。被调用过程会保存所有的会被覆盖的受保护寄存器,它们被称为被调用者保存的(在跳转到被调用过程之后被保存);而需要保护的不受保护寄存器会在调用过程中被保存起来,它们被称为调用者保存的(在跳转到被调用过程之前被保存)。
保存寄存器$s0
-$s7
,$ra
,$sp
是受保护寄存器,临时寄存器$t0
-$t9
,参数寄存器$a0
-$a3
,返回值寄存器$v0
-$v1
是不受保护寄存器。
大部分临时寄存器是在赋值前临时保存数值,赋值完成后结果保存在保存寄存器中,临时寄存器就失去了作用,所以它们是不受保护寄存器。
这里有些疑问,当我们编写MIPS汇编代码时,我们知道每个被调用过程存在栈框架中的数值对应哪个寄存器。但当我们编写高级程序语言时,通过编译产生了汇编代码。机器是如何判断这些对应关系的?
示例:
C语言代码:
int main() {
int y = 1;
y = f(1, 2) + y;
...
}
int f(int a, int b) {
int x;
y = a + b - a * b;
return y;
}
相对应的MIPS汇编代码:
# $s0 = y
addi $s0, $0, 1
addi $a0, $0, 1
addi $a1, $0, 2
jal f;
add $t0, $v0, $0
add $s0, $s0, $t0
# $s0 = x
f:
addi $sp, $sp, -4
sw $s0, 0($sp)
add $t0, $a0, $a1
mul $t1, $a0, $a1
# mul指令将后两个寄存器的乘积存进第一个寄存器
sub $s0, $t0, $t1
add $v0, $s0, $0
lw $s0, 0($sp)
addi $sp, $sp, 4
jr $ra
过程调用中的嵌套与递归
不用调用其他过程的过程叫叶子过程,调用其他过程的过程叫非叶子过程。非叶子过程往往需要额外保存不受保护寄存器中的参数寄存器与$ra
等。
递归是特殊的非叶子过程,它们会调用它们自己。