生活随笔
收集整理的這篇文章主要介紹了
C/C++中手动获取调用堆栈【转】
小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
轉(zhuǎn)自:http://blog.csdn.net/kevinlynx/article/details/39269507
版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。
當(dāng)我們的程序core掉之后,如果能獲取到core時(shí)的函數(shù)調(diào)用堆棧將非常有利于定位問(wèn)題。在Windows下可以使用SEH機(jī)制;在Linux下通過(guò)gdb使用coredump文件即可。
但有時(shí)候由于某些錯(cuò)誤導(dǎo)致堆棧被破壞,發(fā)生拿不到調(diào)用堆棧的情況。
一些基礎(chǔ)預(yù)備知識(shí)本文不再詳述,可以參考以下文章:
- 函數(shù)調(diào)用棧的獲取原理分析
- 寄存器、函數(shù)調(diào)用與棧幀
需要知道的信息:
- 函數(shù)調(diào)用對(duì)應(yīng)的call指令本質(zhì)上是先壓入下一條指令的地址到堆棧,然后跳轉(zhuǎn)到目標(biāo)函數(shù)地址
- 函數(shù)返回指令ret則是從堆棧取出一個(gè)地址,然后跳轉(zhuǎn)到該地址
- EBP寄存器始終指向當(dāng)前執(zhí)行函數(shù)相關(guān)信息(局部變量)所在棧中的位置,ESP則始終指向棧頂
- 每一個(gè)函數(shù)入口都會(huì)保存調(diào)用者的EBP值,在出口處都會(huì)重設(shè)EBP值,從而實(shí)現(xiàn)函數(shù)調(diào)用的現(xiàn)場(chǎng)保存及現(xiàn)場(chǎng)恢復(fù)
- 64位機(jī)器增加了不少寄存器,從而使得函數(shù)調(diào)用的參數(shù)大部分時(shí)候可以通過(guò)寄存器傳遞;同時(shí)寄存器名字發(fā)生改變,例如EBP變?yōu)镽BP
在函數(shù)調(diào)用中堆棧的情況可用下圖說(shuō)明:
將代碼對(duì)應(yīng)起來(lái):
[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;??}?? 在函數(shù)g()中斷點(diǎn),看看堆棧中的內(nèi)容(64位機(jī)器):
[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>?? 對(duì)應(yīng)的堆棧圖:
可以看看例子中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的下一條指令??...?? 順帶一提,每個(gè)函數(shù)入口和出口,對(duì)應(yīng)的設(shè)置RBP代碼為:
[plain] view plaincopy
(gdb)?disassemble?g??...??0x00000000004005ae?<g()+0>:?????push???%rbp???????????????#?保存調(diào)用者的RBP到堆棧??0x00000000004005af?<g()+1>:?????mov????%rsp,%rbp??????????#?設(shè)置自己的RBP??...??0x0000000000400603?<g()+85>:????leaveq????????????????????#?等同于:movq?%rbp,?%rsp????????????????????????????????????????????????????????????#?????????popq?%rbp??0x0000000000400604?<g()+86>:????retq???????????????????????? 由以上可見(jiàn),通過(guò)當(dāng)前的RSP或RBP就可以找到調(diào)用堆棧中所有函數(shù)的RBP;找到了RBP就可以找到函數(shù)地址。因?yàn)?#xff0c;任何時(shí)候的RBP指向的堆棧位置就是上一個(gè)函數(shù)的RBP;而任何時(shí)候RBP所在堆棧中的前一個(gè)位置就是函數(shù)返回地址。
由此我們可以自己構(gòu)建一個(gè)導(dǎo)致gdb無(wú)法取得調(diào)用堆棧的例子:
[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運(yùn)行該程序:
[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無(wú)法獲取堆棧,在函數(shù)g()中RBP被改寫(xiě)為0,gdb從0偏移一個(gè)地址長(zhǎng)度即0x8,嘗試從0x8內(nèi)存位置獲取函數(shù)地址,然后提示Cannot access memory at address 0x8。
RBP出現(xiàn)了問(wèn)題,我們就可以通過(guò)RSP來(lái)手動(dòng)獲取調(diào)用堆棧。因?yàn)镽SP是不會(huì)被破壞的,要通過(guò)RSP獲取調(diào)用堆棧則需要偏移一些局部變量所占的空間:
[plain] view plaincopy
(gdb)?p?$rsp??$2?=?(void?*)?0x7fffffffe360??(gdb)?x/8ag?$rsp+16?????????????#?g()中局部變量占16字節(jié)??0x7fffffffe370:?0x7fffffffe390??0x400631?<b(int,?char**)+43>??0x7fffffffe380:?0x7fffffffe498??0x1a561cbc0??0x7fffffffe390:?0x7fffffffe3b0??0x40064f?<main(int,?char**)+27>??0x7fffffffe3a0:?0x7fffffffe498??0x100000000?? 基于以上就可以手工找到調(diào)用堆棧:
[plain] view plaincopy
g()??0x400631?<b(int,?char**)+43>??0x40064f?<main(int,?char**)+27>?? 上面的例子本質(zhì)上也是破壞堆棧,并且僅僅破壞了保存了的RBP。在實(shí)際情況中,堆棧可能會(huì)被破壞得更多,則可能導(dǎo)致手動(dòng)定位也較困難。
堆棧被破壞還可能導(dǎo)致更多的問(wèn)題,例如覆蓋了函數(shù)返回地址,則會(huì)導(dǎo)致RIP錯(cuò)誤;例如堆棧的不平衡。導(dǎo)致堆棧被破壞的原因也有很多,例如局部數(shù)組越界;delete/free棧上對(duì)象等。
omit-frame-pointer
使用RBP獲取調(diào)用堆棧相對(duì)比較容易。但現(xiàn)在編譯器都可以設(shè)置不使用RBP(gcc使用-fomit-frame-pointer,msvc使用/Oy),對(duì)于函數(shù)而言不設(shè)置其RBP意味著可以節(jié)省若干條指令。在函數(shù)內(nèi)部則完全使用RSP的偏移來(lái)定位局部變量,包括嵌套作用域里的局部變量,即使程序?qū)嶋H運(yùn)行時(shí)不會(huì)進(jìn)入這個(gè)作用域。
例如:
[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?? 可以發(fā)現(xiàn)f2()沒(méi)有操作RBP之類的指令了。
總結(jié)
以上是生活随笔為你收集整理的C/C++中手动获取调用堆栈【转】的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。