linux-c編程-棧回溯.md

通常察看函數運行時堆棧的方法是使用 GDB(bt命令) 之類的外部調試器, 可是, 有些時候爲了分析程序的 BUG,(主要針對長時間運行程序的分析),在程序出錯時打印出函數的調用堆棧是很是有用的.linux

1 獲取堆棧信息

在glibc頭文件execinfo.h中聲明瞭三個函數用於獲取當前線程的函數調用堆棧.shell

#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);

使用的時候有幾點須要注意的地方:數組

  • backtrace的實現依賴於棧指針(fp寄存器),在gcc編譯過程當中任何非零的優化等級(-On參數)或加入了棧指針優化參數-fomit-frame-pointer後都將不能正確獲得程序棧信息;
  • backtrace_symbols的實現須要符號表的支持,在gcc編譯過程當中須要加入-rdynamic參數;
  • 內聯函數沒有棧幀,它在編譯過程當中被展開在調用的位置;
  • 尾調用優化(Tail-call Optimization)將複用當前函數棧,而再也不生成新的函數棧,這將致使棧信息不能正確被獲取。

以下對各個函數進行分別介紹和示例框架

1.1. backtrace

int backtrace(void **buffer, int size);

該函數用於獲取當前線程的調用堆棧,函數

獲取的信息將會被存放在buffer中,它是一個指針列表,參數size用來講明buffer數組長度。性能

返回值是實際獲取的指針個數最大不超過size大小.優化

在buffer中的指針實際是從堆棧中獲取的返回地址, 每個堆棧框架有一個返回地址。ui

某些編譯器的優化選項對獲取正確的調用堆棧有干擾,另外內聯函數沒有堆棧框架;刪除框架指針也會致使沒法正確解析堆棧內容

1.2. backtrace_symbols

char **backtrace_symbols(void *const *buffer, int size);

backtrace_symbols將從backtrace函數獲取的信息轉化爲一個字符串數組.線程

參數:buffer是從backtrace函數獲取的指針數組;size是該數組中的元素個數(backtrace的返回值)。指針

返回值指向字符串數組的指針,每一個字符串包含了一個相對於buffer中對應元素的可打印信息。
它包括函數名,函數的偏移地址,和實際的返回地址。

只有使用ELF二進制格式的程序才能獲取函數名稱和偏移地址

可能須要傳遞相應的連接參數,以支持函數名功能

在使用GNU ld連接器的系統中,須要傳遞-rdynamic連接參數,-rdynamic可用來通知連接器將全部符號添加到動態符號表中。

該函數的返回值是經過malloc函數申請的空間,所以調用者必須使用free函數來釋放指針,如不能申請足夠的內存backtrace_symbols將返回NULL。

示例1:

/* gcc backtrace_symbols.c -o backtrace_symbols -rdynamic */

/*
 * #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);
 */

#include <stdio.h>
#include <stdlib.h>
#include <execinfo.h>

/* Obtain a backtrace and print it to @code{stdout}. */
void print_trace(void)
{
    void *array[10];
    size_t size;
    char **strings;
    size_t i;

    size = backtrace(array, 10);
    strings = backtrace_symbols(array, size);
    if (NULL == strings)
    {
        perror("backtrace_symbols");
        exit(EXIT_FAILURE);
    }

    printf("Obtained %zd stack frames.\n", size);

    for (i = 0; i < size; i++)
        printf("%s\n", strings[i]);

    free(strings);
    strings = NULL;
}

/* A dummy function to make the backtrace more interesting. */
void dummy_function(void)
{
    print_trace();
}

int main(void)
{
    dummy_function();
    return 0;
}

執行以下:

$ ./backtrace_symbols
Obtained 5 stack frames.
./backtrace_symbols(print_trace+0x28) [0x4009df]
./backtrace_symbols(dummy_function+0x9) [0x400a99]
./backtrace_symbols(main+0x9) [0x400aa5]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f2126fb5830]
./backtrace_symbols(_start+0x29) [0x400909]

1.3 backtrace_symbols_fd

void backtrace_symbols_fd(void *const *buffer, int size, int fd);

backtrace_symbols_fd 與 backtrace_symbols 函數具備相同的功能, 不一樣的是它不會給調用者返回字符串數組,而是將結果寫入文件描述符爲 fd 的文件中, 每一個函數對應一行.它不須要調用malloc函數,所以適用於有可能調用該函數會失敗的狀況

示例2:

/* gcc backtrace_symbols_fd.c -o backtrace_symbols_fd -rdynamic -Wall */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <execinfo.h>

void demo_fn3(void)
{
    int nptrs;
#define SIZE 100
    void *buffer[SIZE];

    nptrs = backtrace(buffer, SIZE);
    printf("backtrace() returned %d addresses\n", nptrs);

    backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO);
}

