堆疊指標與框架指標
想像桌上有一根插釘,你把紙條備忘錄一張張疊上去,最新的在最上面。要加一張就把它滑上插釘;要拿就取最上面那張。CPU 正好有這樣一根插釘——記憶體中的呼叫堆疊——而堆疊指標是一個永遠指向這疊頂端的暫存器。隨著函式被呼叫與返回,堆疊會增長與縮減,堆疊指標也隨之移動。
在 x86-64 上,堆疊指標是暫存器 rsp。堆疊習慣上朝較低位址增長,所以推入(push)一個值會把 rsp 減去再寫入該處,彈出(pop)則讀取再加回。每一次函式呼叫、每一個存放在堆疊上的區域變數,都靠 rsp 的移動運作。另一個暫存器——框架指標(x86-64 上的 rbp,也叫基底指標)——通常停在當前函式區段開頭附近的固定位置,並在整個呼叫期間留在那裡。rbp 被釘住後,函式的引數與區域變數就位於相對 rbp 的穩定偏移處(像 rbp-8、rbp-16),即使 rsp 因推入與呼叫不斷移動,每一項仍可用已知的位移量取到。
正是這兩個暫存器讓函式得以擁有私有的區域變數,也讓遞迴得以運作:每次呼叫都拿到自己那片堆疊。現代編譯器常把框架指標最佳化掉(-fomit-frame-pointer 選項),改用 rsp 偏移取到所有東西——這釋放出 rbp 當作額外的通用暫存器,卻讓除錯器的堆疊回溯變得較難。當你在除錯器裡看到呼叫堆疊追蹤時,它正是順著這些指標一框接一框走下去的。
push rax 等於先 sub rsp, 8 再把 rax 寫到 [rsp];若已建立框架,某個區域變數可能以 mov eax, [rbp-4] 讀取。
rsp 追蹤堆疊的即時頂端;rbp 若有使用,則為區域變數與引數釘住一個穩定的參考點。
這個「堆疊」是一塊隨呼叫增減的行程記憶體區域——不是抽象的後進先出堆疊資料結構,儘管行為類似。框架指標是選用的:以 -fomit-frame-pointer 編譯的程式碼沒有以 rbp 為基底的框架,所以別假設每個函式都會建立一個。