呼叫者保存與被呼叫者保存暫存器
暫存器數量有限,而一個函式與它所呼叫的函式都想用它們。沒有約定的話,它們會不斷覆寫彼此的值。解法是把暫存器依「誰負責在呼叫前後保住它」分成兩組。想像兩個室友共用一間廚房:有些架子是你的、可以擺亂(用的人在它重要之前自己收拾),有些是共用的、必須隨時保持整潔。
呼叫者保存暫存器(caller-saved,也叫易變的或暫時的)可被被呼叫的函式隨意覆寫。若呼叫者在這種暫存器裡有個呼叫之後還要用的值,那就是呼叫者的責任,要在呼叫前把它存到堆疊、呼叫後再還原。被呼叫者保存暫存器(callee-saved,非易變的、要保存的)則是相反的承諾:被呼叫的函式可以用它們,但前提是先存下舊內容、並在返回前還原,讓呼叫者完全察覺不到。在 RISC-V 上,t 系列暫存器(t0 到 t6)是呼叫者保存,s 系列(s0 到 s11)是被呼叫者保存。
這種劃分之所以重要,是因為它把無謂的保存降到最少。若每個值都總是被保存,呼叫就會慢;若什麼都不保存,值就會憑空消失。劃分讓編譯器把短命的值放進呼叫者保存暫存器(便宜,因為常常根本不必保存),把必須跨越許多次呼叫存活的值放進被呼叫者保存暫存器(在函式進入時保存一次即可)。手寫組語時的誠實陷阱:搞錯某個暫存器屬於哪一組,你就會弄丟一個需要的值,或沒能還原一個你承諾過要還原的值,產生只有跨越呼叫邊界時才會現形的臭蟲。
需要某個值跨越呼叫而存活?把它放進被呼叫者保存暫存器,並把那個暫存器保存一次: myfunc: addi sp, sp, -16 sd ra, 8(sp) sd s0, 0(sp) # 因為我即將跨呼叫使用 s0,先存起來 mv s0, a0 # 把引數安全地留在 s0 jal ra, helper # helper 可能弄壞 t0-t6,但必須保住 s0 add a0, a0, s0 # s0 仍是我保存的值,有保證 ld s0, 0(sp) ld ra, 8(sp) addi sp, sp, 16 ret
呼叫之後還需要的值放進被呼叫者保存暫存器;短命的暫存值放進呼叫者保存的暫存暫存器。
這些名稱講的是「責任歸屬」而非「安不安全」:呼叫者保存的暫存器是「呼叫者若在意就得自己存」;被呼叫者保存的暫存器是「被呼叫者若要用就得自己存」。搞混兩者是經典的 ABI 臭蟲。