當我們的程式core掉之後,如果能擷取到core時的函數調用堆棧将非常有利于定位問題。在windows下可以使用seh機制;在linux下通過gdb使用coredump檔案即可。
但有時候由于某些錯誤導緻堆棧被破壞,發生拿不到調用堆棧的情況。
一些基礎預備知識本文不再詳述,可以參考以下文章:
函數調用棧的擷取原理分析
寄存器、函數調用與棧幀
需要知道的資訊:
函數調用對應的<code>call</code>指令本質上是先壓入下一條指令的位址到堆棧,然後跳轉到目标函數位址
函數傳回指令<code>ret</code>則是從堆棧取出一個位址,然後跳轉到該位址
ebp寄存器始終指向目前執行函數相關資訊(局部變量)所在棧中的位置,esp則始終指向棧頂
每一個函數入口都會儲存調用者的ebp值,在出口處都會重設ebp值,進而實作函數調用的現場儲存及現場恢複
64位機器增加了不少寄存器,進而使得函數調用的參數大部分時候可以通過寄存器傳遞;同時寄存器名字發生改變,例如ebp變為rbp
在函數調用中堆棧的情況可用下圖說明:
将代碼對應起來:
在函數<code>g()</code>中斷點,看看堆棧中的内容(64位機器):
對應的堆棧圖:
可以看看例子中<code>0x400631 <b(int, char**)+43></code>和<code>0x40064f <main(int, char**)+27></code>中的代碼:
順帶一提,每個函數入口和出口,對應的設定rbp代碼為:
由以上可見,通過目前的rsp或rbp就可以找到調用堆棧中所有函數的rbp;找到了rbp就可以找到函數位址。因為,任何時候的rbp指向的堆棧位置就是上一個函數的rbp;而任何時候rbp所在堆棧中的前一個位置就是函數傳回位址。
由此我們可以自己建構一個導緻gdb無法取得調用堆棧的例子:
使用gdb運作該程式:
<code>bt</code>無法擷取堆棧,在函數<code>g()</code>中rbp被改寫為0,gdb從0偏移一個位址長度即0x8,嘗試從0x8記憶體位置擷取函數位址,然後提示<code>cannot access memory at address 0x8</code>。
rbp出現了問題,我們就可以通過rsp來手動擷取調用堆棧。因為rsp是不會被破壞的,要通過rsp擷取調用堆棧則需要偏移一些局部變量所占的空間:
基于以上就可以手工找到調用堆棧:
上面的例子本質上也是破壞堆棧,并且僅僅破壞了儲存了的rbp。在實際情況中,堆棧可能會被破壞得更多,則可能導緻手動定位也較困難。
堆棧被破壞還可能導緻更多的問題,例如覆寫了函數傳回位址,則會導緻rip錯誤;例如堆棧的不平衡。導緻堆棧被破壞的原因也有很多,例如局部數組越界;delete/free棧上對象等。