4. 崩溃信号详解

Android Native 崩溃的“第一现场”几乎总是由内核投递的信号(signal)触发。当进程访问非法内存、执行非法指令、除零或主动调用 abort 时,内核或运行时库会向进程发送一个信号;若该信号未被捕获或处理失败,进程就会终止,并在 tombstone 中留下信号编号、信号名、故障地址和寄存器快照。理解每种信号的含义、产生原因与典型堆栈特征,是从“看到崩溃”走向“定位根因”的关键一步。

信号是内核与进程之间的异步通知机制。Native 崩溃诊断的核心链路是:异常事件 → 内核投递信号 → 默认处理或信号处理器 → 进程终止 → tombstone/日志记录。信号编号、si_code、fault addr 与 backtrace 共同构成判断依据。

一、核心原理:信号如何变成崩溃

Linux 中每个信号都有编号和默认动作。与 Native 崩溃最相关的是同步信号:它们由 CPU 异常或运行时错误直接触发,而不是由其他进程随意发送。同步信号的特点是“可复现、与当前指令强相关”,因此 tombstone 中的 fault addr 和 PC 寄存器往往能直接指向出错指令。

Android 的 bionic 链接器会为部分信号安装处理器,用于生成 tombstone。当信号到达时,处理器抓取寄存器、调用栈和内存映射,写入 /data/tombstones/,然后重新抛出信号让进程按默认方式终止。因此我们看到的堆栈既包含崩溃线程的原始调用链,也可能包含信号处理器的帧。

Native 崩溃信号产生与处理链路 异常事件 非法地址 / 非法指令 除零 / abort / 断点 内核 / 运行时 CPU 异常或库函数 调用 raise / abort 投递信号 SIGSEGV / SIGABRT SIGBUS / SIGILL 等 信号处理器 抓取寄存器与栈 生成 tombstone tombstone 关键字段 signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 同步信号(可复现) SIGSEGV / SIGBUS SIGILL / SIGFPE / SIGTRAP 与当前指令强相关 异步信号(外部触发) SIGABRT(abort 主动触发) SIGKILL / SIGTERM 需结合日志判断来源 定位三要素 信号名 + si_code fault addr / 寄存器 backtrace 符号化

二、六类核心信号的含义与典型堆栈特征

不同信号对应不同的故障语义。SIGSEGV 表示无效内存访问,最常见的是空指针解引用、野指针、栈溢出和 use-after-free;SIGABRT 通常由 abort()、断言失败、C++ 未捕获异常或堆破坏检测触发;SIGBUS 表示总线错误,多与地址对齐错误、mmap 文件截断后访问有关;SIGILL 表示执行了非法指令,常见于指令集不匹配、跳转到数据区或栈被破坏;SIGFPE 表示算术异常,整数除零是最典型场景;SIGTRAP 由断点指令触发,调试器、__builtin_trap() 或某些运行时检查会使用它。

信号 编号 核心含义 典型产生原因 堆栈/日志特征
SIGSEGV 11 无效内存访问 空指针、野指针、越界、栈溢出 fault addr 常为 0x0 或非法地址,PC 指向访存指令
SIGABRT 6 进程主动中止 abort、assert、未捕获异常、堆破坏 堆栈含 abort/raise,常伴随 “Abort message”
SIGBUS 7 总线/对齐错误 未对齐访问、mmap 文件被截断 fault addr 为对齐异常地址,常出现在 memcpy 类函数
SIGILL 4 非法指令 指令集不匹配、跳转到数据、栈破坏 PC 指向非代码区或未知指令,反汇编异常
SIGFPE 8 算术异常 整数除零、取模零、溢出陷阱 PC 指向除法指令,寄存器含被除数与零除数
SIGTRAP 5 断点/陷阱 调试断点、__builtin_trap、运行时检查 PC 指向断点指令,常由调试器或 sanitizer 触发

三、方法流程:从 tombstone 反推信号根因

拿到一份 tombstone 后,建议按固定顺序阅读,避免被大量寄存器信息干扰。第一步看 signal 行,确认信号编号、名称和 si_code;第二步看 fault addr,判断是空指针、非法地址还是对齐问题;第三步看崩溃线程的 backtrace,找到第一个属于业务代码的帧;第四步结合 memory map 判断该地址落在哪个模块或匿名映射;第五步用 addr2line 或 ndk-stack 做符号化。

# 典型 tombstone 头部
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
    x0  0000000000000000  x1  0000007f8a3c2b40
    pc  0000007f8b2c1a34  sp  0000007fc3d2e100

# 符号化命令
ndk-stack -sym out/target/product/arm64/symbols \
          -dump tombstone_04

# 或使用 addr2line
aarch64-linux-android-addr2line -f -C -e libxxx.so 0x1a34
阅读 tombstone 时,先看信号与 si_code,再看 fault addr,最后看 backtrace。顺序颠倒容易被信号处理器帧误导,把崩溃点定位到 bionic 而不是业务代码。

四、案例与场景:同一信号的不同根因

案例一:某视频应用在播放结束时崩溃,tombstone 显示 signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0,backtrace 第一帧是 libplayer.so 的 releaseDecoder。结合代码发现对象已释放但指针未置空,属于典型的 use-after-free 后空指针解引用。

案例二:某应用在低端机上随机崩溃,信号为 SIGBUS,fault addr 不是 0,而是某个非对齐地址。进一步查看发现代码用 reinterpret_cast 将 char* 强转为 int* 后直接读取,在 ARM 严格对齐平台上触发总线错误。修复方式是改用 memcpy 或确保对齐。

案例三:某 SDK 在初始化时崩溃,信号为 SIGABRT,日志中出现 Abort message: 'FORTIFY: memset: prevented write past end of buffer'。这说明是运行时保护检测到缓冲区越界后主动 abort,根因在越界写而非 abort 本身。

五、误区与复盘

  • 误区一:看到 SIGSEGV 就认定是空指针。 fault addr 为 0 才更倾向空指针;非零地址可能是越界、释放后访问或权限问题。
  • 误区二:忽略 si_code。 SEGV_MAPERR 表示地址未映射,SEGV_ACCERR 表示权限不足,二者排查方向不同。
  • 误区三:把 SIGABRT 当成独立故障。 abort 往往是结果而非原因,必须找到 Abort message 和触发 abort 的上游逻辑。
  • 误区四:只看崩溃线程。 多线程场景下,崩溃线程可能只是“受害者”,真正破坏内存的线程在别处,需要结合日志时间线与其他线程状态。
  • 误区五:不做符号化就下结论。 没有符号的 backtrace 只能看到地址,无法确认业务帧,容易误判。
信号只是入口,不是结论。SIGSEGV、SIGABRT、SIGBUS、SIGILL、SIGFPE、SIGTRAP 各自指向不同的故障类别,但最终根因仍需结合 fault addr、si_code、backtrace、memory map 与业务日志综合判断。建立“信号 → 类别 → 地址 → 符号 → 代码”的固定分析路径,才能稳定提升 Native 崩溃定位效率。