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

C 語言函數呼叫堆疊幀深度剖析:為什麼區域陣列會「神奇」共享記憶體?

     編輯:關於C語言

現象:一段「有鬼」的代碼

void input(void) {
    int arr[20];
    for (int i = 0; i < 20; i++) arr[i] = i;
}

void output(void) {
    int arr[20];
    for (int i = 0; i < 20; i++) printf("%d\n", arr[i]); // 竟然印出 0~19!
}

int main(void) {
    input();
    output();
    while (1) {}
}

輸出竟然是 0~19! 兩個函數各自的 `arr` 明明是不同變數,為何 `output` 能讀到 `input` 寫入的值?

關鍵知識:堆疊向下增長與幀結構

  • 堆疊方向:高位址 → 低位址 (x86/x64/ARM 皆同)
  • 關鍵暫存器:
    • RBP/EBP (Base Pointer) — 幀基址,指向當前幀底部
    • RSP/ESP (Stack Pointer) — 幀頂,隨 push/pop 移動

函數呼叫完整流程 (x86-64 System V ABI)

sequenceDiagram
    participant Caller
    participant Callee
    Caller->>Callee: 1. 參數放入暫存器/堆疊
    Caller->>Callee: 2. CALL 指令 (推 RIP, 跳轉)
    Callee->>Callee: 3. PUSH RBP (保存呼叫者幀基址)
    Callee->>Callee: 4. MOV RSP, RBP (建立新幀基址)
    Callee->>Callee: 5. SUB RSP, N (分配區域變數空間)
    Callee->>Callee: 6. 執行函數本體
    Callee->>Caller: 7. MOV RBP, RSP (釋放區域空間)
    Callee->>Caller: 8. POP RBP (恢復呼叫者幀基址)
    Callee->>Caller: 9. RET (彈 RIP 跳回)

為什麼 `input` 與 `output` 共用記憶體?

  1. `input()` 執行時,在堆疊分配 20×4=80 bytes 給 `arr`,寫入 0~19。
  2. `input()` 返回,堆疊指針 `RSP` 恢復,**但記憶體內容不會清零**。
  3. `output()` 被呼叫,**重用完全相同的堆疊位置** 分配自己的 `arr` (相同大小、相同對齊)。
  4. `output()` 未初始化即讀取,剛好讀到殘留的 0~19。

這是未定義行為 (UB):讀取未初始化區域變數。巧合下輸出「正確」,實則極度危險。

加入 `printf` 後發生什麼?

int main(void) {
    input();
    printf("middle\n");   // 呼叫庫函數
    output();
}

`printf` 內部使用大量區域變數,**覆蓋了堆疊上殘留的資料**,導致 `output` 讀到亂碼 (前半段可能倖存,後半段被覆蓋)。

編譯器優化的影響

教訓:永遠不要依賴未初始化記憶體的內容,無論編譯器模式。

正確做法:參數傳遞或靜態儲存

/* 方案 1: 呼叫者分配,傳指標 */
void input(int *arr, size_t n) {
    for (size_t i = 0; i < n; i++) arr[i] = i;
}
void output(const int *arr, size_t n) {
    for (size_t i = 0; i < n; i++) printf("%d\n", arr[i]);
}
int main(void) {
    int buf[20];
    input(buf, 20);
    output(buf, 20);
}

/* 方案 2: 靜態區域變數 (單例模式) */
int *get_buffer(void) {
    static int buf[20];  // 僅初始化一次,儲存在 .bss
    return buf;
}

進階:查看組合碼驗證

$ gcc -Og -S stack_demo.c -o stack_demo.s
$ grep -A5 -B5 'sub.*rsp' stack_demo.s
input:
    pushq %rbp
    movq %rsp, %rbp
    subq $80, %rsp      # 分配 80 bytes 給 arr[20]
    ...
output:
    pushq %rbp
    movq %rsp, %rbp
    subq $80, %rsp      # 相同位移,相同位置!
    ...

總結:堆疊幀記憶體模型心智圖

高位址
┌─────────────────────┐
│ 呼叫者區域變數       │ ← RBP (呼叫者幀基址)
├─────────────────────┤
│ 返回位址 (RIP)       │
├─────────────────────┤
│ 呼叫者 RBP (保存)    │ ← RBP (被呼叫者幀基址)
├─────────────────────┤
│ 被呼叫者區域變數     │ ← RSP (幀頂,向下增長)
│   (arr[20] 在這裡)   │
└─────────────────────┘
低位址

核心原則

  1. 區域變數 = 堆疊暫存,函數返回即失效,內容隨後覆蓋。
  2. 相同大小/對齊的區域陣列 在連續呼叫中極大機率重用同一記憶體區塊。
  3. 未初始化讀取 = UB,編譯器有權優化掉、填充 0xCC、或保留垃圾值。
  4. 跨函數共享資料 必須透過參數指標、返回指標、靜態/動態儲存 顯式傳遞。

延伸閱讀

  • CSAPP 第 3 章《程式的機器級表示》— 堆疊幀、呼叫慣例
  • System V AMD64 ABI 文件 — 參數傳遞、紅區、向量暫存器保存
  • GCC `-fstack-protector` / Clang `-fsanitize=address` — 堆疊緩衝區保護與檢測工具
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
模式行為原因
Debug (MSVC/GCC -O0)區域變數自動填 0xCC / 不初始化方便偵錯偵測未初始化讀取
Release (-O2/-O3)`input()` 可能被**內聯/刪除** (無副作用)編譯器見 `arr` 未被外部觀測,直接優化掉