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

本文涉及的产品
注册配置 MSE Nacos/ZooKeeper,182元/月
Serverless 应用引擎免费试用套餐包,4320000 CU,有效期3个月
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 在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选项以保留符号信息,确保堆栈中的函数名可被解析。
相关文章
|
5月前
|
数据可视化 前端开发 开发工具
如何在网页中嵌入UE/Unity/WebGL程序,并与网页端通信
LarkXR实时云渲染平台,为UE数字孪生提供的产品化、平台化功能模块,以及必备的二次开发能力。
217 11
如何在网页中嵌入UE/Unity/WebGL程序,并与网页端通信
|
5月前
|
存储 SQL 大数据
从 o11y 2.0 说起,大数据 Pipeline 的「多快好省」之道
SLS 是阿里云可观测家族的核心产品之一,提供全托管的可观测数据服务。本文以 o11y 2.0 为引子,整理了可观测数据 Pipeline 的演进和一些思考。
326 35
|
5月前
|
监控 容灾 算法
阿里云 SLS 多云日志接入最佳实践:链路、成本与高可用性优化
本文探讨了如何高效、经济且可靠地将海外应用与基础设施日志统一采集至阿里云日志服务(SLS),解决全球化业务扩展中的关键挑战。重点介绍了高性能日志采集Agent(iLogtail/LoongCollector)在海外场景的应用,推荐使用LoongCollector以获得更优的稳定性和网络容错能力。同时分析了多种网络接入方案,包括公网直连、全球加速优化、阿里云内网及专线/CEN/VPN接入等,并提供了成本优化策略和多目标发送配置指导,帮助企业构建稳定、低成本、高可用的全球日志系统。
629 54
|
人工智能 搜索推荐 API
AI尝鲜:使用dify监测金融市场情绪
本实验介绍了如何利用dify创建金融市场情绪工作流,通过输入公司名称(如英伟达),使用Tavily搜索引擎获取相关金融新闻,并借助大模型(如通义千问)进行情绪分析,输出介于-1到1之间的情绪评分。实验分为四步:安装dify、设置模型供应商、配置搜索引擎以及创建工作流。最终,用户可运行工作流,获得量化的市场情绪数据,为量化交易策略提供依据。
AI尝鲜:使用dify监测金融市场情绪
|
5月前
|
监控 Kubernetes Go
日志采集效能跃迁:iLogtail 到 LoongCollector 的全面升级
LoongCollector 在日志场景中实现了全面的重磅升级,从功能、性能、稳定性等各个方面均进行了深度优化和提升,本文我们将对 LoongCollector 的升级进行详细介绍。
453 86
|
5月前
|
人工智能 移动开发 搜索推荐
增强现实让广告“活”起来——AR 赋能营销的新玩法
增强现实让广告“活”起来——AR 赋能营销的新玩法
260 25
|
5月前
|
前端开发 JavaScript
借助 CodeBuddy,轻松打造「一分钟冥想」App
有一天,我突发奇想,想做一个非常简单但美观的应用:**「一分钟冥想」**。它不需要繁复的交互,也不涉及音频流媒体或账户体系,只是一个安静、优雅的页面,引导用户专注呼吸,放松身心,完成短暂而高效的 60 秒冥想。我把这个想法交给了 CodeBuddy,启动了一个全新的 UniApp 项目,开始了这段愉快的前端实现之旅。 --- ## 需求分析:越简单的产品越考验设计 最初我和 CodeBuddy 明确了目标:**打造一个拥有 SVG 呼吸圈动画、渐变背景、引导语音(可以占位)、简约 UI 和播放按钮的冥想页面。** CodeBuddy 快速分析了项目范围和复杂度,这将是一个前端单页面
113 4
借助 CodeBuddy,轻松打造「一分钟冥想」App
|
4月前
|
存储 弹性计算 数据可视化
如何在公有云部署UE/Unity实时云渲染推流平台
以阿里云主机为例,介绍如何在公有云上部署Paraverse平行云LarkXR实时云渲染平台,支持UE、Unity等各类引擎开发的三维可视化程序上云,应用于数字孪生、教育虚仿、展览展示、元宇宙及数字人等3D/XR场景中。
|
5月前
|
关系型数据库 MySQL 定位技术
MySQL与Clickhouse数据库:探讨日期和时间的加法运算。
这一次的冒险就到这儿,期待你的再次加入,我们一起在数据库的世界中找寻下一个宝藏。
214 9