3. 信号与异常机制

3.1 现象描述

应用程序因非法内存访问或内部断言失败被操作系统强制终止,进程退出码通常为 139(SIGSEGV)或 134(SIGABRT)。终端或日志中记录类似“Segmentation fault (core dumped)”或“Aborted (core dumped)”的提示。用户侧表现为进程突然消失、未响应、闪退,甚至触发操作系统的崩溃报告窗口。

3.2 典型特征

信号默认行为核心转储典型来源
SIGSEGV (信号 11)终止进程是空指针解引用、释放后使用、栈溢出、越界访问
SIGABRT (信号 6)终止进程是assert() 失败、abort() 主动调用、C++ 未捕获异常时 libc++ 内部调用

关键判断规则:SIGSEGV 由硬件内存保护单元触发(非法地址访问);SIGABRT 由软件主动调用 abort 或断言失败触发。前者属于内存越界/野指针,后者属于逻辑契约被破坏。

3.3 排查路径

以下故障树帮助快速定位根因:

SIGSEGV / SIGABRT 故障排查树 进程崩溃 (信号) SIGSEGV(信号 11) SIGABRT(信号 6) 空指针解引用 野指针 / UAF 栈/堆溢出 assert 失败 abort() 调用 C++ 异常 数据采集:core dump / 日志 数据采集:backtrace / 堆栈 gdb bt addr2line dmesg/pmsg 异常代码(si_code)补充 SIGSEGV 的 si_code:SEGV_MAPERR(地址未映射)、SEGV_ACCERR(权限违反) SIGABRT 的 si_code 通常为 SI_TKILL(来自 raise/abort)或 SI_USER(来自 kill)

3.4 判断规则

  • SIGSEGV:检查 /proc/sys/kernel/core_pattern 确保 core dump 已启用;ulimit -c unlimited 设置;崩溃后立即使用 gdb /path/to/binary core 加载 core 文件,执行 bt/bt full 查看调用栈。重点关注 rip 附近的指令以及 rdi/rsi 中是否为无效地址。
  • SIGABRT:堆栈顶层通常是 __GI_abort 或 __assert_fail。向上追溯找到调用 assert() 或 abort() 的位置,检查断言条件是否被违反。

3.5 修复动作

问题类型修复动作
空指针解引用(SIGSEGV)增加 if (ptr == nullptr) 防御检查;使用智能指针替代裸指针;初始化指针后尽快赋值。
释放后使用(UAF)改用 std::shared_ptr/weak_ptr,或使用引用计数;释放后立即置空(ptr = nullptr);启用 AddressSanitizer 检测。
栈溢出(SIGSEGV)减少局部大数组;增大栈大小(pthread_attr_setstacksize);将大数据分配至堆上。
assert 失败(SIGABRT)修正断言条件或修复逻辑错误;若为外部输入导致,将 assert 改为错误处理(if (!cond) return error)。
C++ 异常逃逸(SIGABRT)捕获所有异常:try { ... } catch (...) { /* 记录 */ };确保析构函数不抛出异常。

数据采集示例:启用 core dump 并解析堆栈

# 启用 core dump
ulimit -c unlimited
echo "/tmp/core.%e.%p" > /proc/sys/kernel/core_pattern

# 使用 gdb 分析
gdb ./myapp /tmp/core.myapp.12345
(gdb) bt
(gdb) info registers rdi rsi rip

3.6 复测步骤

  1. 回归测试:重新编译并运行触发崩溃的用例,确认不再出现信号终止。
  2. 压力测试:使用 stress 或 parallel 重复执行目标功能 100 次以上,检查是否有内存错误。
  3. Sanitizer 验证:开启 AddressSanitizer(-fsanitize=address)和 UndefinedBehaviorSanitizer(-fsanitize=undefined)重新构建并运行,确保无告警。
  4. 边界输入测试:传入空值、极大值、负数、畸形结构体,确认程序不崩溃。

注意:仅消除崩溃表象但未修复根因(例如用 signal(SIGSEGV, SIG_IGN) 屏蔽信号)会导致更隐蔽的数据损坏,切勿在生产环境使用。

3.7 预防建议

  • 代码提交前强制启用 ASAN/UBSan 进行本地测试。
  • 使用静态分析工具(Clang-Tidy、Cppcheck)扫描空指针和越界访问。
  • 遵循 RAII 原则,避免裸 new/delete,优先使用 std::vector、std::string、智能指针。
  • 在 CI 流水线中增加崩溃 catch 测试:故意构造错误场景并验证程序能优雅退出并输出错误日志,而非触发 SIGABRT。
  • 设置进程异常退出自动采集 core dump 并归档,定期统计 TOP 崩溃源。