例外 (Exceptions)

Overview Table

主題 核心內容 書頁
ECF 定義 control flow 的突然改變,反應「非程式變數所能表達」的系統狀態變化 p.758
Exception 定義 對 processor state 變化(event)所做的突然控制轉移,由硬體+OS 共同實作 p.759
Exception handling 透過 exception table(jump table)間接呼叫 exception handler p.760-761
四大類例外 Interrupt / Trap / Fault / Abort(非同步 1 類 + 同步 3 類) p.762-764
Linux/x86-64 例外 最多 256 種;0-31 由 Intel 定義,32-255 由 OS 定義 p.765
System call syscall 指令實作的 trap;參數走暫存器不走 stack p.765-767

例外控制流 (Exceptional Control Flow, ECF) 總覽 (p.758-759)

處理器從開機到關機,PC 依序取值 a0,a1,,an1,每個 ak 是指令 Ik 的位址;從 akak+1 的轉移稱為 control transfer,其序列即 control flow。平滑的 flow 中 IkIk+1 在記憶體相鄰;jump/call/return 造成的跳躍是為了反應「程式內部狀態」(程式變數)。但系統還必須反應與程式變數無關的系統狀態變化(timer 到期、封包抵達、磁碟資料就緒、子行程終止),這類突然的控制流改變統稱 exceptional control flow (ECF)

ECF 存在於系統的每一層:

理解 ECF 的五個理由 (p.758-759):理解 OS 如何實作 I/O、process、virtual memory;理解應用程式如何透過 trap / system call 向 OS 要求服務;能寫 shell、Web server 等程式;是理解 concurrency 的第一步;理解 C++/Java 軟體例外(try/catch/throw)的底層原理。

本章定位:在此之前你學的是「應用程式與硬體」的互動;從本章起開始學「應用程式與作業系統」的互動——而這些互動全都繞著 ECF 展開。

例外的解剖 (Anatomy of an Exception) (p.759-760)

Exception = 對 processor state 中某個變化(event)的回應而發生的控制流突然改變。state 編碼在處理器內部的位元與訊號中。event 可能與當前指令直接相關(page fault、算術溢位、除以零),也可能無關(system timer 到期、I/O 完成)。

  Application program              Exception handler
  ───────────────────              ─────────────────
        │
  Event │  I_curr ── Exception ──────►┐
 occurs │  I_next ◄──────┐            │ Exception
  here  │                │            │ processing
        │        Exception return ────┘
        ▼        (optional:回 I_curr / 回 I_next / abort)

處理器偵測到 event 後,透過稱為 exception table 的 jump table 做一次間接程序呼叫(即 exception),進入 OS 的 exception handler。handler 處理完後三選一:

  1. 把控制權還給 I_curr(event 發生時正在執行的指令)。
  2. 把控制權還給 I_next(若無例外本來要執行的下一條指令)。
  3. abort 被中斷的程式。
「hardware vs. software exception」術語 (p.760 Aside):C++/Java 的 try/catch/throw 也叫 exception,那是應用層軟體例外;本章的 exception 指硬體+OS 層機制。通常由上下文即可分辨。

8.1.1 Exception Handling (p.760-762)

例外處理是硬體與軟體的緊密分工。每種例外被指派唯一的非負整數 exception number:一部分由處理器設計者指定(divide by zero、page fault、memory access violation、breakpoint、arithmetic overflow),另一部分由 OS kernel 設計者指定(system call、外部 I/O 裝置 signal)。

                Exception number k
                     (× 8)
                       │           Exception table
                       ▼           ┌──────────────┐
 Exception table ──►( + )────────► │ 0: handler 0 │──► Code for handler 0
  base register                    │ 1: handler 1 │──► Code for handler 1
 (special CPU reg)                 │ 2: handler 2 │──► Code for handler 2
                                   │      ...     │
                                   │ n-1          │──► Code for handler n-1
                                   └──────────────┘

Handler 位址 = exception table base register + k × 8(exception number 為表的索引;表的起始位址存在特殊 CPU 暫存器 exception table base register)。

例外 vs. 一般程序呼叫 (p.761-762)

面向 一般 procedure call Exception
返回位址 下一條指令 依例外類別,是 I_currI_next
額外壓入的狀態 壓入額外 processor state(如 x86-64 的 EFLAGS,含 condition codes),供 handler 返回後重啟程式
使用的 stack 呼叫者的 user stack 若從 user program 轉移到 kernel,一律壓入 kernel stack
執行模式 user mode(受限) handler 在 kernel mode 執行,可存取所有系統資源

