程序又崩了?一招精准定位段错误!
引言
在C/C++程序开发过程中,是不是经常会遇到这种场景:时间紧迫匆忙上线,程序突然崩溃。开发同事拿到日志,一看无法定位。临近节点快要交付,各方领导在催。加班加点痛苦排查,找出问题羞愧。
程序崩溃不可怕,无从排查才尴尬。特别是后台程序,“噶”的悄无声息,似乎它未曾存在过。
场景列举
问题现场
先举例一种crash
的代码:
void causeSegmentationFault() { int* ptr = nullptr; *ptr = 1; // 这会引发段错误 } int main() { // 触发段错误 causeSegmentationFault(); return 0; }
输出
执行程序能看到crash
提示。
./exe Segmentation fault
分析
从实现上,能够快速的看出访问非法内存,导致crash
。
而在实际项目中,这类骚操作往往藏的很深,且没有关键日志,第一时间很难发现程序崩溃了,以及崩溃的大致范围。
既然问题存在,那如何解决呢?徘徊不定,先问魔镜:
Me: 魔镜啊魔镜,上述代码有什么问题? 魔镜:哈哈哈哈哈哈哈,你的程序崩溃啦。这么愚蠢的写法,真让人笑掉大牙。谁让你用空指针啊,赶紧回炉重造吧~ Me: ??? 给你接的网线,网速过快了吧。 别秀了,说正事。如果是大型项目,运行时出现crash,如何快速且精准地定位问题根源。 魔镜:神曲太魔性了,给我洗脑了都。 运行时出现程序崩溃,通常有三种解决方法: 1. 增加日志记录: 在代码关键位置添加日志,通过检查日志来推测崩溃原因。 2. 生成coredump文件: 配置系统生成coredump文件,并使用gdb分析以定位段错误。 3. 获取异常信号,打印堆栈: 通过注册信号处理函数(如 signal 或 sigaction),在程序崩溃(如段错误、断言失败等)时自动捕获当前的函数调用堆栈,并将其输出到日志或标准输出。
通过与AI
的友好沟通,了解大致有三种常用方案:
- 增加日志记录:
增加大量日志,能够大致定位崩溃的接口。但在实际项目中,崩的点往往前后都没有日志(老泪纵横)。 - 生成coredump文件:
配置生成coredump
文件确实能够通过GDB
快速定位问题。然而,极端情况下,可能会遇到coredump
文件过大或生成不完整,导致无法解析和定位问题。 - 打印崩溃堆栈:
通过捕获异常信号,并打印出堆栈信息。这种方式通过日志就能够大致定位出问题所在,且无副作用,是比较理想的一种方式。
设计方案
通常情况下,比较完备的大型项目以上三种方式应该都会存在。这里简单记录一下如何在程序崩溃时,日志记录当前堆栈信息。
- 注册信号处理函数
使用sigaction()
或signal()
注册信号处理回调,捕获如SIGSEGV
、SIGABRT
等异常信号。 - 将堆栈信息输入到日志
- 在信号处理回调中,通过
backtrace()
获取调用栈帧。 backtrace_symbols()
转换为可读字符串。- 使用
dladdr()
解析符号地址获取更精确的函数名和源文件信息。 - C++ 代码,需通过
abi::__cxa_demangle()
对经过Name Manglin
g 的函数名进行demangle
还原,获取可读的函数签名。
详细设计
实现
实现上比较简单,按照上述描述,代码大致如下:
std::string DumpBacktrace(const int32_t frames) { std::ostringstream oss; void* callStack[frames]; int32_t numFrames = ::backtrace(callStack, frames); char** symbols = ::backtrace_symbols(callStack, numFrames); for (int32_t i = 0; i < numFrames; ++i) { Dl_info info; std::string funcName; if (dladdr(callStack[i], &info)) { int32_t status = -1; char* demangled = abi::__cxa_demangle(info.dli_sname, nullptr, 0, &status); if (demangled != nullptr) { funcName = demangled; free(demangled); } else if (info.dli_sname != nullptr) { funcName = info.dli_sname; } else { funcName = "??"; } } else { funcName = (symbols != nullptr ? symbols[i] : "??"); } oss << "#" << std::setw(2) << i << " " << funcName << " at " << callStack[i] << std::endl; } if (symbols != nullptr) { free(symbols); } if (numFrames >= frames) { oss << "# [truncated]" << std::endl; } return oss.str(); } void signalHandler(int signum) { std::cout << "Caught signal " << signum << ", printing backtrace:" << std::endl; std::string backtrace = DumpBacktrace(20); std::cout << backtrace << std::endl; // 恢复默认信号处理 signal(signum, SIG_DFL); // 重新发送信号以触发默认行为 raise(signum); } void causeSegmentationFault() { int* ptr = nullptr; *ptr = 1; // 这会引发段错误 } int main() { // 注册信号处理函数 signal(SIGSEGV, signalHandler); // 触发段错误 causeSegmentationFault(); return 0; }
输出
程序运行时触发段错误(Signal 11),捕获异常并打印堆栈信息如下:
$ ./exe Caught signal 11, printing backtrace: # 0 DumpBacktrace[abi:cxx11](int) at 0x5615568f8539 # 1 signalHandler(int) at 0x5615568f88dd # 2 ?? at 0x7fbfa33af520 # 3 causeSegmentationFault() at 0x5615568f8978 # 4 main at 0x5615568f89a2 # 5 ?? at 0x7fbfa3396d90 # 6 __libc_start_main at 0x7fbfa3396e40 # 7 _start at 0x5615568f8345 Segmentation fault
通过分析堆栈打印的函数,大致能够定位问题接口causeSegmentationFault
,再结合源码很快能够排查出问题所在。
总结
- 实现崩溃堆栈信息跟踪流程很简单:先捕获异常信号(如
SIGSEGV
、SIGABRT
等),然后在信号处理函数中打印堆栈信息即可。 - 需要注意的是,C++接口存在名称修饰,故需要调用
abi::__cxa_demangle
还原函数名。 - 关于名称修饰,查了一下相关资料,描述如下:
- g++ 编译 C++ 代码:会进行名称修饰,需用 abi::__cxa_demangle 还原。
- g++ 编译 C 代码:若未使用 extern "C",会按 C++ 规则修饰函数名,导致链接错误;添加 extern "C" 后则不会修饰。
- gcc 编译 C 代码:始终不会修饰函数名。
- 注意使用堆栈打印时,编译时需添加
-O0 -g -rdynamic
选项以保留符号信息,确保堆栈中的函数名可被解析。