3. 信号与异常机制:SIGSEGV、SIGABRT、异常代码
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 排查路径
以下故障树帮助快速定位根因:
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 复测步骤
- 回归测试:重新编译并运行触发崩溃的用例,确认不再出现信号终止。
- 压力测试:使用
stress或parallel重复执行目标功能 100 次以上,检查是否有内存错误。 - Sanitizer 验证:开启 AddressSanitizer(
-fsanitize=address)和 UndefinedBehaviorSanitizer(-fsanitize=undefined)重新构建并运行,确保无告警。 - 边界输入测试:传入空值、极大值、负数、畸形结构体,确认程序不崩溃。
注意:仅消除崩溃表象但未修复根因(例如用 signal(SIGSEGV, SIG_IGN) 屏蔽信号)会导致更隐蔽的数据损坏,切勿在生产环境使用。
3.7 预防建议
- 代码提交前强制启用 ASAN/UBSan 进行本地测试。
- 使用静态分析工具(Clang-Tidy、Cppcheck)扫描空指针和越界访问。
- 遵循 RAII 原则,避免裸 new/delete,优先使用
std::vector、std::string、智能指针。 - 在 CI 流水线中增加崩溃 catch 测试:故意构造错误场景并验证程序能优雅退出并输出错误日志,而非触发 SIGABRT。
- 设置进程异常退出自动采集 core dump 并归档,定期统计 TOP 崩溃源。