硬體觸發例外之後,其餘工作由軟體(exception handler)完成。handler 處理完 event 後,可選擇性執行特殊的「return from interrupt」指令返回:把狀態 pop 回處理器的控制與資料暫存器、若中斷的是 user program 則恢復 user mode,再把控制權交還被中斷的程式。

8.1.2 Classes of Exceptions(四大類)(p.762-764)

Class 成因 Async/Sync 返回行為
Interrupt 來自 I/O 裝置的 signal Async 一律返回 I_next
Trap 蓄意(intentional)的例外 Sync 一律返回 I_next
Fault 可能可恢復的錯誤 Sync 可能返回 I_curr(重新執行),否則 abort
Abort 不可恢復的致命錯誤 Sync 永不返回
Async vs. Sync 的判準:Interrupt 不是由任何特定指令的執行造成(非同步);trap/fault/abort 是執行當前指令的直接結果(同步),該指令稱為 faulting instruction (p.763)。

Interrupts (p.762-763)

I/O 裝置(網卡、磁碟控制器、timer chip)藉由拉高處理器晶片上的 interrupt pin 並把識別裝置的 exception number 放上 system bus 來觸發中斷。其 handler 常稱 interrupt handler

 (1) Interrupt pin goes high      (2) Control passes to handler
     during I_curr                    after I_curr FINISHES
        I_curr ─────────────────────────►┐
        I_next ◄────────┐                │ (3) Interrupt
                        │                │     handler runs
          (4) Handler returns ───────────┘
              to I_next

Traps 與 System Calls (p.763-764)

Trap 是蓄意的例外——執行某指令的預期結果;handler 一律返回 I_next。最重要的用途:在 user program 與 kernel 之間提供類似程序呼叫的介面,即 system call。user program 需要 kernel 服務時(讀檔 read、建立 process fork、載入程式 execve、終止 process exit),執行處理器提供的特殊指令 syscall n 要求服務 n;trap 進入 handler,由 handler 解碼參數並呼叫適當的 kernel routine。

 (1) Application makes a        (2) Control passes to handler
     system call
        syscall ─────────────────────────►┐
        I_next ◄────────┐                 │ (3) Trap
                        │                 │     handler runs
          (4) Handler returns ────────────┘
              to I_next (instruction following the syscall)

從程式設計師觀點,system call 與一般函式呼叫看起來相同,但實作大不同:

面向 一般函式呼叫 System call
執行模式 user mode,可執行的指令種類受限 kernel mode,可執行 privileged instructions
Stack 與呼叫者共用同一 stack 使用 kernel 定義的 stack

Faults (p.764)

Fault 源自 handler 可能修正的錯誤條件。handler 若能修正錯誤 → 返回 faulting instruction 重新執行;若不能 → 返回 kernel 的 abort routine 終止造成 fault 的程式。

 (1) I_curr causes a fault      (2) Control passes to handler
        I_curr ─────────────────────────►┐
          ▲                              │ (3) Fault
          │                              │     handler runs ──► abort
          └── (4) Handler re-executes ───┘        (若無法修正)
              current instruction (I_curr)

經典例子:page fault——指令參照的 virtual address 對應的 page(典型 4 KB 的連續 virtual memory 區塊)不在記憶體,須自磁碟載入。page fault handler 載入該頁後返回 faulting instruction;重新執行時該頁已在記憶體,指令得以完成而不再 fault(詳見 09-Virtual-Memory/01-Address-Spaces-and-VM-Caching)。

Aborts (p.764)

Abort 源自不可恢復的致命錯誤,典型如 DRAM/SRAM 位元損毀造成的 parity error 等硬體錯誤。abort handler 永不把控制權還給應用程式,而是轉給 kernel 的 abort routine 終止該程式。

 (1) Fatal hardware error       (2) Control passes to handler
        I_curr ─────────────────────────►┐
                                         │ (3) Abort handler runs
                                         │
          (4) Handler returns ───────────┴──► abort routine
              to abort routine                (terminates program)

8.1.3 Exceptions in Linux/x86-64 Systems (p.765-767)

x86-64 最多 256 種例外類型:

Exception number 描述 類別
0 Divide error Fault
13 General protection fault Fault
14 Page fault Fault
18 Machine check Abort
32-255 OS-defined exceptions Interrupt 或 trap

Linux/x86-64 Faults 與 Aborts (p.765)

分類與名稱的陷阱:divide error 與 general protection fault 雖歸類為 Fault(理論上可能可恢復),但 Linux/Unix 實務上不嘗試恢復、直接終止程式;且 shell 的回報字串容易誤導——divide error 顯示 "Floating exception"(跟浮點無關),#13 顯示 "Segmentation fault"。

Linux/x86-64 System Calls (p.765-767)

