程序與執行時堆疊 (Procedures & the Run-Time Stack)

Overview Table

主題 核心內容 書頁
Procedure 三大機制 Passing controlPassing dataAllocating/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:

Important

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 的配置/釋放順序。

        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"(低位址,向下成長)
Tip

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 位址並跳回
call:  R_new = R - 8;  M[R_new] = A(下一條指令位址);  PC = 目標
ret :  PC = M[R];      R_new = R + 8

書中 multstore 範例 (p.278):main0x400563 執行 callq 400540,push 返回位址 0x400568%rsp0x7fffffffe8400x7fffffffe838;multstoreretq 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 保證返回到正確位置

Warning

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.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
Tip

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)

多數程序的區域資料放得進暫存器;必須放記憶體的常見情況有三:

典型做法:subq $N, %rsp 配置、addq $N, %rsp 釋放,對應 Figure 3.25 的 Local variables 區。

caller/swap_add 範例 (p.285):caller 因為要對 arg1arg2&,以 subq $16, %rsp 配置 16 bytes,arg1 在 offset 0、arg2 在 offset 8;用 leaq 8(%rsp), %rsimovq %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

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,不屬於上述任一類

p.288 範例 P(x,y) 呼叫 Q 兩次:x 存 %rbp(第一次 call 期間要活)、Q(y) 結果存 %rbx(第二次 call 期間要活);開頭 pushq %rbp; pushq %rbx; subq $8, %rsp(對齊),結尾反序 popq %rbx; popq %rbp

Important

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

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 暫存器前保存、返回前反序還原
為何 callersubq $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
Warning

一般化敘述的例外:(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。