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

简介: 在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++:运行时获取堆栈信息和源代码行数的终极指南
1622 0
|
存储 编译器 C++
QT之QFlags详解
QT之QFlags详解
769 0
|
存储 API C++
【Qt 信号槽】深入探索 Qt 信号和槽机制中的引用传递“ (“A Deep Dive into Reference Passing in Qt Signal and Slot Mechanism“)
【Qt 信号槽】深入探索 Qt 信号和槽机制中的引用传递“ (“A Deep Dive into Reference Passing in Qt Signal and Slot Mechanism“)
1487 0
|
安全 Linux 开发者
⭐⭐⭐⭐⭐Linux C/C++ 进程崩溃诊断以及有效数据收集:解锁代码问题快速定位与修复的方法
⭐⭐⭐⭐⭐Linux C/C++ 进程崩溃诊断以及有效数据收集:解锁代码问题快速定位与修复的方法
1265 1
|
11月前
|
存储 人工智能 Shell
Lua与C语言接口编程实战指南:打造高性能、灵活的程序
本文深入介绍了 Lua 与 C 语言的交互机制,重点分析了 Lua 作为胶水语言在嵌入式系统、游戏开发(如 Skynet、OpenResty)中的应用。内容涵盖 Lua 环境搭建、虚拟栈管理、C 与 Lua 的相互调用、闭包、Userdata 和注册表的使用等核心技术,并结合代码示例讲解了如何在实际项目中实现 Lua 与 C 的高效交互,适合希望掌握 Lua 扩展与嵌入开发的工程师参考学习。
1243 0
|
11月前
|
编解码 Ubuntu Linux
时隔半年,Linux 性能重新超越 Windows 11
用户可以访问 OpenBenchmarking.org 页面查看 Windows 和 Linux 之间所有 103 项基准测试的完整结果。
740 0
|
算法 安全 开发者
优化if-else的11种方案
优化if-else结构的方法多种多样,通过选择合适的方法,可以提高代码的可读性、可维护性和灵活性。本文详细介绍了11种优化if-else的方法,并通过代码示例说明了每种方法的具体应用。希望这些方法能够帮助开发者在实际编程
575 21
|
存储 监控 安全
如何排查常见的 Windows 应用程序错误和崩溃
本文介绍了如何通过事件日志分析来诊断Windows应用程序错误和崩溃的根本原因。文章首先解释了应用错误的表现形式及常见事件ID(如1000、1001等),并分析了导致崩溃的原因,包括硬件问题(如存储不足、外部因素)和软件问题(如编码错误、数据损坏、.NET Framework兼容性)。接着,提供了几种故障排除方法,例如运行系统文件检查器(SFC)、执行干净启动、检查更新以及重新安装.NET Framework。最后,探讨了使用日志管理工具(如EventLog Analyzer)集中分析崩溃事件的功能,包括预置报表、时间轴分析、实时警报和自动化响应,帮助管理员高效解决应用问题。
2758 1
条件变量函数pthread_cond_timedwait实现业务场景
条件变量函数pthread_cond_timedwait实现业务场景
661 0
|
Linux C语言
Linux gcc升级全过程,过程超详细
Linux gcc升级全过程,过程超详细
8109 0
Linux gcc升级全过程,过程超详细

热门文章

最新文章