C++中backtrace打印函数调用栈callstack-dbg_assert

56 篇文章 3 订阅
38 篇文章 1 订阅

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;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值