文章目录

  • 前言
  • 实现打印堆栈信息的函数
  • 显示堆栈调用信息
  • 编译时无法添加-rdynamic选项
  • 总结
  • 程序源码


前言

关于什么是函数调用堆栈在上篇文章《windows环境下C++代码打印函数堆栈调用情况》中已经介绍过了,简单的来说就是可以展现出函数之间的调用关系,上篇文章展示了如何在windows上打印出函数调用堆栈,其中用到了windows系统上的API,这些接口在linux上是无法使用的,因为工作的关系,也常常需要在linux的调试程序,所以本文介绍一下如何在linux上打印出C++程序的调用堆栈。

实现打印堆栈信息的函数

在linux系统上想打印函数调用堆栈信息,需要引用头文件<execinfo.h>,然后利用函数backtracebacktrace_symbols来获取当时的函数调用堆栈信息,以下的代码实现了一个简单的打印堆栈新的函数,堆栈深度最大同样设置为12层。

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

#define STACK_INFO_LEN  1024

void ShowTraceStack(const char* szBriefInfo)
{
    static const int MAX_STACK_FRAMES = 12;
    void *pStack[MAX_STACK_FRAMES];
    static char szStackInfo[STACK_INFO_LEN * MAX_STACK_FRAMES];

    char ** pStackList = NULL;
    int frames = backtrace(pStack, MAX_STACK_FRAMES);
    pStackList = backtrace_symbols(pStack, frames);
    if (NULL == pStackList)
        return;

    strcpy(szStackInfo, szBriefInfo == NULL ? "stack traceback:\n" : szBriefInfo);
    for (int i = 0; i < frames; ++i)
    {
        if (NULL == pStackList[i])
            break;

        strncat(szStackInfo, pStackList[i], STACK_INFO_LEN);
        strcat(szStackInfo, "\n");
    }

    printf("%s", szStackInfo); // 输出到控制台,也可以打印到日志文件中
}

void func2()
{
    bool isError = true;
    if (isError)
    {
        ShowTraceStack("error in func2\n");
    }
    else
    {
        printf("this is func2\n");
    }
}

void func1()
{
    int sum = 0;
    for (int i = 0; i < 100; ++i)
        sum += i;

    func2();
}


int main(int argc, char* argv[])
{
    printf("hello world\n");
    func1();

    return 0;
}

显示堆栈调用信息

上面的测试代码中函数的调用逻辑为:main()函数调用func1()函数,然后func1()函数调用func2()函数,当func2()中发生问题的时候打印当时的堆栈信息,然后我们编译一下查看运行结果

[albert@localhost#10:59:03#/home/albert/test/backtrace]$g++ -rdynamic linuxtraceback.cpp -o linuxtraceback
[albert@localhost#10:59:05#/home/albert/test/backtrace]$./linuxtraceback
hello world
error in func2
./linuxtraceback(_Z14ShowTraceStackPKc+0x25) [0x400a19]
./linuxtraceback(_Z5func2v+0x1c) [0x400b06]
./linuxtraceback(_Z5func1v+0x32) [0x400b46]
./linuxtraceback(main+0x1e) [0x400b66]
/lib64/libc.so.6(__libc_start_main+0xfd) [0x7fbbcae43d1d]
./linuxtraceback() [0x400939]

上面的运行结果已经展示了程序函数的调用关系,其中编译选项中的-rdynamic是很重要的,它实际上是一个链接选项,作用是把所有符号(而不仅仅只是程序已使用到的外部符号)都添加到动态符号表里,以便那些通过 dlopen()backtrace()这样的函数使用,换句话说就是如果不加这个选项在调用堆栈中就可能看不到函数名。

上面的调用堆栈中函数名大致能看出来,但是有些奇怪的字母,可以通过工具c++fileter来处理,处理之后就可以看到正常的函数名了,具体使用方式如下:

[albert@localhost#10:59:12#/home/albert/test/backtrace]$./linuxtraceback | c++filt
hello world
error in func2
./linuxtraceback(ShowTraceStack(char const*)+0x25) [0x400a19]
./linuxtraceback(func2()+0x1c) [0x400b06]
./linuxtraceback(func1()+0x32) [0x400b46]
./linuxtraceback(main+0x1e) [0x400b66]
/lib64/libc.so.6(__libc_start_main+0xfd) [0x7f73aea34d1d]
./linuxtraceback() [0x400939]

编译时无法添加-rdynamic选项

如果是自己写的小项目或者小程序,编译选项是可以随便改的,没有什么关系,需要查看堆栈信息加上-rdynamic就可以了,但是如果是公司的大型项目,编译选项是不会随便改的,可能是直接使用automake生成的,先来看一下不添加-rdynamic选项编译之后的运行结果

[albert@localhost#11:22:15#/home/albert/test/backtrace]$g++ linuxtraceback.cpp -o linuxtraceback
[albert@localhost#11:22:18#/home/albert/test/backtrace]$./linuxtraceback
hello world
error in func2
./linuxtraceback() [0x4007d9]
./linuxtraceback() [0x4008c6]
./linuxtraceback() [0x400906]
./linuxtraceback() [0x400926]
/lib64/libc.so.6(__libc_start_main+0xfd) [0x7f4e12ba2d1d]
./linuxtraceback() [0x4006f9]

可以看到不添加-rdynamic选项编译之后运行虽然能显示出调用堆栈,但都是一些函数地址,无法看到函数名,这时可以通过工具addr2line帮助我们定位问题,这个工具的作用就是将函数地址转换成函数所在的行,使用方法就是在命令行运行addr2line 0x4008c6 -e ./linuxtraceback,具体使用时替换函数地址和可运行程序的名字即可

说实话这个小程序中使用运行addr2line 0x4008c6 -e ./linuxtraceback没有看到我想要的,只显示了??:0,仿佛被优化掉了,但是我在正式的项目中使用这个方法是可以得到函数所在行的,这也帮助我查到了一个隐藏很深的BUG。

总结

  1. linux平台下可以利用函数backtracebacktrace_symbolsbacktrace_symbols_fd来获取当时的函数调用堆栈信息
  2. 使用上述函数时,需要引用头文件<execinfo.h>,编译时最好加上-rdynamic选项
  3. 如果实在无法添加-rdynamic,可以通过addr2line辅助查找问题