概要
本篇博客主要包括兩個方面的內容:
- 整理棧涉及到的一些基本概念、ARM架構下棧相關的操作指令;
- 分析一個函數調用實例。
* 棧的基本知識
棧的概念
- 棧
首先,棧是一種先進后出(FILO)的數據結構,棧底是第一個進棧數據所在的位置,棧頂是最后一個進棧數據所在的位置。
其次,棧也是內存中的一段特殊空間,用於存放函數參數、函數上下文(寄存器)、函數返回地址、局部變量等。
Ps. 返回值一般是在R0寄存器中返回,不使用棧。
-
棧幀
系統在運行過程中,會為每個進程分配一個棧空間(互不干擾),而進程中的每個函數在被調用時會分配棧上的一塊連續的空間區域,這個空間區域就是棧幀,函數在執行過程中可以隨意使用屬於自己棧幀上的空間,當函數返回時,棧幀空間將被收回,后續可能會分配給其他函數使用。每個函數的棧幀使用棧頂指針和棧底指針來界定。如下圖所示:

-
棧的相關寄存器和指針
| 寄存器 | 名稱 | 功能 |
|---|---|---|
| R11 | frame pointer,FP寄存器 | 用於保存FP指針,即棧底指針 |
| R12 | IP寄存器 | 用於暫存SP,即棧頂 |
| R13 | stack pointer,SP寄存器 | 用於保存SP指針,即棧頂指針 |
| R14 | link register,LR寄存器 | 用於保存函數的返回地址 |
| R15 | PC寄存器 | 用於保存程序計數器的值 |
棧的分類
-
滿/空棧【Full/Empty】
根據SP指針指向的位置,棧可分為滿棧和空棧,如下圖:

SP指向最后入棧的數據時為滿棧(左圖);SP指向下一個將要放入數據的空位置時為空棧(右圖)。形象的理解為,SP指針指向最后一個數據,這個棧沒有空位置,那就是滿的;而當SP指針指向一個空位置,那么這個棧就不滿,就是空的。
-
升/降棧【Ascending/Descending】
根據SP指針的移動方向,棧可以分為升棧和降棧,如下圖:

當數據入棧時,SP指針從低地址向高地址移動(即棧的生長方向為Low->High),此為升棧(左圖);相反的,SP指針從高地址向低地址移動(即棧的生長方向為High->Low),此為降棧(右圖)。
-
棧的四種情況
綜上,兩兩結合可以得到四種類型的棧,即:
| 滿降棧FD | 滿升棧FA | 空降棧ED | 空升棧EA |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Ps. ARM為滿降棧。
棧相關指令(存取指令)
##以32位ARM為例,ARM64中棧的指令有所改變##
首先需要明確的一點是,存取指令有很多,但實際的使用需要結合棧的種類。如果是滿棧的話,肯定是需要先改變SP指針再壓棧;出棧則相反,需要先出棧,再改變SP指針。而如果是空棧,壓棧時先壓入數據,再改變SP指針;出棧時,先移動SP指針,再彈出數據。
批量存取指令為:LDM(取)和STM(存),即load multiple & store multiple。
結合四種類型的棧,那么就會存在以下四對存取指令:
| 😃 | 滿降棧 | 滿升棧 | 空降棧 | 空升棧 |
|---|---|---|---|---|
| 批量存 | STMFD | STMFA | STMED | STMEA |
| 批量取 | LDMFD | LDMFA | LDMED | LDMEA |
ARM是滿降棧,因此使用STMFD和LDMFD指令。
STMFD指令即向棧中壓入多個數據,采用事先遞減方式(DB,before decrease),先將SP指針減小,再壓入數據;
LDMFD指令即彈出棧中的多個數據,采用事后遞增方式(IA,after increase),先彈出數據,再將SP指針增大。
其他指令類似,在這里不做過多討論。重點是要理解不同類型棧的壓棧和出棧規律。
Ps. 此外還有事后遞減(DA)和事先遞增(IB)方式。
* 函數調用實例分析棧的變化
下面看一個實例,如下圖,main函數調用test_b函數,在test_b函數中首先是函數序言(Prologue,一般用於在棧中保存一些環境變量等),然后是test_b函數的函數體(完成相應功能),最后是函數結語(Epilogue,一般用於恢復棧中保存的變量,准備好返回main函數的環境)。

函數序言
執行STMFD指令,向棧中壓入R11和LR,即main函數的FP指針和返回地址,確保test_b函數執行完畢后可以正確返回main函數中的相應位置繼續執行后續指令以及可以正確恢復main函數的棧幀。隨着數據的壓入,SP = SP - 8。
執行ADD指令,更新當前的棧底指針(R11寄存器,FP指針),指向test_b函數棧幀的棧底。
執行SUB指令,抬高棧頂,為test_b函數創建棧幀,后續代碼的執行可以用到這塊棧空間。
以上過程示意圖如下:

函數結語
執行SUB指令,更新SP = FP - 4,即指向保存的R11值。
執行LDMFD指令,彈出之前保存的main函數棧底指針,以及返回地址賦值給PC寄存器,這樣的話main函數的執行流和棧幀全部恢復了,然后SP = SP + 8。
以上過程示意圖如下:

參考鏈接
- ARM匯編之棧與函數 函數調用棧示例分析
- ARM-棧 棧的基本概念和作用
- ARM下C語言棧幀機制
- [arm-匯編stmdb、ldmia、stmfd、ldmfd]




