Skip to content

Native Crash:Signal、Breakpad 与堆栈还原 ​

回到总览:性能、测试、排障相关模块:APM 线上性能监控与稳定性体系建设 · Android App 进程启动全流程 (Zygote → ActivityThread → 首帧)

一句话定义 ​

Native Crash 指运行在 ART/NDK 之外的原生代码(C/C++,含 Flutter Engine、第三方 SO) 因非法内存访问等原因被内核发 Signal(SIGSEGV/SIGABRT…)强杀;Breakpad/Crashpad 在信号处理里把崩溃现场写成 minidump,配合带符号的二进制在后端还原出源码行号——它是「线上稳定性」这条主线(06 APM)在原生侧不可绕过的一环。

代码索引 ​

对应 Lab:native-crash-breakpad —— SIGSEGV 触发 + tombstone 抓取 + ndk-stack/Breakpad 符号还原

为什么需要 ​

  • 为什么 Java 层 try/catch 抓不到 Native Crash?
    • 一句话答:Native Crash 是内核级 Signal,不走 JVM 异常机制,Java 的 catch 管不到;它直接终止进程,只能在更底层用信号处理函数或崩溃采集库(Breakpad)拦截。
  • 为什么 Native 崩溃堆栈经常是一堆内存地址(#00 0x0000...),看不出哪行代码?
    • 一句话答:发布包剥离了符号表(同 10 瘦身页的 --split-debug-info 思路),崩溃时只有指令地址;需要保留构建时生成的符号文件,离线用 minidump_stackwalk / ndk-stack 把地址映射回源码行号。
  • 为什么有的 Native 崩溃只在「某种 CPU 架构 / 某批设备」出现?
    • 一句话答:常因 ABI 不匹配或对齐/指令集差异(如给 arm64 设备加载了 armv7 的 SO、或对未对齐地址做特定指令),这类问题在交叉编译与多 ABI 分发里高发。

底层机制 ​

1. Signal 与崩溃采集流程 ​

进程收到致命 Signal 后内核中止它;若注册了信号处理函数(Breakpad 注入),则在处理函数里把寄存器、栈、模块列表写成 minidump。

  • 常见 Signal 含义
    Signal典型触发高频根因
    SIGSEGV访问非法/空地址空指针解引用、野指针、栈溢出
    SIGABRT主动 abort / 断言失败JNI 误用、NDK assert、双重 free
    SIGBUS对齐/总线错误未对齐访问、映射内存截断
    SIGILL非法指令ABI 不匹配、损坏的指令流
    SIGFPE算术异常除零、整数溢出陷阱
  • 可能输出(Android tombstone 片段)
    signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)
    Cause: null pointer dereference
    backtrace:
        #00 pc 00001234  libfoo.so (foo_func+12)
        #01 pc 00005678  libfoo.so (Java_com_x_Foo_call+40)
  • 预期现象:tombstone 给出 pc(指令地址)与模块名;没有符号时只有 foo_func+12 这种偏移,拿到对应 SO 的带符号版本后用 ndk-stack/minidump_stackwalk 还原成 foo.c:42。

2. Breakpad 与符号还原 ​

Breakpad 把崩溃写成跨平台统一的 minidump,再用 dump_syms 从带符号二进制抽出符号、用 minidump_stackwalk 合成可读堆栈。

  • 可能输出(符号还原命令示意)
    bash
    # 1. 从带符号 SO 抽符号
    dump_syms libfoo.so > libfoo.so.sym
    # 2. 用 minidump + 符号还原堆栈
    minidump_stackwalk crash.dmp ./symbols > readable_stack.txt
  • 预期现象:还原后堆栈从 pc 00001234 变成 foo.c:42 in foo_func,可直接定位源码;符号文件必须像 TLS 的 symbols、混淆的 mapping 一样永久归档,否则历史崩溃永远不可读。

Android / Flutter / iOS / Backend 对照 ​

维度AndroidFlutter(Engine/SO)iOSBackend
采集Breakpad/Crashpad + tombstone同 Android(Engine 亦原生)Crash Reporter(.ips/异常类型)core dump / 各语言 tracer
符号NDK .so 符号 / ndk-stacklibapp.so 的符号(同 10 页)dSYM各语言的 debug info
典型崩溃JNI 误用、野指针Engine bug、插件 SO 冲突EXC_BAD_ACCESS段错误 / OOM-kill
信号体系Linux Signal同左Mach 异常(映射为崩溃类型)Signal / SIGSEGV

