Linux調用backtrack函數打印程序崩潰時的調用堆棧

轉自:http://blog.sina.com.cn/s/blog_6e2282880100wlrt.html

可以給自己的程序都加上這個東西,便於快速的找到錯誤吧,看到別人都是這麼用的
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <signal.h>


//signal 函數用法參考http://www.kernel.org/doc/man-pages/online/pages/man2/signal.2.html
//backtrace ,backtrace_symbols函數用法參考 http://www.kernel.org/doc/man-pages/online/pages/man3/backtrace.3.html

static void WidebrightSegvHandler(int signum) {
    void *array[10];
    size_t size;
    char **strings;
    size_t i, j;

    signal(signum, SIG_DFL);

    size = backtrace (array, 10);
    strings = (char **)backtrace_symbols (array, size);

    fprintf(stderr, "widebright received SIGSEGV! Stack trace:\n");
    for (i = 0; i < size; i++) {
        fprintf(stderr, "%d %s \n",i,strings[i]);
    }

    free (strings);
    exit(1);
}

int invalide_pointer_error(char * p)
{
    *p = 'd'; //讓這裏出現一個訪問非法指針的錯誤
    return 0;
}


void error_2(char * p)
{
    invalide_pointer_error(p);
}

void error_1(char * p)
{
     error_2(p);
}

void error_0(char * p)
{
     error_1(p);
}





int main() 
{

    //設置 信好的處理函數,各種 信號的定義見http://www.kernel.org/doc/man-pages/online/pages/man7/signal.7.html
    signal(SIGSEGV, WidebrightSegvHandler); // SIGSEGV      11       Core    Invalid memory reference
    signal(SIGABRT, WidebrightSegvHandler); // SIGABRT       6       Core    Abort signal from


    char *a = NULL;
    error_0(a);
    exit(0);

}

widebright@widebright:~/桌面$ gcc main.c 
widebright@widebright:~/桌面$ ./a.out 
widebright received SIGSEGV! Stack trace:
0 ./a.out [0x8048580] 
1 [0xb807a400] 
2 ./a.out [0x8048636] 
3 ./a.out [0x8048649] 
4 ./a.out [0x804865c] 
5 ./a.out [0x80486a9] 
6 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5) [0xb7f19775] 

然後爲了定位錯誤,我們需要加上-g參數重新編譯一個帶調試信息的版本
widebright@widebright:~/桌面$ gcc -g main.c 
widebright@widebright:~/桌面$ ./a.out 
widebright received SIGSEGV! Stack trace:
0 ./a.out [0x8048580] 
1 [0xb7fb3400] 
2 ./a.out [0x8048636] 
3 ./a.out [0x8048649] 
4 ./a.out [0x804865c] 
5 ./a.out [0x80486a9] 
6 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5) [0xb7e52775] 
7 ./a.out [0x80484c1] 

加上-rdynamic 參數的話,輸出的符號更清楚一些,不過好像地址不一樣了。
widebright@widebright:~/桌面$ gcc -g -rdynamic main.c 
widebright@widebright:~/桌面$ ./a.out 
widebright received SIGSEGV! Stack trace:
0 ./a.out [0x8048840] 
1 [0xb7f3d400] 
2 ./a.out(error_2+0x11) [0x80488f6] 
3 ./a.out(error_1+0x11) [0x8048909] 
4 ./a.out(error_0+0x11) [0x804891c] 
5 ./a.out(main+0x4b) [0x8048969] 
6 /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe5) [0xb7ddc775] 
7 ./a.out [0x8048781] 


可以看到有調試信息的時候,錯誤是一樣的。然後就可以用gdb定位和調試錯誤了:
-----------------------
(gdb) info line *0x8048580
Line 19 of "main.c" starts at address 0x804856d <WidebrightSegvHandler+25>
   and ends at 0x8048583 <WidebrightSegvHandler+47>.
