Appearance
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 Crash 是内核级 Signal,不走 JVM 异常机制,Java 的
- 为什么 Native 崩溃堆栈经常是一堆内存地址(
#00 0x0000...),看不出哪行代码?- 一句话答:发布包剥离了符号表(同 10 瘦身页的
--split-debug-info思路),崩溃时只有指令地址;需要保留构建时生成的符号文件,离线用minidump_stackwalk/ndk-stack把地址映射回源码行号。
- 一句话答:发布包剥离了符号表(同 10 瘦身页的
- 为什么有的 Native 崩溃只在「某种 CPU 架构 / 某批设备」出现?
- 一句话答:常因 ABI 不匹配或对齐/指令集差异(如给 arm64 设备加载了 armv7 的 SO、或对未对齐地址做特定指令),这类问题在交叉编译与多 ABI 分发里高发。
底层机制
1. Signal 与崩溃采集流程
进程收到致命 Signal 后内核中止它;若注册了信号处理函数(Breakpad 注入),则在处理函数里把寄存器、栈、模块列表写成 minidump。
- 常见 Signal 含义
Signal 典型触发 高频根因 SIGSEGV 访问非法/空地址 空指针解引用、野指针、栈溢出 SIGABRT 主动 abort / 断言失败 JNI 误用、NDK assert、双重 freeSIGBUS 对齐/总线错误 未对齐访问、映射内存截断 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 对照
| 维度 | Android | Flutter(Engine/SO) | iOS | Backend |
|---|---|---|---|---|
| 采集 | Breakpad/Crashpad + tombstone | 同 Android(Engine 亦原生) | Crash Reporter(.ips/异常类型) | core dump / 各语言 tracer |
| 符号 | NDK .so 符号 / ndk-stack | libapp.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 |
复习检查题
为什么 Java 的
try/catch拦不住 Native Crash?它和 JVM 异常的本质区别是什么? 答:Native Crash 是内核向进程发送的 Signal(如 SIGSEGV),属于操作系统级的进程终止机制,根本不经过 JVM 的异常栈;而try/catch只捕获 JVM 抛出的Throwable。所以原生崩溃只能由更底层的信号处理或 Breakpad 之类采集库拦截,Java 层无法捕获,最多只能在「调用原生前」规范 JNI、判空来预防触发。为什么 Native 崩溃堆栈初始是内存地址,需要「符号文件」才能看懂?符号文件忘归档会怎样? 答:发布包为减小体积剥离了调试符号,崩溃时只有指令地址(
pc 0x...+ 模块偏移)。符号文件(带符号 SO / dSYM)记录了地址到「文件:行号:函数」的映射,离线用ndk-stack/minidump_stackwalk还原。若没归档符号,历史崩溃永远停在不可读地址,事后无法定位源码——所以它必须像 TLS 的symbols一样随发版永久保存。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 线上稳定性看板。