2012-05-30 53 views
4

堆棧和堆棧幀我試圖環繞的功能概念我的頭調用。這個問題是在低級別語言的背景下提出的,而不是高級別語言。在低級語言

從到目前爲止我的理解,當一個函數被調用時,局部變量和參數獲取存儲在堆棧上的堆棧幀。每個堆棧幀都與一個函數調用相關聯。我不太清楚的部分是誰負責創建框架?我的程序是否應該查看程序中的函數聲明並手動將局部變量複製到堆棧中的新幀上?

回答

3

是......

假設你有一個像C這樣的允許遞歸的語言。爲了實現這個功能,函數的每個實例必須由該函數的其他實例自包含。堆棧是一個完美的地方,因爲代碼可以在分配時「分配」和引用項目,而無需知道物理地址,它全部通過引用進行訪問。你所關心的只是在函數的上下文中跟蹤該引用,並將堆棧指針恢復到當你輸入函數時的位置。

現在你必須有一個調用約定,一個適合遞歸等。兩個流行的選擇(使用簡化模型)是寄存器傳遞和堆棧傳遞。你可以擁有並實際上將混合實際上(基於寄存器,你將用完寄存器,並且必須爲其餘參數返回堆棧)。

假設我所談論的虛擬硬件神奇地處理了返回地址而沒有與寄存器或堆棧混淆。

註冊通過。定義一組特定的硬件/處理器寄存器來保存參數,比如說r0總是第一個參數,r1是第二個參數,r2是第三個參數。並假設返回值是r0(這是簡化的)。

堆棧通過。讓我們定義您在堆棧上推送的第一件東西是最後一個參數,然後是最後一個參數。當你返回時,讓我們說返回值是堆棧中的第一件事。

爲什麼要聲明一個調用約定?這樣調用者和被調用者都知道準則是什麼以及在哪裏查找參數。表面上看起來寄存器傳遞看起來很棒,但是當你用完寄存器時,你必須把東西保存在堆棧中。當你想從一個被調用者轉到另一個函數的調用者時,你可能不得不保存調用寄存器中的項目,以免丟失這些值。你在堆棧中。

int myfun (int a, int b, int c) 
{ 
    a = a + b; 
    b+=more_fun(a,c) 
    return(a+b+c); 
} 

A,B和C通話後用於more_fun,more_fun至少需要R0和R1來傳遞參數a和c,所以你需要保存R0和R1的地方,這樣就可以1)用它們調用more_fun()和2),以便在你從more_fun()返回後不會丟失你需要的值a和b。你可以將它們保存在其他寄存器中,但是如何保護這些寄存器不被被調用的函數修改。最終,東西被保存在堆棧中,這是動態的,並通過引用而不是物理地址來訪問。所以

有人想打電話給myfun,我們正在使用寄存器寄存器。

r0 = a 
r1 = b 
r2 = c 
call myfun 
;return value in r0 

myfun: 
r0 = r0 + r1 (a = a + b) 
;save a and b so we dont lose them 
push r0 (a) 
push r1 (b) 
r0 = r0 (a) (dead code, can be optimized out) 
r1 = r2 (c) 
call more_fun 
;morefun returns something in r0 
pop r1 (recover b) 
r1 = r1 + r0 (b = b+return value) 
pop r0 (recover a) 
;r0 is used for returning a value from a function 
r0 = r0 + r1 (= a+b) 
r0 = r0 + r2 (=(a+b)+c) 
return 

調用函數(調用者)知道準備在R0,R1,R2三個參數,並採取r0中的 返回值。被調用者知道接受r0,r1,r2作爲傳入參數並返回r0,它知道當它成爲某個其他函數的調用者時,它必須保留一些東西。

如果我們使用堆棧使用我們的調用約定

int myfun (int a, int b, int c) 
{ 
    a = a + b; 
    b+=more_fun(a,c) 
    return(a+b+c); 
} 

傳遞參數現在我們必須做出一些寄存器的規則,我們定義調用規則說,1)你可以摧毀任何寄存器(但是sp和pc和psr),2)你必須保存每個寄存器,以便當你返回調用函數時從不會看到它的寄存器發生了變化,或者你是否定義了3)某些寄存器是從零開始並且可以隨意修改的必須保存,如果使用。我想說,爲了簡單起見,您可以銷燬除sp,pc和spr之外的寄存器。