(gdb) list *0x8048580
0x8048580 is in WidebrightSegvHandler (main.c:19).
14        char **strings;
15        size_t i, j;
16    
17        signal(signum, SIG_DFL);
18    
19        size = backtrace (array, 10);
20        strings = (char **)backtrace_symbols (array, size);
21    
22        fprintf(stderr, "widebright received SIGSEGV! Stack trace:\n");
23        for (i = 0; i < size; i++) {
-----------------
(gdb) list *0x8048636
0x8048636 is in error_2 (main.c:41).
36    
37    
38    void error_2(char * p)
39    {
40        invalide_pointer_error(p);
41    }
42    
43    void error_1(char * p)
44    {
45         error_2(p);
--------------
(gdb) list *0x8048649
0x8048649 is in error_1 (main.c:46).
41    }
42    
43    void error_1(char * p)
44    {
45         error_2(p);
46    }
47    
48    void error_0(char * p)
49    {
50         error_1(p);

=============
(gdb) br main.c:40
Breakpoint 1 at 0x804862b: file main.c, line 40.
(gdb) run
Starting program: /home/widebright/桌面/a.out 

Breakpoint 1, error_2 (p=0x0) at main.c:40
40        invalide_pointer_error(p);
(gdb) stepi
0x0804862e    40        invalide_pointer_error(p);
(gdb) stepi
0x08048631    40        invalide_pointer_error(p);
(gdb) stepi
invalide_pointer_error (p=0x0) at main.c:32
32    {
(gdb) stepi
0x08048616    32    {
(gdb) stepi
33        *p = 'd'; //讓這裏出現一個訪問非法指針的錯誤
(gdb) stepi
0x0804861b    33        *p = 'd'; //讓這裏出現一個訪問非法指針的錯誤
(gdb) stepi

Program received signal SIGSEGV, Segmentation fault.
0x0804861b in invalide_pointer_error (p=0x0) at main.c:33
33        *p = 'd'; //讓這裏出現一個訪問非法指針的錯誤

(gdb) print p
$1 = 0x0
(gdb) print *p
Cannot access memory at address 0x0




===============================================
好像使用    
    int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
http://www.kernel.org/doc/man-pages/online/pages/man2/sigaction.2.html
這個函數註冊信號的處理函數的話,可以得到更多的信息,比如出錯 時候的寄存器的值等等。
因爲他函數 最後一個參數傳過來一個ucontext_t *ucontext 的指針
可以看到 “善用backtrace解決大問題” http://blog.chinaunix.net/u/3425/showart_263408.html 這個網頁上有給出一個例子。


最初看到這個用法的的在redhat的安裝程序的anaconda裏面的。


------------------------
關於backtrack的原理 的解釋,參考這個:
從別人blog上拷來的,地址:http://blog.csdn.net/absurd/archive/2005/12/13/551585.aspx 

開發嵌入式軟件通常是比較麻煩的事,一些常用的工具往往無法使用,在開發PC軟件時簡單的任務,此時變得很複雜。今天就遇到了這樣一件事,折騰了幾個小時,僅僅是爲知道call stack。 

我編譯了一個程序放到PDA(ARM9+LINUX+UCLIBC)上面運行,出現了一個ASSERT,並顯示了文件名和行號,原來是調用了一個沒有實現 的函數,我很想知道是誰調用了它,這看似簡單的問題卻讓我很頭疼,如果有gdb,那好辦-用bt命令就可以搞定,如果用的libc,那也好辦-用 backtrace函數就可以搞定,問題是兩者都沒有。 

想來想去只有自己寫一個backtrace,要實現這個功能並不難,如果我們知道調用堆棧的格式,就可以很容易取出上層調用者的指令地址,有了這些上層調用者的指令地址,我們可以通過MAP文件找到指令地址對應的源文件名和行號。 

下面簡要介紹一下實現原理: 

要獲得調用者的地址,有必要介紹一下堆棧的格式: 

+---------------------------+ (高地址) 
+_參數1__________+ 
+---------------------------+ 
+_參數2__________+ 
+---------------------------+ 參數的順序依賴於調用方式 
+_參數.__________+ 
+---------------------------+ 
+_參數N__________+ 
+---------------------------+ 
+_eip____________+ 返回本次調用後,下一條指令的地址 
+----------------------------+ 
+_ebp____________+ 這裏保存的調用者的ebp 
+----------------------------+ 
(ebp 指向這裏:相當於調用者和被調用者的分界線) 
+----------------------------+ 
+_臨時變量1_______+ 
+----------------------------+ 
+_臨時變量2_______+ 
+----------------------------+ 
+_臨時變量.________+ 
+----------------------------+ 
+----------------------------+ 
+_臨時變量N_______+ 
+----------------------------+(低地址) 
由於優化、調用方式、編譯器的不同,上述佈局部可能有所不同,但一般來說,第一個局部變量前是調用者的ebp,ebp前是返回後下一條指令的地址。 

知道了這個結構,要獲得上層調用的者指令地址就容易了,我們可以用如下代碼模擬glibc提供的backtrace的功能: 
int backtrace (void **BUFFER, int SIZE) 
int n = 0; 
int *p = &n; 
int i = 0; 

int ebp = p[1]; 
int eip = p[2]; 

for(i = 0; i < SIZE; i++) 
BUFFER[i] = (void*)eip; 
p = (int*)ebp; 
ebp = p[0]; 
eip = p[1]; 

return SIZE; 

附: 
通過addr2line可以找到地址對應的文件名和行號,不用手動去查MAP文件了。 


=======================
windows系統上面要實現同樣的功能,可能要調用Debug Help Library 裏面的StackWalk64 等函數。
http://msdn.microsoft.com/en-us/library/ms680650(VS.85).aspx


找到一個使用StackWalk64 的例子http://www.cppblog.com/kevinlynx/archive/2008/03/28/45628.html
這裏又是一個模擬backtrace(stackwalk)函數的例子
http://www.cnblogs.com/lbq1221119/archive/2008/04/18/1159956.html


其實你可以在程序的任何地方調用backtrace和 stackwalk函數的,呵呵


//////////////////another example
//funstack.c
#define _GNU_SOURCE
#include <memory.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <ucontext.h>
#include <dlfcn.h>
#include <execinfo.h>

#if defined(REG_RIP)
# define SIGSEGV_STACK_IA64
# define REGFORMAT "6lx"
#elif defined(REG_EIP)
# define SIGSEGV_STACK_X86
# define REGFORMAT "x"
#else
# define SIGSEGV_STACK_GENERIC
# define REGFORMAT "%x"
#endif

static void signal_segv(int signum, siginfo_t* info, void*ptr) {
        static const char *si_codes[3] = {"", "SEGV_MAPERR", "SEGV_ACCERR"};

        size_t i;
        ucontext_t *ucontext = (ucontext_t*)ptr;

#if defined(SIGSEGV_STACK_X86) || defined(SIGSEGV_STACK_IA64)
        int f = 0;
        Dl_info dlinfo;
        void **bp = 0;
        void *ip = 0;
#else
        void *bt[20];
        char **strings;
        size_t sz;
#endif

#if defined(SIGSEGV_STACK_X86) || defined(SIGSEGV_STACK_IA64)
# if defined(SIGSEGV_STACK_IA64)
        ip = (void*)ucontext->uc_mcontext.gregs[REG_RIP];
        bp = (void**)ucontext->uc_mcontext.gregs[REG_RBP];
# elif defined(SIGSEGV_STACK_X86)
        ip = (void*)ucontext->uc_mcontext.gregs[REG_EIP];
        bp = (void**)ucontext->uc_mcontext.gregs[REG_EBP];
# endif

        fprintf(stderr, "Stack trace:\n");
        while(bp && ip) {
                if(!dladdr(ip, &dlinfo))
                        break;

                const char *symname = dlinfo.dli_sname;

                fprintf(stderr, "% 2d: %p %s+%u (%s)\n",
                                ++f,
                                ip,
                                symname,
                                (unsigned)(ip - dlinfo.dli_saddr),
                                dlinfo.dli_fname);

                if(dlinfo.dli_sname && !strcmp(dlinfo.dli_sname, "main"))
                        break;

                ip = bp[1];
                bp = (void**)bp[0];
        }
#else
        fprintf(stderr, "Stack trace (non-dedicated):\n");
        sz = backtrace(bt, 20);
        strings = backtrace_symbols(bt, sz);

        for(i = 0; i < sz; ++i)
                fprintf(stderr, "%s\n", strings[i]);
#endif
        fprintf(stderr, "End of stack trace\n");
        return;
}
int setup_sigsegv() {
        struct sigaction action;
        memset(&action, 0, sizeof(action));
        action.sa_sigaction = signal_segv;
        action.sa_flags = SA_SIGINFO;
        if(sigaction(SIGUSR1, &action, NULL) < 0) {
                perror("sigaction");
                return 0;
        }

        return 1;
}



void func1()
{
        raise(SIGUSR1);
        return ;

}
void func2()
{
        raise(SIGUSR1);
        return ;

}

void entry()
{
        func1();
        func2();
        return;
}
int main()
{
        setup_sigsegv();
        entry();
}

//////////
這問題C版ms討論了很多次了,info gcc  
__builtin_return_address
__builtin_frame_address
還有一個相關的, info libc
glibc中,
backtrace
backtrace_symbols

///////////////////////

在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>
 
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的朋友相信不會對這個選項感到陌生。

///////////////////////////////////////////////////

介紹幾個關於C/C++程序調試的函數


最近調試程序學到的幾個挺有用的函數,分享一下,希望對用C/C++的朋友有所幫助!

1. 調用棧系列
下面是函數原型:

1 2 3 4
#include "execinfo .h" int backtrace(void **buffer, int size); char **backtrace_symbols(void*const *buffer, int size); void backtrace_symbols_fd(void *const *buffer, int size, int fd);

接下來,對上面三個函數進行介紹:
(1)backtrace用來獲得當前程序的調用棧,把結果存在buffer中。通常,我們用gdb調試程序,設置合適的斷點,停下來之後,用backtrace(bt)命令,就可以看到當前的調用棧。但是,有的時候,用到條件斷點的時候,gdb的功能就沒有程序本身的強大了,這個時候,可以考慮在程序中調用backtrace函數,來獲取調用棧。
(2)backtrace_symbols把用backtrace獲取的調用棧轉換成字符串數組,以字符串數組的形式返回,使用者需要在外面釋放返回的字符串數組所佔用的內存
(3)backtrace_symbols_fd把用backtrace獲取的調用棧信息寫到fd所指定的文件中

1
void * __builtin_return_address (unsigned int level)

這個函數用來得到當前函數,或者調用它的函數的返回地址,得到這個地址後,通過gdb反彙編,便可得到調用函數相關的信息,這也是在應用中獲取調用棧的一種方法。

2. 內存分配、釋放系列

1 2
#include "malloc .h" size_t malloc_usable_size((void *__ptr));

這個函數的用法是返回調用malloc後實際分配的可用內存的大小。我們都知道在C++中,operator new()可以重載各種各樣的版本,可以傳入調用時的相關信息來跟蹤內存分配情況,但是operator delete()卻只有兩種形式,不能隨意重載,尤其是全局的operator delete(),只有一種版本,這個時候就比較痛苦了,究竟釋放了多少內存呢? 這時候malloc_usable_size()這個函數就有用武之地了,調用它便可以獲取當前釋放的內存的大小。這裏需要注意的是,如果在delete中用了malloc_usable_size來計算釋放的內存大小,那麼在new中也請用它來統計開闢的內存,這樣才能對應起來。因爲在調用malloc時,很多時候實際分配的內存會比用戶申請的要大一些,所以如果兩邊的統計方法對應不起來的話,統計結果也會有比較大的判別。
這裏關於new/delete重載的一些細節我不做說明,之前我寫過一篇文章的,不明白的朋友可以去看一下這篇文章《細說C++中的new與delete》。


發佈了50 篇原創文章 · 獲贊 7 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章