在一个具有递归特性的编程语言中,其必须能够区别出函数定义还是函数调用。函数定义是指明此函数的动作(作用),函数调用则是使函数生成对应的执行指令或者说是让函数“活动”起来。定义只有一次,但调用可以N很多次;
每一条指令都有其对内存的要求, 在函数的调用及返回活动期间,在内存中需要包含:
1、自身存储空间(local storage)用来存储参数、局部变量、其它一些辅助空间及执行代码;
2、返回信息,用来确定函数调用完成后返回地址及如何执行;
这部分内存空间只在函数运行期间有效,一旦函数运行结束,此部分内存空间就将被收回。
举例说明其在内存中记录的样子
如代码
void Simple(int a, int b) // two value params
{
int temp1, temp2; // temporary local variable
....
通过上面的函数声明,编译器就能够确定其空间内需要存放4个整数类型值,a、b、temp1、temp2。假设每个整型值占据4个字节,则此记录至少需要16个字节的空间。但编译器并不会立刻为其分配存储空间,而是在函数调用时分配。
其分配会像如下形式的压栈操作:
从图可看出其会把参数先压栈,之后才是函数返回地址,之后是一些变量等。并且你会发现,此变量和参数就像两个相邻的结构体。SP指向变量基地址,紧随其后的是函数返回地址,然后是参数。就像如下代码:
struct SimpleActivationLocal {
int temp2; // offset 0 (from SP)
int temp1; // offset 4
};
struct SimpleActivationParameters {
int a; // offset 12 (from SP)
int b; // offset 16
};
为了能够支持函数调用,我们需要再加入两条汇编指令。
Call指令,和Jmp类似,但他会开辟一个空间用来保存函数返回时下一条指令执行的地址
用法:
Jumps to strlen & saves the next instruction's address on stack
Call <strlen>
Ret
指令通常会在函数体生成汇编代码最后一行,用来返回函数返回值的。在函数返回时会把返回值保存到 RV寄存器中,改变PC的值为函数返回后一下条指令地址(保存在SP寄存器中),并开始执行。
Returns control to saved instruction address stored on stack
Ret
下面首先来个简单的例程
例1
static int Add(int one, int two)
{
int temp, sum;
sum = one + two;
return sum;
}
分析如上代码,其需要20字节的存储空间,8字节保存参数,4字节保存返回地址,再8字节保存局部变量值。如下
翻译成汇编代码如下
SP = SP - 8 ; make space for local variables
R1 = M[SP + 12] ; load value of parameter one into R1
R2 = M[SP + 16] ; load value of parameter two into R2
R3 = R1 + R2 ; do addition
M[SP] = R3 ; store result in local variable sum
RV = M[SP] ; copy sum's value into RV register (return value)
SP = SP + 8 ; clean up space used for local variables
RET ; return to caller, pick up at saved address
另外要说明的是,认真读代码你会发现,其中函数内部的变量都是不需要的,对于有的聪明的编译器,其如上生成的汇编可如下
; eliminate all local variables, no change to SP
R1 = M[SP + 4] ; load value of parameter one into R1
R2 = M[SP + 8] ; load value of parameter two into R2
RV = R1 + R2 ; compute sum directly into RV register
RET ; no need to clean up locals, there aren't any!
例2
struct fraction {
int numerator;
int denominator;
};
static void Binky(struct fraction param)
{
struct fraction local;
local.denominator = 1;
param.denominator = 2;
}
其生成的汇编如下:
SP = SP - 8 ; make space for local variable
M[SP + 4] = 1 ; set local.denominator = 1
M[SP + 16] = 2 ; set param.denominator = 2
SP = SP + 8 ; clean up locals
RET ; return to caller, no return value stored
可以看出其与例1有很多相似之处。
再举一个关于函数调用的例子
例3
static void Caller(void)
{
int num = 10;
num = Add(num, 45);
num = 100;
}
其函数调用例1的函数。
生成的汇编代码如下
SP = SP - 4 ; make space for local variable num
M[SP] = 10 ; assign local variable num constant value 10
R1 = M[SP] ; load up the value of num (before we change SP
; so we don't have to deal with changed offsets)
SP = SP - 8 ; push space for parameter block of Add's AR
M[SP + 4] = 45 ; initialize parameters in the activation record
M[SP] = R1
CALL <Add> ; the CALL instruction makes space on the stack
; for the return address, saves the PC value there
; and then assigns the PC to the address of the first
; instruction of the Add fn (which transfers control)
SP = SP + 8 ; when control returns here, pop the params off stack
M[SP] = RV ; read return value from RV and store in num
M[SP] = 100 ; assign num constant value 100
SP = SP + 4 ; clean up storage for locals
RET ; return, no value stored in RV since fn has no return
从汇编代码可以看出,其为调用函数参数分配的空间,但并未给被调用函数本身分配空间,因为此时我们并不知道要为他分配多少空间,想想当你调用strlen函数时,你知道为他分配多少空间吗。
所以被调用函数分配空间需要由他自身完成。
对于结构体参数的传递呢?
例4
static void Caller(void)
{
struct fraction actual;
Binky(actual);
}
其汇编代码如下
SP = SP - 8 ; make space for local variable (left uninitialized)
R1 = M[SP] ; store the value of the two fields of the struct "actual"
R2 = M[SP + 4] ; (before we change the SP which will make things messy)
SP = SP - 8 ; push space for the parameter block of Binky's AR
M[SP + 4] = R2 ; initialize the parameter in the AR. This means
M[SP] = R1 ; copying both fields. For a small struct, it is possible
; to store the fields temporarily in register(s) and then
; copy to stack in steps. However, for larger structures,
; it will be necessarily to store the address of the
; structure in a register and then use a specialized
; copy function (something like memcpy) to copy its
; contents to the stack. Something similar is done when
; returning a struct as the return value.
Call <Binky>
SP = SP + 8 ; clean up parameters
SP = SP + 8 ; clean up locals
RET ; return to caller, no return value stored
通过汇编代码你估计也发现,也像注释中所说,结构体作为参数传递有时是很耗费资源的,尤其是在结构体较大时。
下一节会介绍到有关数组和指针的处理。