程序的執(zhí)行過程可看作連續(xù)的函數(shù)調(diào)用。當(dāng)一個函數(shù)執(zhí)行完畢時,程序要回到調(diào)用指令的下一條指令(緊接call指令)處繼續(xù)執(zhí)行。函數(shù)調(diào)用過程通常使用堆棧實現(xiàn),每個用戶態(tài)進(jìn)程對應(yīng)一個調(diào)用棧結(jié)構(gòu)(call stack)。編譯器使用堆棧傳遞函數(shù)參數(shù)、保存返回地址、臨時保存寄存器原有值(即函數(shù)調(diào)用的上下文)以備恢復(fù)以及存儲本地局部變量。
不同處理器和編譯器的堆棧布局、函數(shù)調(diào)用方法都可能不同,但堆棧的基本概念是一樣的。
1 寄存器
寄存器是處理器加工數(shù)據(jù)或運(yùn)行程序的重要載體,用于存放程序執(zhí)行中用到的數(shù)據(jù)和指令。因此函數(shù)調(diào)用棧的實現(xiàn)與處理器寄存器組密切相關(guān)。
在函數(shù)調(diào)用過程中, 除通用寄存器外, 還使用了三個特殊的寄存器
- IP(Instruction Pointer)是指令寄存器, 指向處理器下條等待執(zhí)行的指令地址(代碼段內(nèi)的偏移量),每次執(zhí)行完相應(yīng)匯編指令I(lǐng)P值就會增加
- SP(Stack Pointer)是堆棧指針寄存器,存放執(zhí)行函數(shù)對應(yīng)棧幀的棧頂?shù)刂?也是系統(tǒng)棧的頂部),且始終指向棧頂
- BP(Base Pointer)是棧幀基址指針寄存器,存放執(zhí)行函數(shù)對應(yīng)棧幀的棧底地址,用于C運(yùn)行庫訪問棧中的局部變量和參數(shù)。
不同架構(gòu)的CPU,寄存器名稱被添加不同前綴以指示寄存器的大小。例如x86架構(gòu)用字母“e(extended)”作名稱前綴,指示寄存器大小為32位;x86_64架構(gòu)用字母“r”作名稱前綴,指示各寄存器大小為64位。
2 棧幀
函數(shù)調(diào)用經(jīng)常是嵌套的,在同一時刻,堆棧中會有多個函數(shù)的信息。每個未完成運(yùn)行的函數(shù)占用一個獨(dú)立的連續(xù)區(qū)域,稱作棧幀(Stack Frame)。棧幀是堆棧的邏輯片段,當(dāng)調(diào)用函數(shù)時邏輯棧幀被壓入堆棧, 當(dāng)函數(shù)返回時邏輯棧幀被從堆棧中彈出。棧幀存放著函數(shù)參數(shù),局部變量及恢復(fù)前一棧幀所需要的數(shù)據(jù)等。
編譯器利用棧幀,使得函數(shù)參數(shù)和函數(shù)中局部變量的分配與釋放對程序員透明。編譯器將控制權(quán)移交函數(shù)本身之前,插入特定代碼將函數(shù)參數(shù)壓入棧幀中,并分配足夠的內(nèi)存空間用于存放函數(shù)中的局部變量。使用棧幀的一個好處是使得遞歸變?yōu)榭赡?,因為對函?shù)的每次遞歸調(diào)用,都會分配給該函數(shù)一個新的棧幀,這樣就巧妙地隔離當(dāng)前調(diào)用與上次調(diào)用。
棧幀是一個邏輯概念, 可以認(rèn)為一個函數(shù)所占用的空間就是一個棧幀(包括 BP, SP 局部變量, 下個函數(shù)參數(shù) 返回地址 IP....), 其中BP就指向當(dāng)前函數(shù)棧幀底部, SP永遠(yuǎn)指向棧幀頂部(也是整個調(diào)用棧的頂部);
當(dāng)程序執(zhí)行時SP會隨著數(shù)據(jù)的入棧和出棧而移動。因此函數(shù)中對大部分?jǐn)?shù)據(jù)的訪問都基于BP進(jìn)行。

