文章目录
- 前言
- 实现打印堆栈信息的函数
- 显示堆栈调用信息
- 编译时无法添加-rdynamic选项
- 总结
- 程序源码
前言
关于什么是函数调用堆栈在上篇文章《windows环境下C++代码打印函数堆栈调用情况》中已经介绍过了,简单的来说就是可以展现出函数之间的调用关系,上篇文章展示了如何在windows上打印出函数调用堆栈,其中用到了windows系统上的API,这些接口在linux上是无法使用的,因为工作的关系,也常常需要在linux的调试程序,所以本文介绍一下如何在linux上打印出C++程序的调用堆栈。
实现打印堆栈信息的函数
在linux系统上想打印函数调用堆栈信息,需要引用头文件<execinfo.h>
,然后利用函数backtrace
、backtrace_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。
总结
- linux平台下可以利用函数
backtrace
、backtrace_symbols
、backtrace_symbols_fd
来获取当时的函数调用堆栈信息 - 使用上述函数时,需要引用头文件
<execinfo.h>
,编译时最好加上-rdynamic
选项 - 如果实在无法添加
-rdynamic
,可以通过addr2line
辅助查找问题