呼叫堆疊追蹤與檢查堆疊框(backtrace)
當程式深處出了問題,最迫切的問題是「我們到底是怎麼走到這裡的?」。想像一串電話接力:老闆打給經理、經理打給職員、職員打給櫃台——而現在櫃台卡住了。呼叫堆疊追蹤就是把那整串接力寫下來的版本。它從程式目前所在的最內層函式開始列,一路往外經過每一個呼叫者,直到 main——讓你讀出通向此刻的那一連串呼叫的確切路徑。
具體來說,每一次函式呼叫都會把一個堆疊框推上呼叫堆疊——一個小區塊,存放那次呼叫的區域變數、引數,以及回到它呼叫者的返回位址。呼叫堆疊追蹤不過是把這一疊堆疊框從頂端(你暫停或當掉的地方)走到底端(main)、把每一個都印出來。在 gdb 裡你打「bt」(backtrace),就看到編號的堆疊框:#0 是目前的函式、#1 是它的呼叫者、#2 是那個的呼叫者,依此類推,每個都帶著函式名稱、它的引數與原始碼行號。你接著可以「選取」一個堆疊框來檢查它——gdb 裡的「frame 2」把你的視角移上去到呼叫者 #2,這時「print」就顯示那個堆疊框的區域變數。這讓你能問「呼叫者傳了哪些引數?」與「那個函式做這次呼叫時,它的區域變數是什麼?」,沿著這串往上爬,直到你找出那些值最先出錯的地方。
為何重要:呼叫堆疊追蹤通常是程式當掉或停下來時你得到的單一最有資訊量的東西——它立刻告訴你呼叫路徑,而那往往指向臭蟲附近。技巧是從當掉處往外讀:頂端的堆疊框是症狀出現的地方,但真正的錯誤常常在往下一兩個堆疊框,那裡有一個呼叫者傳了一個壞指標或一個錯誤的大小。誠實的提醒:最佳化的建構可能把函式內聯,使得堆疊框缺失或合併;一個被破壞的堆疊(比方說因為緩衝區溢位)會產生一個亂掉、毫無意義的呼叫堆疊追蹤,而這本身就是記憶體被搞壞的強烈暗示;沒有除錯資訊時,你可能只看到位址或「??」而非名稱。
當掉之後,gdb 的 bt 印出: #0 parse (s=0x0) at parse.c:30 #1 load (path=...) at load.c:12 #2 main () at main.c:5。堆疊框 #0 因為 s 是 0x0 而發生區段錯誤;但 frame 1 揭露了 load() 在把開檔失敗的結果往下傳之前根本沒檢查它——真正的臭蟲在上一個堆疊框。
bt 顯示呼叫鏈;當掉在 #0,但根本成因在 #1。
頂端的堆疊框是症狀浮現的地方,未必是臭蟲所在——往下讀,找出提供那個壞值的呼叫者。一個毫無意義或被截斷的呼叫堆疊追蹤往往意味著堆疊本身被破壞了,那是線索,不是死路。