從彙編角度理解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

通過彙編代碼你估計也發現,也像註釋中所說,結構體作爲參數傳遞有時是很耗費資源的,尤其是在結構體較大時。

下一節會介紹到有關數組和指針的處理。


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