程序又崩了?一招精准定位段错误!
[TOC]
引言
在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
选项以保留符号信息,确保堆栈中的函数名可被解析。