(1) 当一个程序调用另外一个程序时,需要以下6个步骤:
1 将参数放在被调程序能够获得的地方;
2 将控制权交给被调程序;
3 获取被调程序所需的存储资源;
4 执行被调程序;
5 将返回结果放在主调程序能够获得的地方;
6 控制权返还给主调程序。
(2) 寄存器分配
$a0-$a3:用于传递参数;
$v0-$v1:用于返回结果;
$ra:用于存放被调程序执行完成后需要返回的地址。
(3) jal指令
MIPS汇编语言含有一个专门用于程序调用的指令:跳转到目标程序的同时,将下一条指令的地址存放在寄存器$ra中。这条指令是jump-and-link指令。跳转时,该指令将PC+4的值存储在$ra中。
(4) 调用的基本过程
caller将参数放在$a0-$a3中,使用jal跳转到callee,callee执行任务,将结果放在$v0-$v1中,然后使用jr $ra将控制权交还给caller.
(5) 栈
当callee运行结束返回时,caller需要恢复以前自己需要的所有的寄存器的值。因此,在调用发生之前,这些寄存器的值需要保存起来,并且保存到内存中。实际中,使用栈结构来存储这些寄存器的值。栈是一个后进先出的队列,它需要一个指针指向栈中最近分配的用于存储的地址。这样当下一个程序需要保存自己的寄存器值时,它很容易就知道存在哪里,或者恢复寄存器值时知道在哪里找。MIPS使用寄存器$sp来保存栈指针。
栈在增长的时候,即往里面存东西的时候,地址逐渐减小,即从高地址往低地址前进。这就意味着,入栈时,需要对栈指针做减法;出栈时则做加法。
(6) callee需要将一些寄存器保存到内存中
callee的代码以一个标签开始,接下来,该程序需要将自身用到的一些寄存器的旧值保存起来。然后再执行要执行的任务。在返回之前callee要恢复那些用过的寄存器的旧值,最后再跳转到caller。
为了避免保存一些不必要的寄存器值,尤其是临时寄存器的值,MIPS作了下面的分组:
a) $t0-$t9:这10个临时寄存器的值在发生程序调用时是不需要保存的;
b) $s0-$s7:这8个寄存器的值需要被保存(用了哪些就保存哪些)
(7) 嵌套调用
当发生嵌套调用时,即函数调函数,caller将参数寄存器($a0-$a3)或者调用结束后需要的临时寄存器($t0-$t9)压栈;callee将返回地址寄存器$ra 和自身将用到的一些$s0-$s7保存起来。(此处有一个疑问:如果不是嵌套调用,callee还需要保存$ra的值吗)
(8) procedure frame
callee运行时需要的局部变量,如果寄存器放不下,也存放在栈中。栈中存放寄存器值和局部变量的区域称为procedure frame。一些MIPS软件使用frame pointer($fp)指向procedure frame的第一个字。这样就提供了一个固定的基址寄存器方便程序访问局部变量。
(9) heap
对于static变量和动态的数据结构,MIPS习惯按下面的方式来分配和存储。
栈从内存的高地址开始向下增长。内存的最底部是reserved,往上是程序的MIPS机器码(text segment),再往上是static data segment,存放常数和static变量,数组也存在此处,再往上是heap,存放动态变化的链表等数据结构。因此,栈和heap是互相趋进的。C语言在heap上分配和释放内存使用专门的函数:malloc()和free()。为了方便的访问staic数据,MIPS使用全局指针,global pointer,它存放在一个专门的寄存器中,叫做$gp。
(10) 如果callee需要多于4个参数怎么办
MIPS习惯的做法是将额外的参数放在栈中,并且就放在frame pointer所指位置之上。这样callee从$a0-$a3中获取四个参数,通过frame pointer获取其他的参数。