static void demo_fn2(void)
{
    demo_fn3();
}

void demo_fn1(int ncalls)
{
    if (ncalls > 1)
        demo_fn1(ncalls - 1);
    else
        demo_fn2();
}

int main(void)
{
    demo_fn1(3);

    return 0;
}

執行以下:

$ ./backtrace_symbols_fd
backtrace() returned 8 addresses
./backtrace_symbols_fd(demo_fn3+0x2e)[0x4008c5]
./backtrace_symbols_fd[0x40091e]
./backtrace_symbols_fd(demo_fn1+0x25)[0x400946]
./backtrace_symbols_fd(demo_fn1+0x1e)[0x40093f]
./backtrace_symbols_fd(demo_fn1+0x1e)[0x40093f]
./backtrace_symbols_fd(main+0xe)[0x400957]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f4b0cbb4830]
./backtrace_symbols_fd(_start+0x29)[0x4007e9]

2. 段錯誤時自動觸發call trace

固然還能夠利用這backtrace來定位段錯誤發生的位置。

一般狀況系, 程序發生段錯誤時系統會發送 SIGSEGV 信號給程序, 缺省處理是退出函數.

咱們可使用 signal(SIGSEGV, &your_function); 函數來接管 SIGSEGV 信號的處理,
程序在發生段錯誤後, 自動調用咱們準備好的函數, 從而在那個函數裏來獲取當前函數調用棧.

/* gcc dump_stack.c -o dump_stack -rdynamic -Wall -g */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <execinfo.h>
#include <signal.h>

#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
void dump_stack(void)
{
    void *array[30] = {0};
    size_t size = backtrace(array, ARRAY_SIZE(array));

    backtrace_symbols_fd(array, size, STDOUT_FILENO);
}

void sig_handler(int sig)
{
    psignal(sig, "handler");
    dump_stack();
    signal(sig, SIG_DFL);
    raise(sig);
}

void demo_fn3(void)
{
    *((volatile int *)0x0) = 0; /* ERROR,SIGSEGV */
}

void demo_fn2(void)
{
    demo_fn3();
}

void demo_fn1(void)
{
    demo_fn2();
}

int main(int argc, const char *argv[])
{
    if (signal(SIGSEGV, sig_handler) == SIG_ERR)
        perror("can't catch SIGSEGV");

    demo_fn1();

    return 0;
}

執行以下:

$ ./dump_stack
handler: Segmentation fault
./dump_stack(dump_stack+0x45)[0x400a5c]
./dump_stack(sig_handler+0x1f)[0x400aba]
/lib/x86_64-linux-gnu/libc.so.6(+0x354b0)[0x7f3440b2a4b0]
./dump_stack(demo_fn3+0x9)[0x400adf]
./dump_stack(demo_fn2+0xe)[0x400af6]
./dump_stack(demo_fn1+0xe)[0x400b07]
./dump_stack(main+0x38)[0x400b42]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f3440b15830]
./dump_stack(_start+0x29)[0x400969]
Segmentation fault (core dumped)

能夠看出, 真正出異常的函數位置在./dump_stack(demo_fn3+0x9)[0x400adf]

可使用addr2line看下這個位置位於哪一行代碼:

$ addr2line -C -f -e  ./dump_stack 0x400adf
demo_fn3
backtrace/dump_stack.c:28

使用objdump也能夠將函數的反彙編信息dump出來。並使用grep顯示地址0x400adf處先後9行的信息

$ objdump -DS ./dump_stack | grep "400adf"
  400adf:       c7 00 00 00 00 00       movl   $0x0,(%rax)
backtrace$ objdump -DS ./dump_stack | grep -9 "400adf"

0000000000400ad6 <demo_fn3>:

void demo_fn3(void)
{
  400ad6:       55                      push   %rbp
  400ad7:       48 89 e5                mov    %rsp,%rbp
    *((volatile int *)0x0) = 0; /* ERROR,SIGSEGV */
  400ada:       b8 00 00 00 00          mov    $0x0,%eax
  400adf:       c7 00 00 00 00 00       movl   $0x0,(%rax)
}
  400ae5:       90                      nop
  400ae6:       5d                      pop    %rbp
  400ae7:       c3                      retq

0000000000400ae8 <demo_fn2>:

