天天看點

MIPS 架構上函數調用過程中的堆棧和棧幀

   在計算機科學中,Call stack 是指存放某個程式的正在運作的函數的資訊的棧。Call stack 由 stack frames 組成,每個 stack frame 對應于一個未完成運作的函數。

   在當今流行的計算機體系架構中,大部分計算機的參數傳遞,局部變量的配置設定和釋放都是通過操縱程式棧來實作的。棧用來傳遞函數參數,存儲傳回值資訊,儲存寄存器以供恢複調用前處理機狀态。每次調用一個函數,都要為該次調用的函數執行個體配置設定棧空間。為單個函數配置設定的那部分棧空間就叫做 stack frame,也就是說,stack frame 這個說法主要是為了描述函數調用關系的。

Stack frame 組織方式的重要性和作用展現在兩個方面:

  第一,它使調用者和被調用者達成某種約定。這個約定定義了函數調用時函數參數的傳遞方式,函數傳回值的傳回方式,寄存器如何在調用者和被調用者之間進行共享;

   第二,它定義了被調用者如何使用它自己的 stack frame 來完成局部變量的存儲和使用。

MIPS 架構上函數調用過程中的堆棧和棧幀

    上圖描述的是一種典型的(MIPS O32)嵌入式晶片的 stack frame 組織方式。在這張圖中,計算機的棧空間采用的是向下增長的方式,SP(stack pointer) 就是目前函數的棧指針,它指向的是棧底的位置。Current Frame 所示即為目前函數(被調用者)的 frame ,Caller’s Frame 是目前函數的調用者的 frame 。每個 frame 中所存放的内容和存放順序,則由目标體系架構的調用約定(calling convention)定義。如圖所示,MIPS O32調用約定規定了所占空間不大于4 個比特的參數應該放在從 $4到 $8 的寄存器中,剩下的參數應該依次放到調用者 stack frame 的參數域中,并且在參數域中需要為前四個參數保留棧空間;如果被調用者需要使用 $16 到 $23 這些保留寄存器(saved register),就必須先将這些保留寄存器的值儲存在被調用者 stack frame 的保留寄存器域中,當被調用者傳回時恢複這些寄存器值;當被調用者不是葉子函數時,即被調用者中存在對其它函數的調用,需要将 RA(return address) 寄存器 ($31) 值儲存到被調用者 stack frame 的傳回值域中;被調用者所需要使用的局部變量,應儲存在被調用者 stack frame 的本地變量域中。

    在沒有 BP(base pointer) 寄存器的目标架構中,進入一個函數時需要将目前棧指針向下移動 n 比特,這個大小為n比特的存儲空間就是此函數的 stack frame 的存儲區域。此後棧指針便不再移動,隻能在函數傳回時再将棧指針加上這個偏移量恢複棧現場。由于不能随便移動棧指針,是以寄存器壓棧和出棧都必須指定偏移量,這與 x86 架構的計算機對棧的使用方式有着明顯的不同。

    在 RISC 計算機中主要參與計算的是寄存器,saved registers 就是指在進入一個函數後,如果某個儲存原函數資訊的寄存器會在目前函數中被使用,就應該将此寄存器儲存到堆棧上,當函數傳回時恢複此寄存器值。而且由于 RISC 計算機大部分采用定長指令或者定變長指令,一般指令長度不會超過32個位。而現代計算機的記憶體位址範圍已經擴充到 32 位,這樣在一條指令裡就不足以包含有效的記憶體位址,是以RISC計算機一般借助于一個傳回位址寄存器 RA(return address) 來實作函數的傳回。幾乎在每個函數調用中都會使用到這個寄存器,是以在很多情況下 RA 寄存器會被儲存在堆棧上以避免被後面的函數調用修改,當函數需要傳回時,從堆棧上取回 RA 然後跳轉。移動 SP 和儲存寄存器的動作一般處在函數的開頭,叫做 function prologue;恢複這些寄存器狀态的動作一般放在函數的最後,叫做 function epilogue。