現象:一段「有鬼」的代碼
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` 共用記憶體?
- `input()` 執行時,在堆疊分配 20×4=80 bytes 給 `arr`,寫入 0~19。
- `input()` 返回,堆疊指針 `RSP` 恢復,**但記憶體內容不會清零**。
- `output()` 被呼叫,**重用完全相同的堆疊位置** 分配自己的 `arr` (相同大小、相同對齊)。
- `output()` 未初始化即讀取,剛好讀到殘留的 0~19。
這是未定義行為 (UB):讀取未初始化區域變數。巧合下輸出「正確」,實則極度危險。
加入 `printf` 後發生什麼?
int main(void) {
input();
printf("middle\n"); // 呼叫庫函數
output();
}
`printf` 內部使用大量區域變數,**覆蓋了堆疊上殘留的資料**,導致 `output` 讀到亂碼 (前半段可能倖存,後半段被覆蓋)。
編譯器優化的影響
| 模式 | 行為 | 原因 |
| Debug (MSVC/GCC -O0) | 區域變數自動填 0xCC / 不初始化 | 方便偵錯偵測未初始化讀取 |
| Release (-O2/-O3) | `input()` 可能被**內聯/刪除** (無副作用) | 編譯器見 `arr` 未被外部觀測,直接優化掉 |
教訓:永遠不要依賴未初始化記憶體的內容,無論編譯器模式。
正確做法:參數傳遞或靜態儲存
/* 方案 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] 在這裡) │
└─────────────────────┘
低位址
核心原則
- 區域變數 = 堆疊暫存,函數返回即失效,內容隨後覆蓋。
- 相同大小/對齊的區域陣列 在連續呼叫中極大機率重用同一記憶體區塊。
- 未初始化讀取 = UB,編譯器有權優化掉、填充 0xCC、或保留垃圾值。
- 跨函數共享資料 必須透過參數指標、返回指標、靜態/動態儲存 顯式傳遞。
延伸閱讀
- CSAPP 第 3 章《程式的機器級表示》— 堆疊幀、呼叫慣例
- System V AMD64 ABI 文件 — 參數傳遞、紅區、向量暫存器保存
- GCC `-fstack-protector` / Clang `-fsanitize=address` — 堆疊緩衝區保護與檢測工具
- 上一篇文章: C 語言前置處理器完全指南:宏、條件編譯、實戰技巧與避坑指南
- 下一篇文章: Objective-C 字典完全指南:NSDictionary 與 NSMutableDictionary 實戰詳解
Copyright ©
程式碼之家 All Rights Reserved