C++中backtrace打印函数调用栈callstack-dbg_assert及调用栈解析
1. 使用backtrace()函数获取调用栈,是一个指针数组,返回获取到的调用栈个数,结果放到传入的指针数组里面;
2. 调用backtrace_symbols()把获取的指针数组和数组中调用栈个数传递给该函数,会返回一个新的指针数组,
里面是已经转换成符号表的调用栈信息;用完后记得需要free返回的指针变量指向的内存空间;
使用这两个函数需要包含execinfo.h头文件;
下面把这个过程封装成了一个宏函数方便以后使用;
#ifndef __DEBUG_HPP__
#define __DEBUG_HPP__
#include <iostream>
#include <execinfo.h>
/* include <execinfo.h> to use this macro */
#define DBG_ASSERT(x) do { \
if (x) { break; } \
std::cout << "\r\n------ file:" << __FILE__ << " line:" << __LINE__ << " ------" << std::endl;\
void *pptrace_raw[32] = {0}; \
char **pptrace_str = NULL; \
int trace_num = 0, iloop = 0; \
trace_num = backtrace(pptrace_raw, 32); \
pptrace_str = (char **)backtrace_symbols(pptrace_raw, trace_num); \
for (iloop=0; iloop<trace_num; iloop++) { std::cout << pptrace_str[iloop] << std::endl; }\
if (pptrace_str) { delete pptrace_str; } \
} while (0);
#endif
当应用出现异常的时候可以获取一次调用栈,然后再退出,方便问题分析和定位;
例如当出现段错误时往往会收到SIGSEGV信号,这时可以提前注册接收SIGSEGV信号并打印调用栈,然后再恢复默认信号处理;
/* 1. register handler of signal SIGSEGV */
signal(SIGSEGV, sigsegvhandle);
/* 2. define signal handler and reset signal handler to default at then end of handler function */
void sigsegvhandle(int signo) {
std::cout << "sigsegvhandle received signal: " << signo << std::endl;
/* output callstack */
DBG_ASSERT(0);
/* reset signal handle to default */
signal(signo, SIG_DFL);
/* will receive SIGSEGV again and exit app */
}
应用示例
/*****************************************
* Copyright (C) 2018 * Ltd. All rights reserved.
* Created date: 2018-08-02 19:18:13
*******************************************/
#include <iostream>
#include <signal.h>
#include <execinfo.h>
/* include <execinfo.h> to use this macro */
#define DBG_ASSERT(x) do { \
if (x) { break; } \
std::cout << "###### file:" << __FILE__ << " line:" << __LINE__ << " ######" << std::endl;\
void *pptrace_raw[32] = {0}; \
char **pptrace_str = NULL; \
int trace_num = 0, iloop = 0; \
trace_num = backtrace(pptrace_raw, 32); \
pptrace_str = (char **)backtrace_symbols(pptrace_raw, trace_num); \
for (iloop=0; iloop<trace_num; iloop++) { std::cout << pptrace_str[iloop] << std::endl; } \
if (pptrace_str) { delete pptrace_str; } \
} while (0);
void sigsegv_test()
{
std::cout << __func__ << " begin" << std::endl;
char *buff = NULL;
buff[1] = buff[1]; /* will crash here */
std::cout << __func__ << " end" << std::endl;
}
void sigsegvhandle(int signo) {
std::cout << "sigsegvhandle received signal: " << signo << std::endl;
/* output callstack */
DBG_ASSERT(0);
/* reset signal handle to default */
signal(signo, SIG_DFL);
/* will receive SIGSEGV again and exit app */
}
int main() {
/* register handler of signal SIGSEGV */
signal(SIGSEGV, sigsegvhandle);
sigsegv_test();
return 0;
}
编译执行
# g++ vec.cpp -o vec
# ./vec
后会有以下输出
sigsegv_test begin
sigsegvhandle received signal: 11
###### file:vec.cpp line:33 ######
./vec() [0x400c2a]
/lib/x86_64-linux-gnu/libc.so.6(+0x36cb0) [0x7fcbe0a21cb0]
./vec() [0x400b1f]
./vec() [0x400cfa]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7fcbe0a0cf45]
./vec() [0x400a19]
/bin/bash: line 1: 3331 Segmentation fault ./vec
首先使用nm命令获取应用的符号表
比如以上程序编译为vec,注意nm命令对应编译时使用的gcc/g++工具链
# nm -n vec > vec.symbol.txt
如得到的符号表内容如下
U backtrace@@GLIBC_2.2.5
U backtrace_symbols@@GLIBC_2.2.5
U __cxa_atexit@@GLIBC_2.2.5
w __gmon_start__
U __gxx_personality_v0@@CXXABI_1.3
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
w _Jv_RegisterClasses
U __libc_start_main@@GLIBC_2.2.5
U signal@@GLIBC_2.2.5
U _Unwind_Resume@@GCC_3.0
U _ZdlPv@@GLIBCXX_3.4
U _ZNSolsEi@@GLIBCXX_3.4
U _ZNSolsEPFRSoS_E@@GLIBCXX_3.4
U _ZNSt8ios_base4InitC1Ev@@GLIBCXX_3.4
U _ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4
U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@@GLIBCXX_3.4
U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@@GLIBCXX_3.4
00000000004008c8 T _init
00000000004009f0 T _start
0000000000400a20 t deregister_tm_clones
0000000000400a50 t register_tm_clones
0000000000400a90 t __do_global_dtors_aux
0000000000400ab0 t frame_dummy
0000000000400ada T _Z12sigsegv_testv
0000000000400b50 T _Z13sigsegvhandlei
0000000000400ce2 T main
0000000000400d01 t _Z41__static_initialization_and_destruction_0ii
0000000000400d3e t _GLOBAL__sub_I__Z12sigsegv_testv
0000000000400d60 T __libc_csu_init
0000000000400dd0 T __libc_csu_fini
0000000000400dd4 T _fini
0000000000400de0 R _IO_stdin_used
0000000000400de8 r _ZStL19piecewise_construct
0000000000400e3c r _ZZ12sigsegv_testvE8__func__
0000000000401038 r __FRAME_END__
0000000000601de8 t __frame_dummy_init_array_entry
0000000000601de8 t __init_array_start
0000000000601df8 t __do_global_dtors_aux_fini_array_entry
0000000000601df8 t __init_array_end
0000000000601e00 d __JCR_END__
0000000000601e00 d __JCR_LIST__
0000000000601e08 d _DYNAMIC
0000000000602000 d _GLOBAL_OFFSET_TABLE_
0000000000602090 D __data_start
0000000000602090 W data_start
0000000000602098 D __dso_handle
00000000006020a0 B __bss_start
00000000006020a0 D _edata
00000000006020a0 D __TMC_END__
00000000006020a0 B _ZSt4cout@@GLIBCXX_3.4
00000000006021b0 b completed.6726
00000000006021b1 b _ZStL8__ioinit
00000000006021b8 B _end
然后根据调用栈的地址到符号表地址中查找,
注意符号表中的地址表示某个符号(函数)开始的位置;
如对于 0x400c2a 地址对应的符号应该是
0000000000400b50 T _Z13sigsegvhandlei
因为 0x400c2a 地址落在这两个
0000000000400b50 T _Z13sigsegvhandlei
0000000000400ce2 T main
之间;说明0x400c2a是属于_Z13sigsegvhandlei内的;
以此类推可以得到调用关系,和我们代码的实际调用关系是一致的(由于C++编译时符号表函数名会被修改添加一些参数类型信息);
###### file:vec.cpp line:33 ######
./vec() [0x400c2a] _Z13sigsegvhandlei
/lib/x86_64-linux-gnu/libc.so.6(+0x36cb0) [0x7fcbe0a21cb0]
./vec() [0x400b1f] _Z12sigsegv_testv
./vec() [0x400cfa] main
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7fcbe0a0cf45]
./vec() [0x400a19] _start
如果需要解析库文件中的符号,也可以使用nm命令获取库文件对应的符号表;
不过需要注意的是库文件的符号表示从0开始的,需要获取程序运行时的内存映射关系
可以使用 cat /proc/(程序运行时的PID号)/maps 获取
比如
00400000-00402000 r-xp 00000000 08:31 2885631 /mnt/home_user_tmp/tmp/vec
00601000-00602000 r--p 00001000 08:31 2885631 /mnt/home_user_tmp/tmp/vec
00602000-00603000 rw-p 00002000 08:31 2885631 /mnt/home_user_tmp/tmp/vec
7f52e0f5e000-7f52e1063000 r-xp 00000000 08:01 31195432 /lib/x86_64-linux-gnu/libm-2.19.so
7f52e1063000-7f52e1262000 ---p 00105000 08:01 31195432 /lib/x86_64-linux-gnu/libm-2.19.so
7f52e1262000-7f52e1263000 r--p 00104000 08:01 31195432 /lib/x86_64-linux-gnu/libm-2.19.so
7f52e1263000-7f52e1264000 rw-p 00105000 08:01 31195432 /lib/x86_64-linux-gnu/libm-2.19.so
7f52e1264000-7f52e141e000 r-xp 00000000 08:01 31196721 /lib/x86_64-linux-gnu/libc-2.19.so
7f52e141e000-7f52e161e000 ---p 001ba000 08:01 31196721 /lib/x86_64-linux-gnu/libc-2.19.so
7f52e161e000-7f52e1622000 r--p 001ba000 08:01 31196721 /lib/x86_64-linux-gnu/libc-2.19.so
7f52e1622000-7f52e1624000 rw-p 001be000 08:01 31196721 /lib/x86_64-linux-gnu/libc-2.19.so
7f52e1624000-7f52e1629000 rw-p 00000000 00:00 0
7f52e1629000-7f52e163f000 r-xp 00000000 08:01 31195442 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f52e163f000-7f52e183e000 ---p 00016000 08:01 31195442 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f52e183e000-7f52e183f000 rw-p 00015000 08:01 31195442 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f52e183f000-7f52e1925000 r-xp 00000000 08:01 25297244 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19
7f52e1925000-7f52e1b24000 ---p 000e6000 08:01 25297244 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19
7f52e1b24000-7f52e1b2c000 r--p 000e5000 08:01 25297244 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19
7f52e1b2c000-7f52e1b2e000 rw-p 000ed000 08:01 25297244 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19
7f52e1b2e000-7f52e1b43000 rw-p 00000000 00:00 0
7f52e1b43000-7f52e1b66000 r-xp 00000000 08:01 31195713 /lib/x86_64-linux-gnu/ld-2.19.so
7f52e1d32000-7f52e1d37000 rw-p 00000000 00:00 0
7f52e1d63000-7f52e1d65000 rw-p 00000000 00:00 0
7f52e1d65000-7f52e1d66000 r--p 00022000 08:01 31195713 /lib/x86_64-linux-gnu/ld-2.19.so
7f52e1d66000-7f52e1d67000 rw-p 00023000 08:01 31195713 /lib/x86_64-linux-gnu/ld-2.19.so
7f52e1d67000-7f52e1d68000 rw-p 00000000 00:00 0
7ffe6a82b000-7ffe6a84c000 rw-p 00000000 00:00 0 [stack]
7ffe6a927000-7ffe6a929000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
因此如果需要解析 /lib/x86_64-linux-gnu/libc.so.6(+0x36cb0) [0x7fcbe0a21cb0]
则需要首先减去对应的偏移地址即 0x7fcbe0a21cb0 - 7f52e1264000 然后再到 libc.so.6 对应的符号表中查询;
由于 libc.so.6 不是我们自己编译的库,因此在这里不再继续解析了;
如果是自己编译的库文件则可以继续;
增加一个函数 getmapsinfo(),用于自动获取 maps 信息
需要包含以下头文件
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
用于获取pid及读文件操作;
/*****************************************
* Copyright (C) 2018 * Ltd. All rights reserved.
* Created date: 2018-08-02 19:18:13
*******************************************/
#include <iostream>
#include <signal.h>
#include <execinfo.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
/* include <execinfo.h> to use this macro */
#define DBG_ASSERT(x) do { \
if (x) { break; } \
std::cout << "###### file:" << __FILE__ << " line:" << __LINE__ << " ######" << std::endl;\
void *pptrace_raw[32] = {0}; \
char **pptrace_str = NULL; \
int trace_num = 0, iloop = 0; \
trace_num = backtrace(pptrace_raw, 32); \
pptrace_str = (char **)backtrace_symbols(pptrace_raw, trace_num); \
for (iloop=0; iloop<trace_num; iloop++) { std::cout << pptrace_str[iloop] << std::endl; } \
if (pptrace_str) { delete pptrace_str; } \
} while (0);
void sigsegv_test()
{
std::cout << __func__ << " begin" << std::endl;
char *buff = NULL;
buff[1] = buff[1]; /* will crash here */
std::cout << __func__ << " end" << std::endl;
}
void getmapsinfo()
{
pid_t pid = getpid();
std::string mapsfile = "/proc/";
mapsfile.append(std::to_string(pid));
mapsfile.append("/maps");
std::cout << "##############################################" << std::endl;
std::cout << "pid: " << pid << "maps: " << mapsfile << std::endl;
int fd = open(mapsfile.c_str(), O_RDONLY);
if (fd == -1) {
std::cout << "open error: " << strerror(errno) << std::endl;
return;
}
int len = 0;
char buff[200] = {0};
while ((len = read(fd, buff, sizeof(buff)-1)) > 0) {
buff[len] = '\0';
std::cout << buff;
}
std::cout << "##############################################" << std::endl;
}
void sigsegvhandle(int signo) {
std::cout << "sigsegvhandle received signal: " << signo << std::endl;
/* output callstack */
DBG_ASSERT(0);
getmapsinfo();
/* reset signal handle to default */
signal(signo, SIG_DFL);
/* will receive SIGSEGV again and exit app */
}
int main() {
/* register handler of signal SIGSEGV */
signal(SIGSEGV, sigsegvhandle);
sigsegv_test();
return 0;
}