linux环境下C++代码打印函数堆栈调用情况

前言

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

实现打印堆栈信息的函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#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()中发生问题的时候打印当时的堆栈信息,然后我们编译一下查看运行结果

1
2
3
4
5
6
7
8
9
10
[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来处理,处理之后就可以看到正常的函数名了,具体使用方式如下:

1
2
3
4
5
6
7
8
9
[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选项编译之后的运行结果

1
2
3
4
5
6
7
8
9
10
[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辅助查找问题

程序源码

打印堆栈信息–源码传送门

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客