天天看點

C/C++中手動擷取調用堆棧C/C++中手動擷取調用堆棧

當我們的程式core掉之後,如果能擷取到core時的函數調用堆棧将非常有利于定位問題。在windows下可以使用seh機制;在linux下通過gdb使用coredump檔案即可。

但有時候由于某些錯誤導緻堆棧被破壞,發生拿不到調用堆棧的情況。

一些基礎預備知識本文不再詳述,可以參考以下文章:

函數調用棧的擷取原理分析

寄存器、函數調用與棧幀

需要知道的資訊:

函數調用對應的<code>call</code>指令本質上是先壓入下一條指令的位址到堆棧,然後跳轉到目标函數位址

函數傳回指令<code>ret</code>則是從堆棧取出一個位址,然後跳轉到該位址

ebp寄存器始終指向目前執行函數相關資訊(局部變量)所在棧中的位置,esp則始終指向棧頂

每一個函數入口都會儲存調用者的ebp值,在出口處都會重設ebp值,進而實作函數調用的現場儲存及現場恢複

64位機器增加了不少寄存器,進而使得函數調用的參數大部分時候可以通過寄存器傳遞;同時寄存器名字發生改變,例如ebp變為rbp

在函數調用中堆棧的情況可用下圖說明:

C/C++中手動擷取調用堆棧C/C++中手動擷取調用堆棧

将代碼對應起來:

在函數<code>g()</code>中斷點,看看堆棧中的内容(64位機器):

對應的堆棧圖:

C/C++中手動擷取調用堆棧C/C++中手動擷取調用堆棧

可以看看例子中<code>0x400631 &lt;b(int, char**)+43&gt;</code>和<code>0x40064f &lt;main(int, char**)+27&gt;</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棧上對象等。

繼續閱讀