Linux 提供數百個 system call,每個有唯一整數編號,對應 kernel 中另一張 jump table 的 offset(此表不是 exception table)。常見範例 (Figure 8.10):

編號 名稱 描述 編號 名稱 描述
0 read 讀檔 37 alarm 排程 alarm signal
1 write 寫檔 39 getpid 取得 process ID
2 open 開檔 57 fork 建立 process
3 close 關檔 59 execve 執行程式
4 stat 取得檔案資訊 60 _exit 終止 process
9 mmap 映射 memory page 到檔案 61 wait4 等待 process 終止
12 brk 重設 heap 頂端 62 kill 送 signal 給 process
32 dup2 複製 file descriptor 33 pause 暫停直到 signal 抵達

C 程式可用 syscall 函式直接呼叫任何 system call,但實務上很少需要:C 標準函式庫為多數 system call 提供 wrapper functions(打包參數 → 以適當指令 trap 進 kernel → 回傳狀態給呼叫者)。本書把 system call 與其 wrapper 統稱 system-level functions

x86-64 syscall 指令的暫存器慣例 (p.766):

與一般 x86-64 函式呼叫慣例的差異:一般呼叫的第 4 個參數用 %rcx,但 syscall 慣例第 4 個參數改用 %r10(%rcx 會被 syscall 指令破壞)。一般呼叫慣例見 03-Machine-Level-Programs/04-Procedures-and-the-Stack

範例:用 write system-level function 取代 printf 的 hello (p.766):

int main()
{
    write(1, "hello, world\n", 13);  /* arg1: stdout; arg2: 位元組序列; arg3: 位元組數 */
    _exit(0);
}

直接以 syscall 指令實作 (Figure 8.11, code/ecf/hello-asm64.sa):

.section .data
string:
  .ascii "hello, world\n"
string_end:
  .equ len, string_end - string
.section .text
.globl main
main:
  # First, call write(1, "hello, world\n", 13)
  movq $1, %rax        # write is system call 1
  movq $1, %rdi        # Arg1: stdout has descriptor 1
  movq $string, %rsi   # Arg2: hello world string
  movq $len, %rdx      # Arg3: string length
  syscall              # Make the system call
  # Next, call _exit(0)
  movq $60, %rax       # _exit is system call 60
  movq $0, %rdi        # Arg1: exit status is 0
  syscall              # Make the system call
術語 Aside (p.767):各 ISA 手冊常把非同步的叫 "interrupt"、同步的叫 "exception",且沒有統稱。本書以 exception 為總稱,需要時才區分 asynchronous exceptions(interrupts)與 synchronous exceptions(traps, faults, aborts)。讀廠商手冊時注意有些只用 "exception" 指同步事件。

Exam/Test Patterns

情境 / 關鍵字 答案
唯一「非同步」的例外類別? Interrupt(來自外部 I/O 裝置,與特定指令無關);trap/fault/abort 皆同步
哪些類別「一律返回 I_next」? InterruptTrap
哪個類別「可能返回 I_curr(重新執行)」? Fault(修正成功則重執行 faulting instruction,否則 abort)
哪個類別「永不返回」? Abort(致命硬體錯誤,如 parity error)
「蓄意的例外」是哪類?最重要用途? Trap;實作 system call(user program 進入 kernel 的受控入口)
exception table 是誰、何時建立? OS 在系統開機 (boot) 時配置並初始化;entry k 存 handler k 的位址
handler 位址如何形成? exception number 當索引:base register + k × 8(exception table base register)
exception 壓 stack 壓在哪? user → kernel 轉移時,返回位址與 processor state(如 EFLAGS)壓在 kernel stack
exception handler 跑在什麼模式? kernel mode,可存取所有系統資源、執行 privileged instructions
system call 參數傳遞方式? 全走暫存器:%rax=編號,參數依序 %rdi,%rsi,%rdx,%r10,%r8,%r9;%rax 回傳;%rcx,%r11 被破壞
syscall 回傳值在 -4095 到 -1 之間? 表示錯誤,值為 negative errno
程式除以零,shell 印什麼? "Floating exception"(divide error, 例外 #0, Fault,Unix 直接 abort)
程式寫入 read-only text segment,shell 印什麼? "Segmentation fault"(general protection fault, 例外 #13)
page fault 屬哪類?返回行為? Fault(#14);handler 從磁碟載入 page 後重新執行 faulting instruction
x86-64 例外編號 0-31 vs. 32-255? 0-31 由 Intel 定義(所有 x86-64 相同);32-255 由 OS 定義(interrupt/trap)
system call 編號查哪張表? kernel 中的另一張 jump table(offset = syscall 編號),不是 exception table
常考 syscall 編號 read=0, write=1, fork=57, execve=59, _exit=60