我們還有一個問題需要解決。誰清理堆棧?當我打電話時,我有兩個物品在進入,並且只有返回值,清理堆棧。有兩種選擇,主叫清理,被叫清理,我隨主叫清除。這意味着被調用者必須從堆棧中以函數的方式返回,它將任何東西放在堆棧上,並且不會從堆棧中取走太多東西。

來電者:

push c 
push b 
push a 
call myfun 
pop result 
pop and discard 
pop and discard 

承擔與此硬件在當前項目的堆棧

myfun: 
;sp points at a 
load r0,[sp+0] (get a) 
load r1,[sp+1] (get b) 
add r0,r1 (a = a+b) 
store [sp+0],r0 (the new a is saved) 
;prepare call to more_fun 
load r0,[sp+2] (get c) 
load r1,[sp+0] (get a) 
push r0 (c) 
push r1 (a) 
call more_fun 
;two items on stack have to be cleaned, top is return value 
pop r0 (return value) 
pop r1 (discarded) 
;we have cleaned the stack after calling more_fun, our offsets are as 
    ;they were when we were called 
load r1,[sp+1] (get b) 
add r1,r0 (b = b + return value) 
store [sp+1],r1 
load r0,[sp+0] (get a) 
load r1,[sp+1] (get b) 
load r2,[sp+2] (get c) 
add r0,r1 (=a+b) 
add r0,r2 (=(a+b)+c) 
store [sp+0],r0 (return value) 
return 

所以我寫了這一切在飛行中可能存在的錯誤堆棧指針SP點。所有這一切的關鍵是你必須定義一個調用約定,如果每個人(調用者和被調用者)都遵循調用約定,那麼編譯就很容易。訣竅在於制定一個有效的調用約定,正如您在上面所看到的,我們必須修改約定並添加規則以使其即使對於這樣一個簡單的程序也能工作。

堆棧幀怎麼樣?

int myfun (int a, int b) 
{ 
    int c; 
    c = a + b; 
    c+=more_fun(a,b) 
    return(c); 
} 

使用基於堆棧

呼叫者

push b 
push a 
call myfun 
pop result 
pop and discard 

被叫

