生活随笔
收集整理的這篇文章主要介紹了
C/C++中手动获取调用堆栈【转】
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
轉自:http://blog.csdn.net/kevinlynx/article/details/39269507
版權聲明:本文為博主原創文章,未經博主允許不得轉載。
當我們的程序core掉之后,如果能獲取到core時的函數調用堆棧將非常有利于定位問題。在Windows下可以使用SEH機制;在Linux下通過gdb使用coredump文件即可。
但有時候由于某些錯誤導致堆棧被破壞,發生拿不到調用堆棧的情況。
一些基礎預備知識本文不再詳述,可以參考以下文章:
需要知道的信息:
- 函數調用對應的call指令本質上是先壓入下一條指令的地址到堆棧,然后跳轉到目標函數地址
- 函數返回指令ret則是從堆棧取出一個地址,然后跳轉到該地址
- EBP寄存器始終指向當前執行函數相關信息(局部變量)所在棧中的位置,ESP則始終指向棧頂
- 每一個函數入口都會保存調用者的EBP值,在出口處都會重設EBP值,從而實現函數調用的現場保存及現場恢復
- 64位機器增加了不少寄存器,從而使得函數調用的參數大部分時候可以通過寄存器傳遞;同時寄存器名字發生改變,例如EBP變為RBP
在函數調用中堆棧的情況可用下圖說明:
將代碼對應起來:
[cpp] view plaincopy
void?g()?{??????int?*p?=?0;??????long?a?=?0x1234;??????printf("%p?%x\n",?&a,?a);??????printf("%p?%x\n",?&p,?p);??????f();??????*p?=?1;??}????void?b(int?argc,?char?**argv)?{??????printf("%p?%p\n",?&argc,?&argv);??????g();??}????int?main(int?argc,?char?**argv)?{??????b(argc,?argv);??????return?0;??}?? 在函數g()中斷點,看看堆棧中的內容(64位機器):
[plain] view plaincopy
(gdb)?p?$rbp??$2?=?(void?*)?0x7fffffffe370??(gdb)?p?&p??$3?=?(int?**)?0x7fffffffe368??(gdb)?p?$rsp??$4?=?(void?*)?0x7fffffffe360??(gdb)?x/8ag?$rbp-16??0x7fffffffe360:?0x1234??0x0??0x7fffffffe370:?0x7fffffffe390??0x400631?<b(int,?char**)+43>??0x7fffffffe380:?0x7fffffffe498??0x1a561cbc0??0x7fffffffe390:?0x7fffffffe3b0??0x40064f?<main(int,?char**)+27>?? 對應的堆棧圖:
可以看看例子中0x400631 <b(int, char**)+43>和0x40064f <main(int, char**)+27>中的代碼:
[plain] view plaincopy
(gdb)?disassemble?0x400631??...??0x0000000000400627?<b(int,?char**)+33>:?callq??0x400468?<printf@plt>??0x000000000040062c?<b(int,?char**)+38>:?callq??0x4005ae?<g()>??0x0000000000400631?<b(int,?char**)+43>:?leaveq???????????????????????????#?call的下一條指令??...????(gdb)?disassemble?0x40064f??...???0x000000000040063f?<main(int,?char**)+11>:??????mov????%rsi,-0x10(%rbp)??0x0000000000400643?<main(int,?char**)+15>:??????mov????-0x10(%rbp),%rsi??0x0000000000400647?<main(int,?char**)+19>:??????mov????-0x4(%rbp),%edi??0x000000000040064a?<main(int,?char**)+22>:??????callq??0x400606?<b(int,?char**)>??0x000000000040064f?<main(int,?char**)+27>:??????mov????$0x0,%eax?????????#?call的下一條指令??...?? 順帶一提,每個函數入口和出口,對應的設置RBP代碼為:
[plain] view plaincopy
(gdb)?disassemble?g??...??0x00000000004005ae?<g()+0>:?????push???%rbp???????????????#?保存調用者的RBP到堆棧??0x00000000004005af?<g()+1>:?????mov????%rsp,%rbp??????????#?設置自己的RBP??...??0x0000000000400603?<g()+85>:????leaveq????????????????????#?等同于:movq?%rbp,?%rsp????????????????????????????????????????????????????????????#?????????popq?%rbp??0x0000000000400604?<g()+86>:????retq???????????????????????? 由以上可見,通過當前的RSP或RBP就可以找到調用堆棧中所有函數的RBP;找到了RBP就可以找到函數地址。因為,任何時候的RBP指向的堆棧位置就是上一個函數的RBP;而任何時候RBP所在堆棧中的前一個位置就是函數返回地址。
由此我們可以自己構建一個導致gdb無法取得調用堆棧的例子:
[cpp] view plaincopy
void?f()?{??????long?*p?=?0;??????p?=?(long*)?(&p?+?1);?????*p?=?0;??}????void?g()?{??????int?*p?=?0;??????long?a?=?0x1234;??????printf("%p?%x\n",?&a,?a);??????printf("%p?%x\n",?&p,?p);??????f();??????*p?=?1;?}????void?b(int?argc,?char?**argv)?{??????printf("%p?%p\n",?&argc,?&argv);??????g();??}????int?main(int?argc,?char?**argv)?{??????b(argc,?argv);??????return?0;??}?? 使用gdb運行該程序:
[plain] view plaincopy
Program?received?signal?SIGSEGV,?Segmentation?fault.??g?()?at?ebp.c:37??37??????????*p?=?1;??(gdb)?bt??Cannot?access?memory?at?address?0x8??(gdb)?p?$rbp??$1?=?(void?*)?0x0?? bt無法獲取堆棧,在函數g()中RBP被改寫為0,gdb從0偏移一個地址長度即0x8,嘗試從0x8內存位置獲取函數地址,然后提示Cannot access memory at address 0x8。
RBP出現了問題,我們就可以通過RSP來手動獲取調用堆棧。因為RSP是不會被破壞的,要通過RSP獲取調用堆棧則需要偏移一些局部變量所占的空間:
[plain] view plaincopy
(gdb)?p?$rsp??$2?=?(void?*)?0x7fffffffe360??(gdb)?x/8ag?$rsp+16?????????????#?g()中局部變量占16字節??0x7fffffffe370:?0x7fffffffe390??0x400631?<b(int,?char**)+43>??0x7fffffffe380:?0x7fffffffe498??0x1a561cbc0??0x7fffffffe390:?0x7fffffffe3b0??0x40064f?<main(int,?char**)+27>??0x7fffffffe3a0:?0x7fffffffe498??0x100000000?? 基于以上就可以手工找到調用堆棧:
[plain] view plaincopy
g()??0x400631?<b(int,?char**)+43>??0x40064f?<main(int,?char**)+27>?? 上面的例子本質上也是破壞堆棧,并且僅僅破壞了保存了的RBP。在實際情況中,堆??赡軙黄茐牡酶?#xff0c;則可能導致手動定位也較困難。
堆棧被破壞還可能導致更多的問題,例如覆蓋了函數返回地址,則會導致RIP錯誤;例如堆棧的不平衡。導致堆棧被破壞的原因也有很多,例如局部數組越界;delete/free棧上對象等。
omit-frame-pointer
使用RBP獲取調用堆棧相對比較容易。但現在編譯器都可以設置不使用RBP(gcc使用-fomit-frame-pointer,msvc使用/Oy),對于函數而言不設置其RBP意味著可以節省若干條指令。在函數內部則完全使用RSP的偏移來定位局部變量,包括嵌套作用域里的局部變量,即使程序實際運行時不會進入這個作用域。
例如:
[cpp] view plaincopy
void?f2()?{??????int?a?=?0x1234;??????if?(a?>?0)?{??????????int?b?=?0xff;??????????b?=?a;??????}??}?? gcc中使用-fomit-frame-pointer生成的代碼為:
[plain] view plaincopy
(gdb)?disassemble?f2??Dump?of?assembler?code?for?function?f2:??0x00000000004004a5?<f2+0>:??????movl???$0x1234,-0x8(%rsp)????#?int?a?=?0x1234??0x00000000004004ad?<f2+8>:??????cmpl???$0x0,-0x8(%rsp)?????????0x00000000004004b2?<f2+13>:?????jle????0x4004c4?<f2+31>????????0x00000000004004b4?<f2+15>:?????movl???$0xff,-0x4(%rsp)??????#?int?b?=?0xff??0x00000000004004bc?<f2+23>:?????mov????-0x8(%rsp),%eax??0x00000000004004c0?<f2+27>:?????mov????%eax,-0x4(%rsp)??0x00000000004004c4?<f2+31>:?????retq?? 可以發現f2()沒有操作RBP之類的指令了。
總結
以上是生活随笔為你收集整理的C/C++中手动获取调用堆栈【转】的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。