呼叫慣例
兩個人要能順暢地來回交接工作,前提是對協定有共識:收件匣放哪、事後誰收拾、輸出怎麼標記。呼叫慣例(calling convention)正是函式呼叫的這套約定協定,被白紙黑字寫下來,好讓任意兩段程式碼——即使是相隔多年、由不同工具編譯出來的——都能彼此呼叫而不出錯。它屬於一份更大的協議,稱為 ABI(應用二進位介面)。
呼叫慣例把實務上的問題都釘死。前幾個引數放在哪些暫存器(在 RISC-V 上是 a0 到 a7)?哪個暫存器存放回傳值(a0,第二個回傳值用 a1)?被呼叫的函式必須保持哪些暫存器原封不動(被呼叫者保存,callee-saved),又可以隨意覆寫哪些(呼叫者保存,caller-saved)?堆疊如何對齊、哪個暫存器是堆疊指標?引數放不下暫存器時,多出來的放在堆疊的哪裡?這些答案沒有一個是硬體規定的;它們都是選擇,但一旦為某平台定下來,大家就都得遵守。
呼叫慣例之所以重要,是因為它是軟體生態系的黏著劑。正因為有它,你的程式才能呼叫別人寫好、只以機器碼形式發布的函式庫;C 程式才能呼叫用組語寫的常式;作業系統與應用程式才能互通。一旦違反慣例,哪怕只是細微之處,分別編譯的各段程式碼之間的呼叫就會以令人抓狂的方式破壞暫存器或堆疊。編譯器會自動遵守它;用手寫組語的人則必須親手遵守,而這正是最容易犯下安靜卻毀滅性錯誤的地方之一。
RISC-V 整數呼叫慣例,簡述如下: a0-a7 :前八個引數(a0/a1 同時是回傳值) ra :返回位址 sp :堆疊指標(保持 16 位元組對齊) s0-s11 :被呼叫者保存(函式返回前必須還原它們) t0-t6 :呼叫者保存的暫存暫存器(被呼叫者可隨意覆寫) 遵守這套,你手寫的函式就能直接接進編譯出來的程式碼。
慣例為每個暫存器指派角色;雙方都得遵守,分別建置的程式碼才能互通。
呼叫慣例是軟體上的約定,而非硬體規則。晶片並不強制執行它;除了隨之而來的臭蟲外,沒有什麼能阻止你違反它。