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

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

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

设计方案

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

  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选项以保留符号信息,确保堆栈中的函数名可被解析。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容