void demo_fn2(void)
{
-D參數表示顯示全部彙編代碼

-S 表示將對應的源碼也顯示出來

如上,也能看到出錯行的信息。

3 更低層的函數

只有使用glibc 2.1或更新版本, 可使用backtrace函數, 所以GCC提供了兩個內置函數用來在運行時取得函數調用棧中的返回地址和幀地址。

void *__builtin_return_address(int level);

獲得當前函數層次爲 level 的返回地址, 即此函數被別的函數調用, 而後此函數執行完畢後, 返回, 所謂返回地址就是調用的時候的地址(實際上是調用位置的下一條指令的地址).

void* __builtin_frame_address (unsigned int level);

獲得當前函數的棧幀的地址.

/* gcc builtin_address.c -o builtin_address */

#include <stdio.h>

void show_backtrace(void)
{
    void *ret = __builtin_return_address(1);
    void *caller = __builtin_frame_address(0);

    printf("ret address [%p], call address [%p]\n", ret, caller);
}

void demo_fn2(void)
{
    show_backtrace();
}

void demo_fn1(void)
{
    demo_fn2();
}

int main(void)
{
    demo_fn1();

    return 0;
}

執行以下:

$ ./builtin_address
ret address [0x400551], call address [0x7ffed99b01c0]

這兩個宏有兩個很致命的問題:

  • 參數不能使用變量;
  • 沒法知道調用棧啥時候到頭了

4. libunwind庫使用

libunwind是目前比較流行的方案,只須要一個函數show_backtrace便可,參考代碼以下:

/* gcc libunwind.c -o libunwind -lunwind -Wall -g */

#include <stdio.h>      // printf
#include <signal.h>

#define UNW_LOCAL_ONLY  // We only need local unwinder.
#include <libunwind.h>

void show_backtrace(void)
{
    unw_cursor_t cursor;
    unw_context_t uc;
    // char buf[4096];

    unw_getcontext(&uc);            // store registers
    unw_init_local(&cursor, &uc);   // initialze with context

    while (unw_step(&cursor) > 0) { // unwind to older stack frame
        char buf[4096];
        unw_word_t offset;
        unw_word_t ip, sp;
        
        // read register, rip
        unw_get_reg(&cursor, UNW_REG_IP, &ip);
        
        // read register, rbp
        unw_get_reg(&cursor, UNW_REG_SP, &sp);
        
        // get name and offset
        unw_get_proc_name(&cursor, buf, sizeof(buf), &offset);
        
        // x86_64, unw_word_t == uint64_t
        printf("0x%016lx <%s+0x%lx>\n", ip, buf, offset);
    }
}

void sig_handler(int sig)
{
    psignal(sig, "handler");
    show_backtrace();
    signal(sig, SIG_DFL);
    raise(sig);
}

void demo_fn3(void)
{
    *((volatile int *)0x0) = 0; /* ERROR,SIGSEGV */
}

void demo_fn2(void)
{
    demo_fn3();
}

void demo_fn1(void)
{
    demo_fn2();
}

int main(void)
{
    if (signal(SIGSEGV, sig_handler) == SIG_ERR)
        perror("can't catch SIGSEGV");
    
    demo_fn1();

    return 0;
}

執行以下:

$ ./libunwind
handler: Segmentation fault
0x00005644ef805b8a <sig_handler+0x21>
0x00007f68ed646f20 <killpg+0x40>
0x00005644ef805baf <demo_fn3+0x9>
0x00005644ef805bc1 <demo_fn2+0x9>
0x00005644ef805bcd <demo_fn1+0x9>
0x00005644ef805bfc <main+0x2c>
0x00007f68ed629b97 <__libc_start_main+0xe7>
0x00005644ef80596a <_start+0x2a>
Segmentation fault

每次使用cursor回溯一幀,直到沒有可用的父棧幀。

使用addr2line查看出錯行以下:

$ addr2line -C -f -e  ./libunwind 0x55d61dfaebaf
??
??:0
$ addr2line -C -f -e  ./libunwind 0xbaf
demo_fn3
/home/rlk/codes/libunwind.c:47

如上,因爲偏移地址是比較小的值,而堆棧中的比較大,所以可適當截掉高位地址。

再用objdump試試結果如何:

$ objdump -DS ./libunwind | grep -6 "baf"
void demo_fn3(void)
{
 ba6:   55                      push   %rbp
 ba7:   48 89 e5                mov    %rsp,%rbp
    *((volatile int *)0x0) = 0; /* ERROR,SIGSEGV */
 baa:   b8 00 00 00 00          mov    $0x0,%eax
 baf:   c7 00 00 00 00 00       movl   $0x0,(%rax)
}
 bb5:   90                      nop
 bb6:   5d                      pop    %rbp
 bb7:   c3                      retq

0000000000000bb8 <demo_fn2>:

如上,也能正確找到出錯位置,但偏移地址也應該試試低位地址。

值得一提的是,代碼中經過函數地址獲取函數名稱的地方是比較耗時的,因此每次採樣都作這個操做是會嚴重影響程序的執行效率。所以使用這種方法作性能分析時是比較耗時的。

email: MingruiZhou@outlook.com

相關文章
相關標籤/搜索