函数调用的汇编实现
本节课我们会来看一下真正的活动记录,之前举的例子往往都是没有包含参数的函数,因此本节课我们会来看一些带有参数的复杂函数。
首先, 我们先看一下c代码和其对应的汇编指令
void foo(int bar, int *baz)
{
char snink[4];
short *why;
why = (short*)(snink + 2);
*why = 50;
}
int main(int argc, char **argv)
{
int i = 4;
foo(i, &i);
return 0;
}
foo:
SP = SP - 8
R1 = SP + 6
M[SP] = R1
R1 = M[SP]
M[R1] = .2 50
SP = SP + 8
RET
SP = SP - 4
M[SP] = 4
SP = SP - 8
R1 = M[SP + 8]
R2 = SP + 8
M[SP] = R1
M[SP + 4] = R2
CALL <foo>
SP = SP + 8
RV = 0
刚开始看可能会一脸懵逼, 现在我们逐条来进行分析:
void foo(int bar, int *baz)
{
char snink[4];
short *why;
....
}
内存中的活动记录如下所示:
注意课程所使用的为32位机器。
函数参数0永远在参数1的下方,以此类推。在参数和局部变量中间的内存区域中,存储着调用函数的信息,以便告诉我们到底哪块代码调用了foo,因此这里依赖着被保存的PC值,以便后续可以继续执行接下来的指令。
目前, 大家可能会对内存中变量的布局感到奇怪, 现在我们从main函数开始, 分析如何这种布局是如何产生的。
int main(int argc, char **argv)
{
int i = 4;
foo(i, &i);
return 0;
}
main函数在调用时, 已经具有以下部分, 至于为什么会产生这部分内容, 在foo函数调用时会进行介绍的。
根据函数调用原则, 我们首先需要为局部变量分配对应的内存空间。
int i = 4;
对应的汇编指令如下所示:
SP = SP - 4;
M[SP] = 4;
这里使用SP表示栈指针,减4是因为栈空间是从高地址到低地址进行分配。
接下来需要为调用foo做准备, 首先我们需要为参数预留空间, 即
SP = SP - 8
R1 = M[SP + 8]
R2 = SP + 8
M[SP] = R1
M[SP + 4] = R2;
之后, 通过以下指令, 将代码的控制权交给foo函数
CALL <foo>
这是一条简单的跳转指令, 注意我们同时也需要保证在跳转之后可以在跳回当前位置, 以确保之后的指令可以顺利执行。
我们来看一下目前到目前为止所有的汇编指令:
SP = SP - 4
M[SP] = 4
SP = SP - 8
R1 = M[SP + 8]
R2 = SP + 8
M[SP] = R1
M[SP + 4] = R2;
CALL <foo>
SP = SP + 8
其中, SP = SP + 8
指令是foo函数执行之后需要执行的指令, 其地址被保存在foo函数的saved pc中, 这是当CALL指令执行时,自动触发的。
现在将foo函数补充完整
void foo(int bar, int *baz)
{
char snink[4];
short *why;
why = (short*)(snink + 2);
*why = 50;
}
首先, 我们需要为局部变量分配相应的空间(这是在编译时就可以确定的), 即
foo: SP = SP - 8
SP = SP - 8
R1 = SP + 6
M[SP] = R1
R1 = M[SP]
M[R1] = .2 50
之后, 我们执行如下指令, 这是因为在函数调用结束之后, 我们需要回收分配的局部空间
SP = SP + 8
这时SP指向了saved pc, 因此最后一条指令就是RET指令。该指令会将saved pc中的值取出来放入到PC寄存器中, 并将SP = SP + 4。
在执行完上述操作之后, 内存中的活动记录变成如下形式。
现在PC指向CALL指令之后的指令, 来回收为参数变量分配的内存区域, 即
PC = PC + 8
最后一条指令是
RV = 0
RV是一个4字节的寄存器, 主要是用来在调用者和被调用函数之间传递返回值。
现在, 我们对这部分内容简单总结一下, 当我们在函数调用的过程中, 我们需要在内存中建立起以下部分。
从图中可以看出, 在函数调用的过程中, 内存分配分成两部分, 参数和saved pc是由用户分配并初始化, 而函数中的局部变量是由函数本身进行分配和初始化。
接下来, 我们再来看看递归函数中, call和ret指令的使用。
int fact(int n)
{
if(n == 0)
return 1;
return n * fact(n - 1);
}
fact:
R1 = M[SP + 4]
BNE R1, 0, PC + 12
RV = 1
RET
R1 = M[SP + 4]
R1 = R1 - 1
SP = SP - 4
M[SP] = R1
CALL <fact>
SP = SP + 4
R1 = M[SP + 4]
RV = RV * R1
RET
注意, 在真实系统中pc是指向下一条指令的, 但是在课程中, 假设pc指向当前指令。