程序與執行時堆疊 (Procedures & the Run-Time Stack)
Overview Table
| 主題 | 核心內容 | 書頁 |
|---|---|---|
| Procedure 三大機制 | Passing control、Passing data、Allocating/deallocating memory | p.275 |
| Run-time stack | 向低位址成長;%rsp 指向 stack top;LIFO 配合 call/return 順序 |
p.275-277 |
| Control transfer | call push return address 並跳轉;ret pop 位址並跳回 |
p.277-280 |
| Data transfer | 前 6 個整數/指標引數用暫存器傳遞;第 7 個以後放 stack;回傳值在 %rax |
p.281-283 |
| Local storage on stack | 用 subq $N, %rsp 配置;需要記憶體位址(&)、陣列/結構、暫存器不夠時才用 |
p.284-287 |
| Local storage in registers | callee-saved:%rbx, %rbp, %r12–%r15;其餘(除 %rsp)為 caller-saved |
p.287-289 |
| Recursive procedures | 每次呼叫有私有 stack frame,標準機制即可支援遞迴與互相遞迴 | p.289-291 |
3.7 程序 (Procedures) 概觀 (p.274-275)
Procedure(程序)是軟體的關鍵抽象:把實作某功能的程式碼包裝起來,配上指定的引數 (arguments) 與可選的回傳值 (return value),在程式各處呼叫。不同語言叫法不同(function、method、subroutine、handler),機器層級支援都涉及同一組機制。設 P 呼叫 Q,Q 執行後返回 P:
- Passing control(控制傳遞):進入 Q 時 PC 設為 Q 的起始位址;返回時 PC 設為 P 中 call 之後的那條指令。
- Passing data(資料傳遞):P 提供參數給 Q;Q 回傳值給 P。
- Allocating/deallocating memory(記憶體配置/釋放):Q 可能需要為區域變數配置空間,返回前釋放。
x86-64 採極簡主義 (minimalist) 策略:每個 procedure 只實作它實際需要的機制,盡量降低呼叫開銷。因此 stack frame 的各部分「用不到就省略」。
3.7.1 Run-Time Stack(執行時堆疊)(p.275-277)
C 的呼叫機制利用 stack 的 LIFO(last-in, first-out) 紀律:Q 執行時,P 與呼叫鏈上的所有程序都暫停 (suspended),只有 Q 需要配置新空間;Q 返回時其區域儲存即可全部釋放——正好符合 stack 的配置/釋放順序。
- x86-64 stack 向低位址成長;
%rsp永遠指向 stack top(最頂元素)。 pushq/popq存取資料;遞減%rsp= 配置空間、遞增%rsp= 釋放空間(無初始值的空間直接減指標即可)。- 程序在暫存器放不下的儲存需求,配置在 stack 上,該區域稱為此程序的 stack frame(堆疊框架)。
- return address 屬於呼叫者 P 的 frame(它保存的是 P 的狀態)。
- 多數程序的 frame 為固定大小,在程序開頭一次配置;變動大小 frame 見 3.10.5(alloca / 變長陣列)。
Stack "bottom"(高位址)
+---------------------------+
| Earlier frames |
+---------------------------+ ---
| Argument n | |
| ... | | Frame for
| Argument 7 | | calling function P
+---------------------------+ |
| Return address | --- <- call 時 push
+---------------------------+ ---
| Saved registers | |
+---------------------------+ | Frame for
| Local variables | | executing function Q
+---------------------------+ |
| Argument build area | | <- Q 呼叫 >6 引數函式時用
+---------------------------+ ---
%rsp -> Stack "top"(低位址,向下成長)
Leaf procedure(葉程序):所有區域變數放得進暫存器、且不呼叫其他函式的程序,完全不需要 stack frame(p.277)。這是最常見的省略情形。
3.7.2 Control Transfer(控制傳遞)(p.277-280)
call Q 做兩件事:(1) 把 return address A(緊接在 call 之後那條指令的位址)push 上 stack;(2) 把 PC 設為 Q 的起始位址。ret 則 pop 出位址 A 並把 PC 設為 A。
| 指令 | 說明 |
|---|---|
call Label |
直接呼叫 (direct):目標以 label 給定 |
call *Operand |
間接呼叫 (indirect):* 加運算元指定符(同 jump 的間接形式) |
ret |
從 stack pop 位址並跳回 |
- objdump 反組譯輸出寫作
callq/retq,後綴 q 只是強調 x86-64 版本,兩種寫法可互換。 - 執行效果(以
%rsp舊值為 R):
call: R_new = R - 8; M[R_new] = A(下一條指令位址); PC = 目標
ret : PC = M[R]; R_new = R + 8
書中 multstore 範例 (p.278):main 在 0x400563 執行 callq 400540,push 返回位址 0x400568、%rsp 從 0x7fffffffe840 變 0x7fffffffe838;multstore 的 retq pop 出 0x400568 跳回,%rsp 恢復原值。
p.279 的 top/leaf 追蹤範例展現巢狀呼叫:main 呼叫 top(100) → top 算出 x-5=95 後呼叫 leaf(95) → leaf 回傳 97 → top 加倍為 194 回傳給 main。每層 call 使 %rsp 減 8 並在 *%rsp 留下正確返回位址,ret 後 %rsp 逐層復原——stack 保證返回到正確位置。
return address 是「call 指令的下一條指令位址」,不是 call 本身的位址。追蹤題(如 Practice Problem 3.32)最常在此出錯。
3.7.3 Data Transfer(資料傳遞)(p.281-283)
x86-64 大多數參數與回傳值透過暫存器傳遞:最多 6 個整數類 (integral,即整數與指標) 引數用暫存器,回傳值放 %rax。暫存器依引數順序固定分配,名稱依運算元大小選用子暫存器:
| Operand size (bits) | 引數 1 | 引數 2 | 引數 3 | 引數 4 | 引數 5 | 引數 6 |
|---|---|---|---|---|---|---|
| 64 | %rdi |
%rsi |
%rdx |
%rcx |
%r8 |
%r9 |
| 32 | %edi |
%esi |
%edx |
%ecx |
%r8d |
%r9d |
| 16 | %di |
%si |
%dx |
%cx |
%r8w |
%r9w |
| 8 | %dil |
%sil |
%dl |
%cl |
%r8b |
%r9b |
超過 6 個引數(n > 6)時:
- 呼叫者 P 在自己的 frame 內配置空間存引數 7..n(即「argument build area」),引數 7 在 stack top。
- stack 上傳參時,所有資料大小一律向上取整為 8 的倍數。
- call 之後(return address 已 push),被呼叫者看到的引數 7、8 位於
8(%rsp)、16(%rsp)。
p.283 的 proc 範例(8 個不同大小引數):a1–a3p 用 %rdi %rsi %edx %rcx %r8w %r9(a2p 為指標,佔完整 64-bit 的 %rcx;a3p 為指標,用 %r9),a4(char)在 %rsp+8、a4p 在 %rsp+16;程式依大小選用 addq/addl/addw/addb。注意第 6 行 movl 8(%rsp), %edx 讀 4 bytes,但後續 addb %dl 只用最低 byte。
proc 執行中的 stack(相對 %rsp):
16 | a4p (8 bytes)
8 | a4 (佔 8 bytes,實際只用 1 byte)
0 | Return address <- %rsp
Practice Problem 3.33 型考點:從 movslq %edi, %rdi(sign-extend 32→64)與 addb %sil, (%rcx) 等指令的子暫存器名稱反推各參數的型別與大小。
3.7.4 Local Storage on the Stack(區域變數放 stack)(p.284-287)
多數程序的區域資料放得進暫存器;必須放記憶體的常見情況有三:
- 暫存器不夠放所有區域資料。
- 對區域變數取位址
&——必須能產生它的記憶體位址(暫存器沒有位址)。 - 區域變數是陣列或結構,須以陣列/結構參照存取(見 03-Machine-Level-Programs/05-Arrays-and-Data-Structures)。
典型做法:subq $N, %rsp 配置、addq $N, %rsp 釋放,對應 Figure 3.25 的 Local variables 區。
caller/swap_add 範例 (p.285):caller 因為要對 arg1、arg2 取 &,以 subq $16, %rsp 配置 16 bytes,arg1 在 offset 0、arg2 在 offset 8;用 leaq 8(%rsp), %rsi 與 movq %rsp, %rdi 產生指標當引數;呼叫完成後從 stack 讀回被交換的值,最後 addq $16, %rsp 釋放。
call_proc 範例 (p.286-287):同時示範「區域變數 + 超額引數」的完整 frame。配置 32 bytes:
call_proc 的 stack frame(相對 %rsp):
32 | (Return address of call_proc 之上)
24 | x1 (long, 8B) 31..24
20 | x2 (int, 4B) 23..20
18 | x3 (short, 2B) 19..18
17 | x4 (char, 1B) 17
8 | Argument 8 = &x4
0 | Argument 7 = 4 <- %rsp
&x1..&x4由leaq產生;引數 7、8 存在 offset 0、8。- call 之後 return address 被 push,
proc端看到引數 7、8 位移各加 8(變成 8、16)。 - 返回後以
movslq/movswl/movsbl把小型別 sign-extend 讀回計算,最後addq $32, %rsp釋放。
3.7.5 Local Storage in Registers(區域值放暫存器)(p.287-289)
暫存器是所有程序共享的單一資源,x86-64 用一套全體(含函式庫)都必須遵守的慣例避免 callee 覆寫 caller 還要用的值:
| 類別 | 暫存器 | 責任 |
|---|---|---|
| Callee-saved | %rbx, %rbp, %r12–%r15 |
被呼叫者 Q 必須保證返回時值不變:不動它,或 push 保存→使用→pop 還原 |
| Caller-saved | 其餘所有(%rax, %rdi, %rsi, %rdx, %rcx, %r8–%r11 等) |
任何函式可任意修改;呼叫者 P 若還需要其值,呼叫前自己保存 |
| 特殊 | %rsp |
stack pointer,不屬於上述任一類 |
- push callee-saved 暫存器形成 frame 中的「Saved registers」區。
- pop 順序必須與 push 相反(LIFO)。
p.288 範例 P(x,y) 呼叫 Q 兩次:x 存 %rbp(第一次 call 期間要活)、Q(y) 結果存 %rbx(第二次 call 期間要活);開頭 pushq %rbp; pushq %rbx; subq $8, %rsp(對齊),結尾反序 popq %rbx; popq %rbp。
Practice Problem 3.34 型考點:callee-saved 只有 6 個(%rbx %rbp %r12–%r15),區域值超過 6 個且都要跨 call 存活時,多出來的必須放 stack。
3.7.6 Recursive Procedures(遞迴程序)(p.289-291)
前述暫存器與 stack 慣例天然支援遞迴:每次呼叫在 stack 上有自己私有的空間(return address、saved registers、必要時的區域變數),多個未完成 (outstanding) 呼叫的區域狀態互不干擾;stack 的配置/釋放順序恰好匹配 call/return 順序。
p.290 遞迴階乘 rfact:
rfact(n): 呼叫深度示意(n=3):
pushq %rbx save | rfact(3): %rbx=3 |
movq %rdi, %rbx n -> %rbx | rfact(2): %rbx=2 |
movl $1, %eax result=1 | rfact(1): ret 1 |
cmpq $1, %rdi 每層各有 saved %rbx
jle .L35 與 return address
leaq -1(%rdi), %rdi n-1
call rfact
imulq %rbx, %rax n * rfact(n-1)
.L35:
popq %rbx restore
ret
n存進 callee-saved 的%rbx(先 push 保存舊值);遞迴 call 返回後保證:(1) 結果在%rax,(2)%rbx仍是本層的n。- 遞迴呼叫與一般呼叫完全相同,無特殊機制;同樣適用於互相遞迴 (mutual recursion)(P 呼叫 Q、Q 又呼叫 P)。
- Practice Problem 3.35 型:由
shrq $2, %rdi+addq %rbx, %rax反推rfun(x) = x + rfun(x>>2)、終止條件x == 0回傳 0。
Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
call 對 %rsp、*%rsp、PC 的效果 |
%rsp -= 8;M[%rsp] = call 的下一條指令位址;PC = 被呼叫者起始位址 |
ret 的效果 |
PC = M[%rsp];%rsp += 8 |
| 第 k 個整數引數在哪(k ≤ 6) | 依序 %rdi %rsi %rdx %rcx %r8 %r9(依大小換用 %edi/%di/%dil 等子暫存器) |
| 第 7、8 個引數在哪(callee 視角) | 8(%rsp)、16(%rsp)(return address 佔 0(%rsp)) |
| 回傳值在哪 | %rax |
| stack 傳參的對齊 | 每個引數向上取整至 8 bytes 的倍數 |
| 哪些是 callee-saved | %rbx, %rbp, %r12, %r13, %r14, %r15(共 6 個) |
函式開頭 pushq %rbx ... 結尾 popq %rbx |
使用 callee-saved 暫存器前保存、返回前反序還原 |
為何 caller 要 subq $16, %rsp |
區域變數被取 &(或為陣列/結構、暫存器不足),必須放 stack |
| 何時完全不需要 stack frame | leaf procedure:區域值全在暫存器且不呼叫其他函式 |
| 遞迴需要特殊支援嗎 | 不需要;每次呼叫有私有 frame,標準 call/return + 暫存器慣例即足夠 |
| 追蹤表填空(Fig 3.27 / PP 3.32) | 進入 callee 時 %rsp 已減 8、*%rsp = 返回位址;ret 後 %rsp 復原 |
| 由子暫存器名反推參數型別(PP 3.33) | %dil/%sil=char、%di=short、%edi=int、%rdi=long/pointer;movslq 表示 int 被 sign-extend |
| 區域值多於 6 個且跨 call 存活(PP 3.34) | 前 6 個用 callee-saved 暫存器,其餘放 stack |
一般化敘述的例外:(1)「引數用暫存器傳」只適用前 6 個整數類引數,浮點引數另用 %xmm 暫存器(見 03-Machine-Level-Programs/07-Floating-Point-Code);(2)「frame 固定大小、開頭配置」不適用變動大小 frame(alloca、變長陣列,3.10.5,見 03-Machine-Level-Programs/06-Buffer-Overflow-and-Pointer-Safety);(3)「許多函式不需要 frame」僅限 leaf procedure。
Related Notes
- 03-Machine-Level-Programs/03-Control-Flow — jump/條件分支與 jump table,call/ret 是其延伸
- 03-Machine-Level-Programs/02-Data-Movement-and-Arithmetic — pushq/popq、mov 系列與運算元格式
- 03-Machine-Level-Programs/05-Arrays-and-Data-Structures — 陣列/結構作為區域變數時的 stack 配置
- 03-Machine-Level-Programs/06-Buffer-Overflow-and-Pointer-Safety — stack frame 被溢位攻擊的原理與防禦
- 03-Machine-Level-Programs/01-Program-Encodings-and-Data-Formats — 反組譯輸出(callq/retq)與資料大小後綴
- 08-Exceptional-Control-Flow/06-Nonlocal-Jumps — setjmp/longjmp 跳脫標準 call/return 紀律的例外