// 偽代碼
void test(int a, int b){
int c = 12;
int d = 13;
}
int main(int argc, const char * argv[]) {
int a = 10;
int b = 11;
test(a, b);
printf("下一條指令")
return 0;
}
從圖中可以看出,函數(shù)調(diào)用時入棧順序為
實參N-1→主調(diào)函數(shù)返回地址→主調(diào)函數(shù)幀基指針EBP→被調(diào)函數(shù)局部變量1-n
- 主調(diào)函數(shù)將參數(shù)按照調(diào)用約定依次入棧(C/C++ 中參數(shù)由右向左一次入棧, 這樣出棧是就能保持參數(shù)傳遞的順序)
- 將指令指針EIP入棧以保存主調(diào)函數(shù)的返回地址(下一條待執(zhí)行指令的地址, 也就是
printf函數(shù)的地址)。 - 此時進(jìn)入
test執(zhí)行指令, 先將主調(diào)函數(shù)(main)的棧幀基指針EBP入棧 - 將主調(diào)函數(shù)(
main)函數(shù)的棧頂指針ESP值賦給被調(diào)函數(shù)的EBP(作為被調(diào)函數(shù)的棧底) - 改變ESP值來為函數(shù)局部變量預(yù)留空間
此時函數(shù)空間已調(diào)整完畢
此時被調(diào)函數(shù)幀基指針(EBP)指向被調(diào)函數(shù)的棧底。以該地址為基準(zhǔn),向上(棧底方向)可獲取主調(diào)函數(shù)的返回地址、參數(shù)值,向下(棧頂方向)能獲取被調(diào)函數(shù)的局部變量值,而該地址處又存放著上一層主調(diào)函數(shù)的幀基指針值。
函數(shù)調(diào)用完成之后
- 將EBP指針值賦給ESP,使ESP再次指向被調(diào)函數(shù)棧底以釋放局部變量;(其實數(shù)據(jù)還在內(nèi)存中, 不過調(diào)整指針之后, 已經(jīng)變成不安全空間)
- 再將已壓棧的主調(diào)函數(shù)幀基指針彈出到EBP
- 彈出返回地址到EIP
- ESP繼續(xù)上移越過參數(shù),最終回到函數(shù)調(diào)用前的狀態(tài),即恢復(fù)原來主調(diào)函數(shù)的棧幀。如此遞歸便形成函數(shù)調(diào)用棧
EBP指針在當(dāng)前函數(shù)運(yùn)行過程中(未調(diào)用其他函數(shù)時)保持不變。在函數(shù)調(diào)用前,ESP指針指向棧頂?shù)刂?,也是棧底地址。在函?shù)完成現(xiàn)場保護(hù)之類的初始化工作后,ESP會始終指向當(dāng)前函數(shù)棧幀的棧頂,此時,若當(dāng)前函數(shù)又調(diào)用另一個函數(shù),則會將此時的EBP視為舊EBP壓棧,而與新調(diào)用函數(shù)有關(guān)的內(nèi)容會從當(dāng)前ESP所指向位置開始壓棧。
3 iOS中的函數(shù)調(diào)用棧
iPhone 5s以上設(shè)備都是用了arm64 (64位的CPU架構(gòu)), 而ARM64 有34個寄存器,包括31個通用寄存器, 所以當(dāng)參數(shù)少的情況下, 用寄存器就可以完成函數(shù)參數(shù)的傳遞, 所以棧幀結(jié)構(gòu)與上圖有所區(qū)別.
void test(int a, int b){
int c = 12;
int d = 13;
printf("%d, %d", a, b);
}
int main(int argc, const char * argv[]) {
int a = 10;
int b = 11;
test(a, b);
printf("下一條指令");
return 0;
}
下圖是根據(jù)上面代碼在Xcode中實際生成的匯編代碼


這里注意,如果用模擬器調(diào)試,使用的是mac電腦的CPU架構(gòu),只有使用真機(jī)調(diào)試,才能看到arm64的匯編代碼
ARM64開始,取消32位的 LDM,STM,PUSH,POP指令! 取而代之的是ldr\ldp str\stp
ARM64里面 對棧的操作是16字節(jié)對齊的!!
在ARM64中,x0-x7寄存器,用來存放參數(shù)、返回值(x0)
入棧順序:
- 分別將兩個參數(shù)賦值給
w0,w1寄存器 - bl指令 調(diào)用函數(shù), 此時會將下一條指令賦值給
LR寄存器(也就是printf) - 進(jìn)入
test中-
sub sp, sp, #0x40提升??臻gsp棧頂指針 -
stp x29, x30, [sp, #0x30]將x29, x30放入棧中,x29(FP):棧底指針x30(LR): 子程序結(jié)束后需要執(zhí)行的下一條指令 -
add x29, sp, #0x30將當(dāng)前函數(shù)棧幀基地址存儲到x29(FP)中
-
- 此時函數(shù)入棧完成開始執(zhí)行業(yè)務(wù)代碼.......
出棧順序:
-
ldp x29, x30, [sp, #0x30]將之前保存在棧中的x29,x30數(shù)據(jù),重新復(fù)制給x29,x30寄存器,相當(dāng)于將x29 x30恢復(fù)到調(diào)用函數(shù)之前的狀態(tài)。 -
add sp, sp, #0x40恢復(fù)SP指針, 抵消入棧時的 提升??臻g的操作。 相當(dāng)于回收函數(shù)調(diào)用開辟的??臻g -
ret返回指令,將LR中存儲的值賦值給PC,結(jié)束子程序,回到函數(shù)調(diào)用前的狀態(tài)繼續(xù)執(zhí)行。