常见场景 ​

  • JNI 误用:在错误线程调用 Java(JNIEnv 线程绑定)、DeleteLocalRef 后继续使用、签名写错导致 FindClass 失败——最终多以 SIGSEGV/SIGABRT 收场。
  • 第三方 SO 冲突:两个 SDK 带了同名/不同版本的原生库,加载时符号打架或 ABI 不一致,特定设备崩溃。
  • 启动期 Native 崩溃:Engine 或加固 SO 在 Application 早期初始化出错,表现为「闪退在开屏」,adb logcat 配合 tombstone 定位最快。

常见误配、事故后果与排障 ​

1. 事故发生:发了不带符号的 release SO,Native 崩溃全是不可读地址 ​

  • 误配原因:CI 打包把 libfoo.so 的调试符号 strip 掉,又没另存带符号副本。
  • 后果:线上 Native 崩溃只能看到 pc 0x...,无法定位源码,长时间修不了。
  • 排障与修法:CI 同时归档带符号 SO 到符号服务(类似 10 页的 symbols 归档),崩溃上报后用 ndk-stack/minidump_stackwalk 还原。

2. 误配:只在 Java 层 try/catch,以为能兜住 Native 崩溃 ​

  • 现象:Native 崩溃照样杀进程,catch 没生效,还误以为已保护。
  • 排障与修法:Native 崩溃必须靠 Breakpad/Crashpad 或系统崩溃采集;Java 层只能防御「不要触发」(规范 JNI 调用、判空),无法捕获 Signal。

3. 事故:ABI 分发错误导致特定架构设备崩溃 ​

  • 后果:armeabi-v7a 的 SO 被塞进 arm64-v8a 包,或反之,设备上加载即 SIGILL/SIGSEGV,只在部分机型复现,极难捞。
  • 排障与修法:用 AAB/split 按 ABI 正确分发,CI 校验每种 ABI 都有对应 SO;用 nativeLibraryAbi 与 Build.SUPPORTED_ABIS 核对。

相近概念对比 ​

概念它回答什么不回答什么
Java 异常JVM 内可控错误管不到内核 Signal / Native 崩溃
Signal(SIGSEGV…)进程为何被内核强杀不提供源码行号,只见地址
tombstone现场原始快照无符号时不可读
Breakpad minidump跨平台统一崩溃格式仍需符号文件才能还原可读堆栈
符号还原地址 → 源码行依赖构建时归档的符号,丢了就废

对应实验 ​

实验说明关键源码
native-crash-breakpad触发 SIGSEGV、读取 tombstone,并用带符号 SO 跑 ndk-stack/minidump_stackwalk 还原堆栈代码内联于 README

复习检查题 ​

  1. 为什么 Java 的 try/catch 拦不住 Native Crash?它和 JVM 异常的本质区别是什么? 答:Native Crash 是内核向进程发送的 Signal(如 SIGSEGV),属于操作系统级的进程终止机制,根本不经过 JVM 的异常栈;而 try/catch 只捕获 JVM 抛出的 Throwable。所以原生崩溃只能由更底层的信号处理或 Breakpad 之类采集库拦截,Java 层无法捕获,最多只能在「调用原生前」规范 JNI、判空来预防触发。

  2. 为什么 Native 崩溃堆栈初始是内存地址,需要「符号文件」才能看懂?符号文件忘归档会怎样? 答:发布包为减小体积剥离了调试符号,崩溃时只有指令地址(pc 0x... + 模块偏移)。符号文件(带符号 SO / dSYM)记录了地址到「文件:行号:函数」的映射,离线用 ndk-stack/minidump_stackwalk 还原。若没归档符号,历史崩溃永远停在不可读地址,事后无法定位源码——所以它必须像 TLS 的 symbols 一样随发版永久保存。

  3. SIGSEGV 和 SIGABRT 在排查方向上有什么不同? 答:SIGSEGV 多指向内存非法访问(空/野指针、栈溢出、越界),排查重点是「谁访问了不该访问的地址」;SIGABRT 多为进程主动 abort(断言失败、double free、JNI 严重误用触发 abort),排查重点是「哪处逻辑主动判了失败并调用 abort」。前者看指针来源,后者看断言/释放逻辑。

速记 ​

  • Signal 五兄弟:SEGV(空/野指针)、ABRT(断言/free 错)、BUS(对齐)、ILL(ABI 错)、FPE(除零)。
  • Java 抓不到:Native 崩溃是内核 Signal,非 JVM 异常。
  • 符号即钥匙:带符号 SO 必归档,否则崩溃永远不可读。
  • 出口接 APM:Native 崩溃堆栈还原后回流 06 线上稳定性看板。

站点构建时间:2026/8/24 23:43:17