呼叫堆疊
函式呼叫其他函式,後者又呼叫更多——而每個呼叫都必須記住該回到哪裡,並保有自己私有的工作空間。呼叫堆疊(call stack)就是替這一切記帳的記憶體區域。想像一疊盤子:每次函式呼叫都在最上面加一個盤子,函式結束時它的盤子就被拿走——後放的先拿。
每次呼叫都把一個堆疊框(stack frame)推上堆疊:一塊保存該呼叫的區域變數、引數,以及「返回位址」(指出要回到呼叫者的何處繼續)的區塊。CPU 用一個堆疊指標暫存器追蹤頂端(x86-64 上是 rsp);呼叫函式會移動堆疊指標以騰出空間,返回則把它移回去。因為最近被呼叫的函式總是最先結束,這種後進先出的紀律恰好對應巢狀呼叫展開的方式。在大多數系統上堆疊向下長——朝較低的位址——所以「推入」一個框其實是從堆疊指標減去。
兩個誠實的點。第一,這裡的「堆疊」是一塊隨呼叫長大縮小的行程記憶體區域——要和堆疊資料結構(抽象的後進先出容器)區分開來;這個區域是以它遵循的紀律來命名的。第二,堆疊是有限的(常為數 MiB),所以無止境的遞迴或一個巨大的區域陣列可能跑出邊界,造成堆疊溢位(stack overflow),通常以當掉回報。堆疊記憶體很快、且在返回時自動回收,這也正是為什麼回傳一個指向區域變數的指標是個臭蟲。
main() 呼叫 f()、f() 呼叫 g():堆疊上有三個框,g 的在最上面。g 返回時它的框被彈出(堆疊指標往回移),控制權回到 f 中 g 的返回位址處繼續,依此一路回到 main。
框隨呼叫巢狀而疊起、隨返回而彈出——後進先出。
呼叫堆疊(一塊記憶體區域)不是堆疊資料結構(抽象的後進先出容器)——這個區域只是以它所遵循的後進先出紀律命名。堆疊有限;失控的遞迴會把它撐爆。那裡的記憶體在返回時自動回收,所以指向區域變數的指標,會在函式一退出的瞬間就懸置。