堆疊指標
想像桌上有根插便條的尖釘:你總是把新便條插在最上面,也總是先拿最上面那張。這種後進先出的疊堆就是堆疊(stack),而程式正是這樣管理函式執行時所需的暫存空間。堆疊位在記憶體中,由單一暫存器——堆疊指標(stack pointer)——記住這堆東西目前的頂端在哪。所有的推入(push)與彈出(pop)都相對於這一個位址進行。
在多數機器上,堆疊往下成長,朝向較小的位址,這感覺有點顛倒,但純粹是個慣例。要推入一個值,先把堆疊指標減去一些(佔下幾個位元組的記憶體騰出空間),再把值存進去。要彈出,就把值載回來,再把堆疊指標加回去以釋放空間。因為堆疊指標永遠標記著已用與未用堆疊記憶體的界線,函式只要在進入時把指標往下移、離開時再移回去,就能安全地為自己的區域變數劃出一塊空間。
堆疊指標之所以重要,是因為它是函式彼此呼叫機制的關鍵樞紐。每一個正在進行的呼叫,都在前一個之下取得自己的一塊堆疊區域(它的堆疊框,stack frame),而堆疊指標讓這些區域不會相撞。如果某個函式在返回前忘了把堆疊指標還原到它原本找到的位置,呼叫者眼中的堆疊就被破壞了,程式通常會當掉。讓堆疊指標保持平衡——進入時往下、離開時恰好移回去——是寫出正確組語的鐵律之一。
推入一個暫存器、稍後再彈出(RISC-V,8 位元組的值): addi sp, sp, -8 # 騰出空間:把堆疊指標往下移 sd ra, 0(sp) # 把返回位址存到堆疊上 ... # 可能會呼叫其他函式的本體 ld ra, 0(sp) # 把它還原 addi sp, sp, 8 # 釋放空間:把堆疊指標移回去
減法是推入(佔用空間),加法是彈出(釋放空間);堆疊指標標記著目前的頂端。
往下成長的慣例雖然常見,卻不是物理定律;不容妥協的是:函式在返回前必須把堆疊指標還原成進入時的值。