Stories

Detail Return Return

C/C++ 之棧幀運作:函數調用的底層密碼 - Stories Detail

寫代碼時隨手寫下的函數調用,背後藏着一套計算機嚴格遵守的"操作手冊"。為什麼參數傳遞要"倒着來"?棧幀是如何"搭起來"又"拆乾淨"的?今天就用32位程序的實例,帶你透過彙編指令看清函數調用的底層邏輯。

一、從一段加法代碼説起

先看這段再普通不過的代碼:

int add_func(int a, int b) {
    int sum = 0;
    sum = a + b;
    return sum;
}

int main() {
    int a = 5, b = 6, sum = 0;
    sum = add_func(a, b); // 核心調用過程
    return 0;
}

就是main調用add_func這一行,背後藏着參數傳遞、棧幀切換、返回值傳遞等一系列操作。我們用調試器一步步拆解,看看計算機是如何"按部就班"完成這個過程的。

二、參數傳遞:為什麼先壓6再壓5?

打開調試器查看main函數的彙編,會發現調用add_func前有四句關鍵指令:

mov eax, dword ptr [ebp-0x14]  ; 從ebp-0x14取b的值6
push eax                      ; 把6壓入棧
mov ecx, dword ptr [ebp-0x08]  ; 從ebp-0x08取a的值5
push ecx                      ; 把5壓入棧

這裏要先明確兩個核心寄存器:

  • EBP:棧幀基址指針,指向當前函數棧幀的"底部"(高地址端)
  • ESP:棧頂指針,始終指向棧的最頂端(低地址端)

由於棧是向下增長的(地址從高到低),局部變量都存在EBP的負偏移位置(比如bebp-0x14aebp-0x08)。而參數傳遞遵循"從右向左"的順序,所以先壓第二個參數6,再壓第一個參數5,棧中會形成"5在下,6在上"的佈局。

三、call指令:悄悄埋下"回家的路標"

執行call add_func時,CPU會自動做一件關鍵事:把下一條指令的地址(比如0x006118CD)壓入棧中。這個地址就是函數執行完後要返回main的"路標"。

此時棧的結構(從高到低)是:

[返回地址]  <-- 剛壓入的call下一條指令地址
[參數a=5]
[參數b=6]

四、棧幀初始化:為新函數"搭舞台"

進入add_func後,第一時間會執行兩句"標準操作":

push ebp      ; 把main函數的EBP值壓棧保存
mov ebp, esp  ; 讓當前ESP成為新棧幀的基址(EBP)

這兩步完成了棧幀的切換:先保存上層函數(main)的棧幀基址,再以當前棧頂為起點,創建add_func的專屬棧幀。

緊接着執行sub esp, 0xCC,意思是把ESP減去0xCC(約204字節),這是在棧上開闢一塊空間,用於存放局部變量(比如sum)。之後還會把多個寄存器的值壓棧保護,避免函數執行時修改這些值影響上層函數。

五、函數執行:參數和局部變量在哪?

add_func內部計算時,彙編指令是這樣的:

mov dword ptr [ebp-0x8], 0    ; 局部變量sum初始化為0(存在ebp-0x8)
mov eax, dword ptr [ebp+0x8]  ; 從ebp+0x8取參數a(5)
add eax, dword ptr [ebp+0xC]  ; 加上ebp+0xC處的參數b(6)
mov dword ptr [ebp-0x8], eax  ; 結果存入sum

這裏的偏移量規律很重要:

  • ebp+0x8:第一個參數(因為ebp+0x4是返回地址,ebp本身是main的EBP)
  • ebp+0xC:第二個參數(每個int佔4字節,所以+0x8+0x4)
  • ebp-0x8:局部變量sum(在新開闢的棧空間裏)

六、返回值傳遞:EAX寄存器的"特殊使命"

計算完成後,會執行mov eax, dword ptr [ebp-0x8],把sum的值存入EAX寄存器。這是C/C++的"約定"——返回值通過EAX傳遞,無論是int、指針還是小結構體,都靠它帶回給調用者。

七、棧幀銷燬:如何"乾淨收尾"?

函數執行結束後,需要銷燬當前棧幀並恢復上層環境,步驟如下:

  1. 恢復寄存器:用pop指令把之前壓棧的寄存器值還原(先進後出,和入棧順序相反)
  2. 釋放局部變量mov esp, ebp讓棧頂回到棧幀基址,相當於"擦掉"局部變量空間
  3. 恢復上層棧幀pop ebp把main的EBP值取回來,EBP重新指向main的棧幀基址
  4. 跳回調用處ret指令從棧中彈出返回地址,交給EIP寄存器(程序計數器),繼續執行main

回到main後,還會執行add esp, 0x8,把之前壓入的兩個參數從棧中"移除"(實際是移動棧頂覆蓋),整個調用過程才算徹底完成。

八、隱藏的風險:你的函數調用可能被"監視"

調試器能清晰看到棧中的參數、局部變量,甚至能修改返回值。如果是商業程序,這意味着邏輯可能被逆向分析,執行流程可能被篡改。

這時候就需要加殼工具(如Virbox Protector)來防護:它能阻止調試器附加,對代碼進行加密混淆,讓棧幀結構和參數傳遞過程"藏起來",不給破解者可乘之機。

函數調用看似簡單,實則是寄存器與堆棧協同工作的精密流程。理解這些細節,不僅能幫你更快定位內存問題,更能讓你意識到:代碼的安全性,往往就藏在這些底層操作裏。下次寫函數時,不妨想想背後的堆棧變化——原來每一行代碼的執行,都有一套嚴格的"操作手冊"。

user avatar cloudimagine Avatar rongyunrongcloud Avatar
Favorites 2 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.