函式序言與函式收尾
多數函式的開頭與結尾都有一小段儀式,與函式計算什麼毫無關係——那是雜務,就像開工前清空工作檯、收工後再整理一番。最開頭那些記帳指令是函式序言,最末尾那些鏡像對稱的指令是收尾。兩者合起來建立與拆除函式的堆疊框:它在堆疊上私有的一片,存放它的區域變數與保存下來的暫存器。
一個經典的 x86-64 序言做三件事:push rbp 把呼叫端的框架指標存上堆疊;mov rbp, rsp 把框架指標停在當前頂端,好讓區域變數能以像 rbp-8 這樣的固定偏移定址;sub rsp, N 透過降低堆疊指標為這個函式的區域變數保留 N 位元組。若函式用到任何被呼叫端保存的暫存器,也會在此推入它們。收尾以相反順序把這一切還原:還原 rsp(常用 mov rsp, rbp 或單純把 N 加回)、彈出保存的暫存器與保存的 rbp,最後 ret 彈出返回位址以跳回呼叫端。單一指令 leave 是 mov rsp, rbp ; pop rbp 那部分的簡寫。
一旦你認得這個樣式,幾乎任何編譯出來之函式的輪廓就會躍然眼前:幾條序言指令、中間是主體、然後是鏡像對稱的收尾與 ret。它也正是呼叫慣例承諾被履行之處——序言保存函式將使用的被呼叫端保存暫存器,收尾再還原它們。經過大量最佳化或極小的葉子函式可能省略其中部分(沒有框架指標、沒有單獨的堆疊保留),所以當一個小函式幾乎沒有序言時,別感到意外。
典型的一個框架:序言 push rbp / mov rbp, rsp / sub rsp, 16,接著是主體,再來是收尾 leave / ret——leave 就是 mov rsp, rbp ; pop rbp。
序言建立堆疊框;收尾拆除它並返回——主體夾在中間。
序言與收尾並非語言所強制,而是編譯器為履行呼叫慣例而產生,所以它們隨最佳化等級與目標而異。以 -fomit-frame-pointer 編譯的葉子函式可能幾乎沒有序言,這會讓依賴框架指標的除錯器回溯陷入困惑。