首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> 關於C

深度解析 C 語言函數調用約定:棧幀、參數傳遞與返回值機制

     編輯:關於C

前言

理解函數調用底層機制,對調試、優化、編寫彙編接口至關重要。本文基於 16 位/32 位 x86 架構,詳細剖析調用約定、棧幀構建、參數訪問與返回流程。

一、標準調用流程(Caller/Callee 視角)

  1. Caller:按從右到左順序將參數壓棧(首參數最後壓棧,便於可變參數定位)
  2. Caller:執行 CALL 指令,壓入返回地址(EIP),跳轉到 Callee
  3. Callee:保存舊 EBP(push ebp),建立新棧幀基址(mov ebp, esp)
  4. Callee:分配局部變量空間(sub esp, N)
  5. Callee:執行函數體,結果放入 EAX/EDX:EAX/ST0
  6. Callee:恢復棧指針(mov esp, ebp 或 leave),彈出舊 EBP(pop ebp)
  7. Callee:RET 返回,Caller 清理參數棧(add esp, N)

二、棧幀內存佈局圖解

高地址
+------------------+
| 參數 3 (最右)     |  <-- [EBP+16] (32位 near call)
+------------------+
| 參數 2            |  <-- [EBP+12]
+------------------+
| 參數 1 (最左)     |  <-- [EBP+8]
+------------------+
| 返回地址          |  <-- [EBP+4]
+------------------+
| 舊 EBP           |  <-- [EBP]  (Callee 保存)
+------------------+
| 局部變量 1        |  <-- [EBP-4]
+------------------+
| 局部變量 2        |  <-- [EBP-8]
+------------------+
| ...              |
低地址 (ESP)

關鍵點:

  • EBP 作為幀指針,參數正偏移,局部變量負偏移
  • 可變參數函數(如 printf)依賴首參數(格式串)確定後續參數數量/類型,故必須右向左壓棧
  • 調用者清理棧(cdecl 約定)保證原型不匹配時棧平衡

三、AT&T 語法彙編實例(32 位,棧向低地址增長)

# fun(p0, p1, p2)
# 1. 參數入棧
movl $p2, 8(%esp)
movl $p1, 4(%esp)
movl $p0, (%esp)

# 2. CALL
call fun          # 壓入返回地址,ESP -4

# 3. 進入 fun
push %ebp         # 保存舊 EBP
movl %esp, %ebp   # 建立新幀

# 此時棧佈局:
# [EBP]     舊 EBP
# [EBP+4]   返回地址
# [EBP+8]   p0
# [EBP+12]  p1
# [EBP+16]  p2

# 4. 分配局部變量
subl $24, %esp    # 6 個 4 字節局部變量

# 5. 返回序列
movl %ebp, %esp   # 或 addl $24, %esp
popl %ebp         # 恢復 EBP,銷毀幀
ret               # 彈出返回地址跳轉

四、C 代碼驗證棧幀佈局

#include 
#include 

void fun(int a, int b) {
    int local = 12;
    unsigned int ebp_val, esp_val;
    long ret_addr;

    /* 內聯彙編讀取 EBP/ESP */
    asm volatile (
        "movl %%ebp, %%eax\n\t"
        "movl %%esp, %%ebx\n\t"
        : "=a"(ebp_val), "=b"(esp_val)
    );
    printf("ebp = %p; esp = %p\n", (void*)ebp_val, (void*)esp_val);
    printf("local vars size: %u bytes\n", ebp_val - esp_val);

    /* 讀取參數與返回地址 */
    asm volatile (
        "movl 12(%%ebp), %%eax\n\t"  // 參數 b
        "movl 8(%%ebp), %%ebx\n\t"   // 參數 a
        "movl 4(%%ebp), %%ecx\n\t"   // 返回地址
        : "=a"(ebp_val), "=b"(esp_val), "=c"(ret_addr)
    );
    printf("p2(b) = %x; p1(a) = %x; ret = %p\n", ebp_val, esp_val, (void*)ret_addr);
}

int main() {
    fun(1, 2);
    return 0;
}

五、總結與避坑指南

  1. 手寫彙編時,必須遵守調用約定:保存被調用者保存寄存器(EBX、ESI、EDI、EBP、ESP)、正確構建/銷毀棧幀
  2. 返回局部變量地址是經典錯誤:leave/pop ebp 後棧幀銷毀,局部變量內存失效
  3. 局部變量空間由編譯器根據符號表在編譯期確定(sub esp, N),動態大小需用 alloca 或堆分配
  4. 調試技巧:GDB 中 info frame、bt、disas 可觀察棧幀與調用鏈

參考資料

  • NASM 手冊 - Calling Conventions 章節
  • 《程序員的自我修養》- 第 3 章 運行時環境
  • System V AMD64 ABI / Microsoft x64 Calling Convention(64 位擴展)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved