程序又崩了?一招精准定位段错误!

简介: 在C/C++开发中,程序崩溃(如段错误)是常见问题,但快速定位崩溃原因却颇具挑战。本文介绍了一种精准定位崩溃问题的方法:通过捕获异常信号(如SIGSEGV),结合`backtrace()`和`abi::__cxa_demangle()`打印堆栈信息,从而快速定位问题接口。相比增加日志或生成coredump文件,此方法更高效且无副作用。实现时需注意编译选项(如`-O0 -g -rdynamic`)以保留符号信息,并处理C++名称修饰问题。

程序又崩了?一招精准定位段错误!



引言

  在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文件过大或生成不完整,导致无法解析和定位问题。
  • 打印崩溃堆栈:
    通过捕获异常信号,并打印出堆栈信息。这种方式通过日志就能够大致定位出问题所在,且无副作用,是比较理想的一种方式。

设计方案

  通常情况下,比较完备的大型项目以上三种方式应该都会存在。这里简单记录一下如何在程序崩溃时,日志记录当前堆栈信息。

  1. 注册信号处理函数
    使用 sigaction()signal() 注册信号处理回调,捕获如SIGSEGVSIGABRT等异常信号。
  2. 将堆栈信息输入到日志
  • 在信号处理回调中,通过backtrace()获取调用栈帧。
  • backtrace_symbols()转换为可读字符串。
  • 使用dladdr()解析符号地址获取更精确的函数名和源文件信息。
  • C++ 代码,需通过abi::__cxa_demangle()对经过 Name Mangling 的函数名进行 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,再结合源码很快能够排查出问题所在。

总结

  • 实现崩溃堆栈信息跟踪流程很简单:先捕获异常信号(如SIGSEGVSIGABRT等),然后在信号处理函数中打印堆栈信息即可。
  • 需要注意的是,C++接口存在名称修饰,故需要调用abi::__cxa_demangle还原函数名。
  • 关于名称修饰,查了一下相关资料,描述如下:
  • g++ 编译 C++ 代码:会进行名称修饰,需用 abi::__cxa_demangle 还原。
  • g++ 编译 C 代码:若未使用 extern "C",会按 C++ 规则修饰函数名,导致链接错误;添加 extern "C" 后则不会修饰。
  • gcc 编译 C 代码:始终不会修饰函数名。
  • 注意使用堆栈打印时,编译时需添加-O0 -g -rdynamic选项以保留符号信息,确保堆栈中的函数名可被解析。
相关文章
|
Linux 编译器 程序员
【Linux 调试秘籍】深入探索 C++:运行时获取堆栈信息和源代码行数的终极指南
【Linux 调试秘籍】深入探索 C++:运行时获取堆栈信息和源代码行数的终极指南
1667 0
|
存储 编译器 C++
QT之QFlags详解
QT之QFlags详解
805 0
|
安全 C++
Spdlog日志库的使用,支持文件名/行号/函数名的log打印输出
Spdlog日志库的使用,支持文件名/行号/函数名的log打印输出
6912 0
|
编解码 Ubuntu Linux
时隔半年,Linux 性能重新超越 Windows 11
用户可以访问 OpenBenchmarking.org 页面查看 Windows 和 Linux 之间所有 103 项基准测试的完整结果。
819 0
|
安全 Linux 开发者
⭐⭐⭐⭐⭐Linux C/C++ 进程崩溃诊断以及有效数据收集:解锁代码问题快速定位与修复的方法
⭐⭐⭐⭐⭐Linux C/C++ 进程崩溃诊断以及有效数据收集:解锁代码问题快速定位与修复的方法
1344 1
|
算法 安全 开发者
优化if-else的11种方案
优化if-else结构的方法多种多样,通过选择合适的方法,可以提高代码的可读性、可维护性和灵活性。本文详细介绍了11种优化if-else的方法,并通过代码示例说明了每种方法的具体应用。希望这些方法能够帮助开发者在实际编程
607 21
|
存储 监控 安全
如何排查常见的 Windows 应用程序错误和崩溃
本文介绍了如何通过事件日志分析来诊断Windows应用程序错误和崩溃的根本原因。文章首先解释了应用错误的表现形式及常见事件ID(如1000、1001等),并分析了导致崩溃的原因,包括硬件问题(如存储不足、外部因素)和软件问题(如编码错误、数据损坏、.NET Framework兼容性)。接着,提供了几种故障排除方法,例如运行系统文件检查器(SFC)、执行干净启动、检查更新以及重新安装.NET Framework。最后,探讨了使用日志管理工具(如EventLog Analyzer)集中分析崩溃事件的功能,包括预置报表、时间轴分析、实时警报和自动化响应,帮助管理员高效解决应用问题。
2995 1
【Qt 学习笔记】Qt常用控件 | 布局管理器 | 水平布局Horizontal Layout
【Qt 学习笔记】Qt常用控件 | 布局管理器 | 水平布局Horizontal Layout
1594 2
|
C++
spdlog 日志库部分源码说明——日志格式设定,DIY你自己喜欢的调试信息,你能调试的远比你想象的还要丰富
spdlog 日志库部分源码说明——日志格式设定,DIY你自己喜欢的调试信息,你能调试的远比你想象的还要丰富
2235 6
|
计算机视觉
使用QT显示OpenCV读取的图片
使用QT显示OpenCV读取的图片
546 1