ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

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

2021-12-04 23:31:33  阅读:238  来源: 互联网

标签:Layer 函数 int backtrace SYMBOL 函数调用 symbols Linux


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 处理,也会略有麻烦。

(完)

标签:Layer,函数,int,backtrace,SYMBOL,函数调用,symbols,Linux
来源: https://blog.csdn.net/nirendao/article/details/121723711

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有