;at this point sp+0 = a, sp+1 = b, but we need room for c, so 
sp=sp-1 (provide space on stack for local variable c) 
;sp+0 = c 
;sp+1 = a 
;sp+2 = b 
load r0,[sp+1] (get a) 
load r1,[sp+2] (get b) 
add r0,r1 
store [sp+0],r0 (store c) 
load r0,[sp+1] (get a) 
;r1 already has b in it 
push r1 (b) 
push r0 (a) 
call more_fun 
pop r0 (return value) 
pop r1 (discarded to clean up stack) 
;stack pointer has been cleaned, as was before the call 
load r1,[sp+0] (get c) 
add r1,r0 (c = c+return value) 
store [sp+0],r1 (store c)(dead code) 
sp = sp + 1 (we have to put the stack pointer back to where 
    ;it was when we were called 
;r1 still holds c, the return value 
store [sp+0],r1 (place the return value in proper place 
    ;relative to callers stack) 
return 

被調用者,如果它使用的堆棧和堆棧指針移動到,它必須把它放回去它在 時被調用。您可以通過在堆棧中添加適當數量的內容來創建堆棧框架以進行本地存儲。你可能有局部變量,通過編譯你可能會提前知道你還必須保存一定數量的寄存器。最簡單的方法是將所有這些全部加起來,併爲整個函數移動堆棧指針一次,並在返回之前將其返回一次。您可以變得更加聰明,隨時隨地調整偏移量,繼續移動堆棧指針,編碼更難,更容易出錯。像gcc這樣的編譯器傾向於將堆棧指針移動到函數中,並在離開之前將其返回。

有些指令會在通話中向堆棧添加內容,並在返回時將其刪除,因此您必須相應地調整偏移量。同樣,圍繞對另一個函數的調用創建和清理可能需要與硬件使用堆棧相關的處理(如果有的話)。

讓你說硬件,當你進行一個調用時,將棧頂的返回值壓入。

int onefun (int a, int b) 
{ 
    return(a+b) 
} 

onefun: 
;because of the hardware 
;sp+0 return address 
;sp+1 a 
;sp+2 b 
load r0,[sp+1] (get a) 
load r1,[sp+2] (get b) 
add r1,r2 
;skipping over the hardware use of the stack we return on what will be the 
;top of stack after the hardware pops the return address 
store [sp+1],r1 (store a+b as return value) 
return (pops return address off of stack, calling function pops the other two 
    ;to clean up) 

一些處理器使用寄存器保存在一個函數被調用的返回值,有時 其註冊硬件使然,有時編譯器選擇一個,並用它作爲 約定。如果函數沒有調用任何其他函數,則可以不使用返回地址寄存器並將其用於返回,也可以在某個時刻將其推入堆棧,然後在返回之前將其彈出,然後使用它返回。如果你的函數確實調用了另一個函數,你必須保留這個返回地址,這樣對下一個函數的調用不會破壞它,並且你無法找到回家的路。所以你要麼將它保存在另一個寄存器,如果你可以或把它放在堆棧

使用我們定義的上述寄存器調用約定,再加上一個名爲rx的寄存器,當進行調用時硬件將返回地址放在rx爲你。

int myfun (int a, int b) 
{ 
    return(some_fun(a+b)); 
} 

myfun: 
;rx = return address 
;r0 = a, first parameter 
;r1 = b, second parameter 
push rx ; we are going to make another call we have to save the return 
     ; from myfun 
;since we dont need a or b after the call to some_fun we can destroy them. 
add r0,r1 (r0 = a+b) 
;we are all ready to call some_fun first parameter is set, rx is saved 
;so the call can destroy it 
call some_fun 
;r0 is the return from some_fun and is going to be the return from myfun, 
;so we dont have to do anything it is ready 
pop rx ; get our return address back, stack is now where we found it 
     ; one push, one pop 
mov pc,rx ; return 
+0

感謝您的詳細解釋。 –

1

通常是一個處理器供應商或第一家開發通俗的語言編譯器的處理器將定義調用一個函數之前,函數調用方應該做的(應該是在棧上是什麼,應該包含的各種寄存器等)以及被調用的函數在返回之前應該做些什麼(包括恢復某些寄存器的值,如果它們已被更改等)。對於一些處理器來說,多種約定已經變得流行,確保任何給定函數的代碼將使用調用代碼所期望的約定通常非常重要。

在8088/8086上,寄存器數量有點少,出現了兩個主要約定:C約定,它指定調用者在調用函數之前應將參數推入堆棧,然後將其彈出(意爲被調用函數應該從堆棧中彈出的唯一東西是返回地址)和Pascal約定,它指定被調用的函數除彈出返回地址外,還應彈出所有傳入的參數。在8086,PASCAL習慣通常允許稍小的代碼(因爲堆清理只需要一次爲每個調用函數一次爲每個函數調用發生,而不是,因爲8086包含版本RET的增加了一個規定值堆棧指針彈出的返回地址。PASCAL習慣的一個缺點是,它需要被調用函數知道有多少字節的價值的參數將被通過。如果被調用的函數沒有彈出完全在很多較新的處理器上,具有少量固定參數數量的例程通常沒有將其參數壓入堆棧,而是由編譯器供應商指定前幾個參數將被放入reg調用函數之前的isters。這通常會比使用基於堆棧的參數獲得更好的性能。然而,具有許多參數或可變參數列表的例程仍然必須使用堆棧。

0

要擴大supercat的回答了一下,設置棧幀主叫和被叫功能的共同責任。堆棧幀通常是指對例程的特定調用而言本地的所有數據。然後,調用例程通過首先將任何基於棧的參數推入堆棧,然後通過調用例程來返回返回地址,從而構建外部堆棧框架。被調用的例程然後通過(通常)將當前幀指針推入(保存)在堆棧上,並建立一個指向下一個空閒棧槽的新框架來構建堆棧幀(內部堆棧幀)的其餘部分。然後它爲堆棧中的局部變量保留堆棧,並且根據所使用的語言,也可能在此時初始化它們。然後可以使用幀指針訪問基於堆棧的參數和局部變量,其中一個具有負值,另一個具有正值偏移量。在退出例程時,舊的堆棧幀將被恢復,本地數據和參數將被彈出,如supercat所概述。