1. 程式人生 > 其它 >【專業技術】在C/C++程式中列印當前函式呼叫棧

【專業技術】在C/C++程式中列印當前函式呼叫棧

前幾天幫同事跟蹤的一個程式莫名退出,沒有core dump(當然ulimit是開啟的)的問題。我們知道,正常情況下,如果程式因為某種異常條件退出的話,應該會產生core dump,而如果程式正常退出的話,應該是直接或者間接的呼叫了exit()相關的函式。

基於這個事實,我想到了這樣一個辦法,在程式開始時,通過系統提供的atexit(),向系統註冊一個回撥函式,在程式呼叫exit()退出的時候,這個回撥函式就會被呼叫,然後我們在回撥函式中打印出當前的函式呼叫棧,由此便可以知道exit()是在哪裡呼叫,從而上述問題便迎刃而解了。上述方法用來解決類似問題是非常行之有效的。

在上面,我提到了在“回撥函式中打印出當前的函式呼叫棧”,相信細心的朋友應該注意到這個了,本文的主要內容就是詳細介紹,如何在程式中列印中當前的函式呼叫棧。

我之前寫過一篇題目為《介紹幾個關於C/C++程式除錯的函式》的文章,看到這裡,請讀者朋友先看一下前面這篇,因為本文是以前面這篇文章為基礎的。我正是用了backtrace()和backtrace_symbols()這兩個函式實現的,下面是一個簡單的例子,通過這個例子我們來介紹具體的方法:

#include<execinfo.h>
#include<stdio.h>
#include<stdlib.h>
voidfun1();
voidfun2();
voidfun3();
voidprint_stacktrace();
intmain()
{
fun3();
}
voidfun1()
{
printf("stackstracebegin:n");
print_stacktrace();
}
voidfun2()
{
fun1();
}
voidfun3()
{
fun2();
}
voidprint_stacktrace()
{
intsize=16;
void*array[16];
intstack_num=backtrace(array,size);
char**stacktrace=backtrace_symbols(array,stack_num);
for(inti=0;i<stack_num;++i)
{
printf("%sn",stacktrace[i]);
}
free(stacktrace);
}

(說明:下面的介紹採用的環境是ubuntu 11.04, x86_64, gcc-4.5.2)

1.通過下面的方式編譯執行:

wuzesheng@ubuntu:~/work/test$gcctest.cc-otest1
wuzesheng@ubuntu:~/work/test$./test1
stackstracebegin:
./test1()[0x400645]
./test1()[0x400607]
./test1()[0x400612]
./test1()[0x40061d]
./test1()[0x4005ed]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xff)[0x7f5c59a91eff]
./test1()[0x400529]

從上面的執行結果中,我們的確看到了函式的呼叫棧,但是都是16進位制的地址,會有點小小的不爽。當然我們可以通過反彙編得到每個地址對應的函式,但這個還是有點麻煩了。不急,且聽我慢慢道來,看第2步。

2. 通過下面的方式編譯執行:

wuzesheng@ubuntu:~/work/test$gcctest.cc-rdynamic-otest2
wuzesheng@ubuntu:~/work/test$./test2
stackstracebegin:
./test2(_Z16print_stacktracev+0x26)[0x4008e5]
./test2(_Z4fun1v+0x13)[0x4008a7]
./test2(_Z4fun2v+0x9)[0x4008b2]
./test2(_Z4fun3v+0x9)[0x4008bd]
./test2(main+0x9)[0x40088d]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xff)[0x7f9370186eff]
./test2()[0x4007c9]

這下終於可以看到函式的名字了,對比一下2和1的編譯過程,2比1多了一個-rdynamic的選項,讓我們來看看這個選項是幹什麼的(來自gcc mannual的說明):

-rdynamic
Passtheflag-export-dynamictotheELFlinker,ontargetsthatsupportit.Thisinstructsthelinkertoaddallsymbols,notonlyusedones,tothedynamicsymboltable.Thisoptionisneededforsomeusesof"dlopen"ortoallowobtainingbacktracesfromwithinaprogram.

從上面的說明可以看出,它的主要作用是讓連結器把所有的符號都加入到動態符號表中,這下明白了吧。不過這裡還有一個問題,這裡的函式名都是mangle過的,需要demangle才能看到原始的函式。關於c++的mangle/demangle機制,不瞭解的朋友可以在搜尋引擎上搜一下,我這裡就不多就介紹了。這裡介紹如何用命令來demangle,通過c++filt命令便可以:

wuzesheng@ubuntu:~/work/test$c++filt<<<"_Z16print_stacktracev"
print_stacktrace()

寫到這裡,大部分工作就ok了。不過不知道大家有沒有想過這樣一個問題,同一個函式可以在程式碼中多個地方呼叫,如果我們只是知道函式,而不知道在哪裡呼叫的,有時候還是不夠方便,bingo,這個也是有辦法的,可以通過address2line命令來完成,我們用第2步中編譯出來的test2來做實驗(address2line的-f選項可以打出函式名, -C選項也可以demangle):

wuzesheng@ubuntu:~/work/test$addr2line-a0x4008a7-etest2-f
0x00000000004008a7
_Z4fun1v
??:0

Oh no,怎麼打出來的位置資訊是亂碼呢?不急,且看我們的第3步。

3. 通過下面的方式編譯執行

wuzesheng@ubuntu:~/work/test$gcctest.cc-rdynamic-g-otest3
wuzesheng@ubuntu:~/work/test$./test3
stackstracebegin:
./test3(_Z16print_stacktracev+0x26)[0x4008e5]
./test3(_Z4fun1v+0x13)[0x4008a7]
./test3(_Z4fun2v+0x9)[0x4008b2]
./test3(_Z4fun3v+0x9)[0x4008bd]
./test3(main+0x9)[0x40088d]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xff)[0x7fa9558c1eff]
./test3()[0x4007c9]
wuzesheng@ubuntu:~/work/test$addr2line-a0x4008a7-etest3-f-C
0x00000000004008a7
fun1()
/home/wuzesheng/work/test/test.cc:20

看上面的結果,我們不僅得到了呼叫棧,而且可以得到每個函式的名字,以及被呼叫的位置,大功告成。在這裡需要說明一下的是,第3步比第2步多了一個-g選項,-g選項的主要作用是生成除錯資訊,位置資訊就屬於除錯資訊的範疇,經常用gdb的朋友相信不會對這個選項感到陌生。

以上轉自羅索實驗室