异常打印堆栈信息,并查找在代码中的具体位置
异常分为很多种,可以参考下图
打印堆栈信息, 需要编译时增加参数 -rdynamic
打印堆栈信息主要是依靠一下几个函数:
#include <execinfo.h>
int backtrace(void **buffer, int size);
char **backtrace_symbols(void *const *buffer, int size);
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
示例代码如下:
#include <iostream>
#include <execinfo.h>
void test_backtrace(){
const int maxFrames = 100; // 堆栈返回地址的最大个数
void *frame[maxFrames]; // 存放堆栈返回地址
int nptrs = backtrace(frame, maxFrames);
char **strings = backtrace_symbols(frame, nptrs); // 字符串数组
if(strings == nullptr){
return;
}
for(int i = 0; i < nptrs; ++ i){
std::cout << strings[i] << '\n';
}
free(strings);
}
int main(){
test_backtrace();
return 0;
}
终端:
$ g++ -o test.out -rdynamic backtrace.cpp
$ ./test.out
./test.out(_Z14test_backtracev+0x38) [0x7f4819400bb2]
./test.out(main+0x9) [0x7f4819400c5a]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f4818691b97]
./test.out(_start+0x2a) [0x7f4819400a9a]
把 cpp 的函数名转为正常的函数名
#include <cxxabi.h>
char* abi::__cxa_demangle(const char *mangled_name, char *output_buffer, size_t *length, int *status)
示例代码:
void test_backtrace(){
const int maxFrames = 100; // 堆栈返回地址的最大个数
void *frame[maxFrames]; // 存放堆栈返回地址
int nptrs = backtrace(frame, maxFrames);
char **strings = backtrace_symbols(frame, nptrs); // 字符串数组
if(strings == nullptr){
return;
}
size_t len = 256;
char *demangled = static_cast<char*>(::malloc(len)); // 用于存放demangle之后的结果
for(int i = 0; i < nptrs; ++ i){
char *leftPar = nullptr; // 左括号
char *plus = nullptr; // 加号
for(char *p = strings[i]; *p; ++ p){ // 找到左括号和加号的位置,两者之间的内容是需要demangle的
if(*p == '(')
leftPar = p;
else if(*p == '+')
plus = p;
}
*plus = '\0';
int status = 0;
char *ret = abi::__cxa_demangle(leftPar+1, demangled, &len, &status);
*plus = '+';
if(status != 0){
std::cout << strings[i] << '\n';
}
else{
demangled = ret;
std::string stack;
stack.append(strings[i], leftPar+1);
stack.append(demangled);
stack.append(plus);
std::cout << stack << '\n';
}
}
free(demangled);
free(strings);
}
输出:
$ g++ -o test.out -rdynamic backtrace.cpp
$ ./test.out
./test.out(test_backtrace()+0x39) [0x7fb9c34013a3]
./test.out(main+0x9) [0x7fb9c340165f]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7fb9c2671b97]
./test.out(_start+0x2a) [0x7fb9c340128a]
封装为异常类
Exception.h
#ifndef EXCEPTION_H_
#define EXCEPTION_H_
#include <stdexcept>
class Exception : public std::runtime_error
{
public:
Exception(const std::string &what);
~Exception() noexcept override = default;
std::string stack_trace() const noexcept {
return m_stack;
}
private:
void fill_stack_trace();
private:
std::string m_stack;
};
#endif // EXCEPTION_H_
Exception.cpp
#include <cxxabi.h> // __cxa_demangle
#include <execinfo.h> // backtrace backtrace_symbols
#include "Exception.h"
Exception::Exception(const std::string &what) :
std::runtime_error(what)
{
fill_stack_trace();
}
void Exception::fill_stack_trace(){
const int maxFrames = 200; // 返回堆栈框架的最大个数
void *frame[maxFrames]; // 存放堆栈框架的的返回地址
int nptrs = ::backtrace(frame, maxFrames);
char **strings = ::backtrace_symbols(frame, nptrs); // 字符串数组
if(strings == nullptr){
return;
}
size_t len = 256;
char *demangled = static_cast<char*>(::malloc(len)); // 用于存放demangled之后的结果
for(int i = 1; i < nptrs; ++ i){
char *leftPar = nullptr; // 左括号
char *plus = nullptr; // 加号
for(char *p = strings[i]; *p; ++ p){ // 找到左括号和加号的位置,两者之间的内容是需要demangle的
if(*p == '(')
leftPar = p;
else if(*p == '+')
plus = p;
}
*plus = '\0';
int status = 0;
char *ret = abi::__cxa_demangle(leftPar+1, demangled, &len, &status);
*plus = '+';
if(status != 0){
m_stack.append(strings[i]);
}
else{
demangled = ret;
m_stack.append(strings[i], leftPar+1);
m_stack.append(demangled);
m_stack.append(plus);
}
m_stack.push_back('\n');
}
free(demangled);
free(strings);
}
测试代码:
#include <iostream>
#include <tools/base/Exception.h>
class ExceptionTest{
public:
ExceptionTest(){
throw Exception("hello world");
}
};
void test_func(){
ExceptionTest b;
}
int main(){
try{
test_func();
}
catch(Exception &e){
std::cout << e.what() << '\n';
std::cout << e.stack_trace() << std::endl;
}
return 0;
}
输出:
$ g++ -o Exception_test -rdynamic Exception_test.cpp
$ ./Exception_test
hello world
./Exception_test(Exception::Exception(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)+0x4e) [0x7f7e06a020f4]
./Exception_test(ExceptionTest::ExceptionTest()+0x5d) [0x7f7e06a01fe5]
./Exception_test(test_func()+0x23) [0x7f7e06a01ddd]
./Exception_test(main+0x1d) [0x7f7e06a01e11]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f7e05c71b97]
./Exception_test(_start+0x2a) [0x7f7e06a01cda]
参考: https://blog.csdn.net/sdjn_lyn/article/details/103791142
https://stackoverflow.com/questions/8623884/gcc-debug-symbols-g-flag-vs-linkers-rdynamic-option
https://www.cnblogs.com/LiuYanYGZ/p/5550544.html
https://www.cnblogs.com/lidabo/p/4545639.html
查找对应的行
可以使用 addr2line
这个工具来根据对应的地址查找相应的行。但是需要编译的时候,使用 -g
选项。
但是直接使用上面的 0x7f7e06a01fe5
这样的长地址会有问题, addr2line 输出的是 ??:0
,经过对比网上的文档里面的地址都是类似于这样 400526
的地址。
经过长时间搜索发现,短地址那个基本是静态编译,编译时候使用 -static
,输出结果就是短地址了,长地址使用了动态编译。 当然还有另外一种说法是,链接的库使用的是没有符号表的,所以会这样。还有说法是,运行的时候输出的时运行时地址,这个地址是 MMU 处理过的,使用 addr2line
并不是运行时,这个地址已经无效了。
参考: https://my.oschina.net/u/4000302/blog/3081293
https://blog.csdn.net/qq_25333681/article/details/99689959
addr2line
使用参数 -f
显示函数名称, -C
可以解析函数为正常名称,-e
指定执行二进制文件。
readelf -a your_exe | grep your_func
先查看具体函数对应的地址。也可以使用 nm -C your_exe | grep your_func
来查看函数对应的地址。 `objdump -DS your_exe | grep -6 "40a3" 类似这样可以查看具体地址对应的汇编代码。
使用如下:
./Exception(test_func()+0x23) [0x556af2926f4d]
使用 nm -C Exception | grep test_func
00000000000022b4 t _GLOBAL__sub_I__Z9test_funcv
0000000000001f2a T test_func()
然后 0x1F2A + 0x23 = 1F4D, 再用 addr2line -fCe Exception 0x1F4D
就可以看到输出了:
test_func()
/home/ptz/test_cpp/exception/Exception_test.cpp:15
参考: https://ubuntuforums.org/showthread.php?t=2418651
剥离调试信息
可以在本地使用 -g
参数生成带调试信息的二进制文件,发布出去的可以是去除调试信息的,使用命令 objcopy --strip-all exe-debug exe-striped
, 这样当收到错误信息时,直接在本地使用 addr2line
来查找到对应的出错地址。
参考: http://rockhong.github.io/backtrace-and-addr2line.html
还可以考虑使用 libbfd
,但是比较麻烦。 参考: https://blog.csdn.net/crazycoder8848/article/details/51456297
使用信号
在代码里面增加:
#include <csignal>
void sighandler_dump_stack(int sig)
{
psignal(sig, "handler");
//dump_stack();
auto e = Exception("signal exception");
std::cout << "what: " << e.what() << std::endl;
std::cout << "trace: " << e.stack_trace() << std::endl;
signal(sig, SIG_DFL);
raise(sig);
}
void func_c()
{
*((volatile int *)0x0) = 0x9999; /* ERROR */
}
int main(){
if (signal(SIGSEGV, sighandler_dump_stack) == SIG_ERR)
perror("can't catch SIGSEGV");
func_c();
return 0;
}
参考: https://www.cnblogs.com/sunowfox/p/6860588.html
https://cloud.tencent.com/developer/section/1009556
https://blog.csdn.net/gatieme/article/details/84189280
__builtin_return_address(level)
可以输出地址
参数为 0 的时候,打印当前函数,参数为 1,打印调用函数。 参考: http://abcdxyzk.github.io/blog/2013/11/20/lang-c-buildin-addr/
参考:
https://www.runoob.com/cplusplus/cpp-exceptions-handling.html
https://www.cnblogs.com/lidabo/p/3635646.html
https://blog.csdn.net/u014608280/article/details/84974877
https://docs.microsoft.com/zh-cn/cpp/cpp/exception-specifications-throw-cpp?view=msvc-160
https://www.cnblogs.com/yangguang-it/p/6435297.html
https://blog.csdn.net/ab0902cd/article/details/107842903
https://www.cs.usfca.edu/~benson/cs326/pintos/bin/backtrace
https://unix.stackexchange.com/questions/561483/forking-addr2line-on-shared-object-from-application-for-constructing-stack-tra
https://blog.csdn.net/matafeiyanll/article/details/109913503
https://stackoverflow.com/questions/51595071/addr2line-returns-0-even-when-compiled-with-g3-gdb-backtrace-works
https://www.cnblogs.com/sky-heaven/p/9239329.html
https://linxiaohui.gitbooks.io/linux-devp-guide/content/runtime/func.html
https://www.cnblogs.com/arnoldlu/p/11607916.html
https://blog.csdn.net/salmonwilliam/article/details/113977762