从汇编角度理解C语言(五) 原

在一个具有递归特性的编程语言中,其必须能够区别出函数定义还是函数调用。函数定义是指明此函数的动作(作用),函数调用则是使函数生成对应的执行指令或者说是让函数“活动”起来。定义只有一次,但调用可以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

通过汇编代码你估计也发现,也像注释中所说,结构体作为参数传递有时是很耗费资源的,尤其是在结构体较大时。

下一节会介绍到有关数组和指针的处理。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章