函數(shù)調(diào)用棧

程序的執(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)行。

函數(shù)調(diào)用棧的典型內(nèi)存布局
// 偽代碼
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

  1. 主調(diào)函數(shù)將參數(shù)按照調(diào)用約定依次入棧(C/C++ 中參數(shù)由右向左一次入棧, 這樣出棧是就能保持參數(shù)傳遞的順序)
  2. 將指令指針EIP入棧以保存主調(diào)函數(shù)的返回地址(下一條待執(zhí)行指令的地址, 也就是printf函數(shù)的地址)。
  3. 此時進(jìn)入test執(zhí)行指令, 先將主調(diào)函數(shù)(main)的棧幀基指針EBP入棧
  4. 將主調(diào)函數(shù)(main)函數(shù)的棧頂指針ESP值賦給被調(diào)函數(shù)的EBP(作為被調(diào)函數(shù)的棧底)
  5. 改變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)用完成之后

  1. 將EBP指針值賦給ESP,使ESP再次指向被調(diào)函數(shù)棧底以釋放局部變量;(其實數(shù)據(jù)還在內(nèi)存中, 不過調(diào)整指針之后, 已經(jīng)變成不安全空間)
  2. 再將已壓棧的主調(diào)函數(shù)幀基指針彈出到EBP
  3. 彈出返回地址到EIP
  4. 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中實際生成的匯編代碼


main 函數(shù)匯編指令

test 函數(shù)匯編指令

這里注意,如果用模擬器調(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)

入棧順序:

  1. 分別將兩個參數(shù)賦值給w0,w1寄存器
  2. bl指令 調(diào)用函數(shù), 此時會將下一條指令賦值給LR寄存器(也就是printf)
  3. 進(jìn)入test
    • sub sp, sp, #0x40 提升??臻g sp 棧頂指針
    • stp x29, x30, [sp, #0x30]x29, x30放入棧中, x29(FP):棧底指針 x30(LR): 子程序結(jié)束后需要執(zhí)行的下一條指令
    • add x29, sp, #0x30 將當(dāng)前函數(shù)棧幀基地址存儲到x29(FP)
  4. 此時函數(shù)入棧完成開始執(zhí)行業(yè)務(wù)代碼.......

出棧順序:

  1. ldp x29, x30, [sp, #0x30] 將之前保存在棧中的x29,x30數(shù)據(jù),重新復(fù)制給x29,x30寄存器,相當(dāng)于將x29 x30恢復(fù)到調(diào)用函數(shù)之前的狀態(tài)。
  2. add sp, sp, #0x40 恢復(fù)SP指針, 抵消入棧時的 提升??臻g的操作。 相當(dāng)于回收函數(shù)調(diào)用開辟的??臻g
  3. ret 返回指令,將LR中存儲的值賦值給PC,結(jié)束子程序,回到函數(shù)調(diào)用前的狀態(tài)繼續(xù)執(zhí)行。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文地址:C語言函數(shù)調(diào)用棧(一)C語言函數(shù)調(diào)用棧(二) 0 引言 程序的執(zhí)行過程可看作連續(xù)的函數(shù)調(diào)用。當(dāng)一個函數(shù)執(zhí)...
    小豬啊嗚閱讀 4,971評論 1 19
  • 棧: 在函數(shù)調(diào)用時,第一個進(jìn)棧的是主函數(shù)中函數(shù)調(diào)用后的下一條指令(函數(shù)調(diào)用語句的下一條可執(zhí)行語句)的地址,然后是函...
    zjfclimin閱讀 4,302評論 0 5
  • 閱讀經(jīng)典——《深入理解計算機(jī)系統(tǒng)》04 函數(shù)調(diào)用時的棧結(jié)構(gòu)變化是一個很有趣的話題,本文就來詳細(xì)剖析這個過程。 棧幀...
    金戈大王閱讀 23,547評論 14 36
  • 由于不是科班出生,又是自學(xué)開發(fā),對很多方面的知識都是只知其然而不知其所以然。加上最近公司事情不多,剛好乘此機(jī)會把長...
    寒咯閱讀 13,448評論 3 8
  • 首先先看圖: 在main函數(shù)調(diào)用func_A的時候,首先在自己的棧幀中壓入函數(shù)返回地址,然后為func_A創(chuàng)建新棧...
    zjfclimin閱讀 7,546評論 1 2

友情鏈接更多精彩內(nèi)容