在C/C++程序中打印當前函數調用棧

原文鏈接:https://www.cnblogs.com/zhurizhe/p/3412369.html

在C/C++程序中打印當前函數調用棧

文章轉自:https://www.cnblogs.com/zhurizhe/p/3412369.html

如有侵權請聯繫作者刪除

 前幾天幫同事跟蹤的一個程序莫名退出,沒有core dump(當然ulimit是打開的)的問題。我們知道,正常情況下,如果程序因爲某種異常條件退出的話,應該會產生core dump,而如果程序正常退出的話,應該是直接或者間接的調用了exit()相關的函數。基於這個事實,我想到了這樣一個辦法,在程序開始時,通過系統提供的atexit(),向系統註冊一個回調函數,在程序調用exit()退出的時候,這個回調函數就會被調用,然後我們在回調函數中打印出當前的函數調用棧,由此便可以知道exit()是在哪裏調用,從而上述問題便迎刃而解了。上述方法用來解決類似問題是非常行之有效的。在上面,我提到了在“回調函數中打印出當前的函數調用棧”,相信細心的朋友應該注意到這個了,本文的主要內容就是詳細介紹,如何在程序中打印中當前的函數調用棧。
      我之前寫過一篇題目爲《介紹幾個關於C/C++程序調試的函數》的文章,看到這裏,請讀者朋友先看一下前面這篇,因爲本文是以前面這篇文章爲基礎的。我正是用了backtrace()和backtrace_symbols()這兩個函數實現的,下面是一個簡單的例子,通過這個例子我們來介紹具體的方法:

 
#include <execinfo .h>
#include <stdio .h>
#include <stdlib .h>
 
void fun1();
void fun2();
void fun3();
 
void print_stacktrace();
 
int main()
{
    fun3();
}
 
void fun1()
{
    printf("stackstrace begin:\n");
    print_stacktrace();
}
 
void fun2()
{
    fun1();
}
 
void fun3()
{
    fun2();
}
 
void print_stacktrace()
{
    int size = 16;
    void * array[16];
    int stack_num = backtrace(array, size);
    char ** stacktrace = backtrace_symbols(array, stack_num);
    for (int i = 0; i < stack_num; ++i)
    {
        printf("%s\n", stacktrace[i]);
    }
    free(stacktrace);
}

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

  • 1. 通過下面的方式編譯運行:
 
wuzesheng@ubuntu:~/work/test$ gcc test.cc -o test1
wuzesheng@ubuntu:~/work/test$ ./test1
stackstrace begin:
./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$ gcc test.cc -rdynamic -o test2
wuzesheng@ubuntu:~/work/test$ ./test2
stackstrace begin:
./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
           Pass the flag -export-dynamic to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of "dlopen" or to allow obtaining backtraces from within a program.

      從上面的說明可以看出,它的主要作用是讓鏈接器把所有的符號都加入到動態符號表中,這下明白了吧。不過這裏還有一個問題,這裏的函數名都是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 -a 0x4008a7 -e test2 -f
0x00000000004008a7
_Z4fun1v
??:0

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

  • 3. 通過下面的方式編譯運行:
 
wuzesheng@ubuntu:~/work/test$ gcc test.cc -rdynamic -g -o test3
wuzesheng@ubuntu:~/work/test$ ./test3
stackstrace begin:
./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 -a 0x4008a7 -e test3 -f -C
0x00000000004008a7
fun1()
/home/wuzesheng/work/test/test.cc:20

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章