Linux教程

Linux下使用backtrace打印函数调用栈信息

本文主要是介绍Linux下使用backtrace打印函数调用栈信息,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Linux下使用backtrace打印函数调用栈信息

Java和Python等语言都有比较简便的方法可以打印函数调用栈,那么在Linux下使用C语言有没有办法呢?
据说有多种方法。本文介绍最基本的方法,即使用 glibc 的 backtrace() 和 backtrace_symbols() 等 API.
在 Linux 下,运行 man 命令可以查看到帮助文档。

man 3 backtrace

文档并不长。下面翻译主要部分如下:

首先,需要包含头文件,并看一下相关的3个API的声明:

#include <exeinfo.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
    backtrace 有 2 个参数,第1个参数实际上是一个 void * 类型的数组,将来所有的 frame 信息都会存在这个数组里;第2个参数 size 的含义是指明该数组的大小。比如,数组大小为5,而实际frames有10层,则只存储最新的 5 个 frames;
    backtrace 的返回值的含义是究竟返回了多少层的 frames,比如,size指定为10,而frames只有5层,则返回5; 若size指定为5,而frames有10层,则也返回5.

  2. backtrace_symbols
    这个函数用来解析每个frame中的函数地址代表的函数名称是什么。它必须要在运行完了上面的 backtrace 函数之后才能用。为什么呢?因为它的第1个参数就是被上面的 backtrace 函数填充了的那个 void * 数组。而它的第2个参数指的是要解析该buffer数组中的几个元素。
    该函数的使用有3个注意点:
    一、它返回的是一个char *数组,代表函数名数组,但是返回的这个 char ** 必须要被调用者 free 掉,而该char * 数组内的每个 char * 是不能被free的;
    二、出错情况下返回 NULL
    三、一般情况下,该函数解析不出函数名;只有在编译的时候加了 “-rdynamic” 选项,将来运行时才能解析出函数名。

  3. backtrace_symbols_fd
    该函数和 backtrace_symbols 类似,都是把函数地址解析成函数名称。不同点在于,它不会返回任何东西,而是把解析出的函数名称写进第3个参数,即一个文件描述符。

  4. 其他
    1> 这3个函数只有 backtrace() 是线程安全函数
    2> 编译器优化,比如 “-O3”, “-O2” 等,可能会导致调用层次丢失(笔者注:后面给一个例子)
    3> inline 函数没有 stack frame ,因此不会被打印出来
    4> Tail-call 优化也会导致函数名称的打印丢失

以下给出一个实例程序:

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

#define MAX_FRAMES 10

int myfunc1(int);
int myfunc2(int);
int myfunc3(int);

void printCallers()
{
    int layers = 0, i = 0;
    char ** symbols = NULL;
    
    void * frames[MAX_FRAMES];
    memset(frames, 0, sizeof(frames));
    layers = backtrace(frames, MAX_FRAMES);
    for (i=0; i<layers; i++) {
        printf("Layer %d: %p\n", i, frames[i]);
    }
    printf("------------------\n");
    
    symbols = backtrace_symbols(frames, layers);
    if (symbols) {
        for (i=0; i<layers; i++) {
            printf("SYMBOL layer %d: %s\n", i, symbols[i]);
        }
         free(symbols);
    }
    else {
        printf("Failed to parse function names\n");
    }
}


int myfunc1(int a)
{
    int b = a + 5;
    int result = myfunc2(b);
    return result;
}

int myfunc2(int b)
{
    int c = b * 2;
    int result = c + myfunc3(c);
    return result;
}

int myfunc3(int c)
{
    int d = c << 2;
    printCallers();
    d = d/0;
    return d;
}

int main()
{
    int result = 0;
    result = myfunc1(1);
    printf("result = %d\n", result);
    
    return 0;
}

这个程序的调用栈是: _start -> _start_main -> main -> myfunc1 -> myfunc2 -> myfunc3 -> printCallers
所以总共是7层.
另外,因为在 myfunc3 调用 printCallers 之后,执行了一句除以0的操作,会导致引起 coredump.

首先,不加任何优化编译:

gcc -rdynamic test.c

运行结果如下:

./a.out
Layer 0: 0x556a4408ab5f
Layer 1: 0x556a4408ac79
Layer 2: 0x556a4408ac4c
Layer 3: 0x556a4408ac27
Layer 4: 0x556a4408aca5
Layer 5: 0x7f4ecbabf34a
Layer 6: 0x556a4408aa3a
------------------
SYMBOL layer 0: ./a.out(printCallers+0x45) [0x556a4408ab5f]
SYMBOL layer 1: ./a.out(myfunc3+0x1e) [0x556a4408ac79]
SYMBOL layer 2: ./a.out(myfunc2+0x1d) [0x556a4408ac4c]
SYMBOL layer 3: ./a.out(myfunc1+0x1e) [0x556a4408ac27]
SYMBOL layer 4: ./a.out(main+0x19) [0x556a4408aca5]
SYMBOL layer 5: /lib64/libc.so.6(__libc_start_main+0xea) [0x7f4ecbabf34a]
SYMBOL layer 6: ./a.out(_start+0x2a) [0x556a4408aa3a]
Floating point exception (core dumped)

由上可见,函数名以及各层函数名都是可以被打印出来的。
运行 gdb ,打印所有的 bt, 然后和上面运行结果中的各个函数地址做对比,就会发现: 基本上上面打印出的几个函数地址都是在 bt 里的。
但 printCallers 的地址不在,这是因为,打印的上述信息的时候,printCallers 正在运行中,而执行除以0语句时,printCallers 已经执行完了,所以core dump文件中不会有这个函数的栈帧。

最后,加一点优化试试:

gcc -O3 -rdynamic test.c

运行结果如下:

./a.out
Layer 0: 0x5593d28eab18
Layer 1: 0x5593d28ea9cb
Layer 2: 0x7f6e0a67034a
Layer 3: 0x5593d28ea9fa
------------------
SYMBOL layer 0: ./a.out(printCallers+0x38) [0x5593d28eab18]
SYMBOL layer 1: ./a.out(main+0xb) [0x5593d28ea9cb]
SYMBOL layer 2: /lib64/libc.so.6(__libc_start_main+0xea) [0x7f6e0a67034a]
SYMBOL layer 3: ./a.out(_start+0x2a) [0x5593d28ea9fa]
Illegal instruction (core dumped)

由以上可见,在编译器做了优化之后,在实际运行中所出现的函数调用栈可能比原先想象中少了几层,更加简单。

最后, 想要用 C 语言在 Linux 下打印调用栈信息,除了本文介绍的方法以外,还可以使用 libunwind.就不在本文介绍范围内了。
此外,如果不是C而是C++程序,则需要做 demangling 处理,也会略有麻烦。

(完)

这篇关于Linux下使用backtrace打印函数调用栈信息的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!