Tombstone 与 ndk-stack:native 崩溃后怎样从现场找到源码行
native crash 和 Kotlin exception 最大的区别是:它不是抛给你一个普通异常,而是系统记录一份崩溃现场。这个现场文件叫 tombstone。
这一篇讲一条排障主线。
崩溃发生
系统生成 tombstone 或 logcat crash dump
用 unstripped .so 符号化
还原函数名和源码行
判断根因类型
Tombstone 是什么
tombstone 是 Android native 崩溃报告。它通常包含:
进程名和线程名
信号类型,例如 SIGSEGV
崩溃地址
寄存器现场
backtrace
memory map
崩溃线程和其他线程信息
可以把它想成事故现场照片。它不能直接告诉你“谁写错了”,但会告诉你事故发生在哪个位置、当时线程栈是什么样。
常见信号
SIGSEGV:访问非法内存,常见于空指针、UAF、越界
SIGABRT:主动 abort,常见于 assert、fatal log、allocator 检测失败
SIGBUS:地址对齐或映射访问错误
SIGILL:执行非法指令,可能是 ABI 或 CPU 特性不匹配
看到 SIGSEGV 时,不要立刻认定是空指针。UAF、越界、野指针都可能表现为 SIGSEGV。
backtrace 为什么需要符号化
崩溃日志可能长这样。
#00 pc 0000000000012340 /data/app/.../lib/arm64/libplayer_core.so
#01 pc 0000000000011a20 /data/app/.../lib/arm64/libplayer_core.so
这只有地址,没有函数名和源码行。要还原,就需要构建时保留的未剥离 .so。
AGP 构建中,未剥离库常在:
app/build/intermediates/cxx/<build-type>/<hash>/obj/<abi>
官方 ndk-stack 文档也说明,它可以从 adb logcat 或 /data/tombstones/ 的 tombstone 中符号化 native 堆栈。
使用 ndk-stack
假设崩溃日志在 crash.txt,符号目录在 obj/arm64-v8a。
ndk-stack -sym app/build/intermediates/cxx/RelWithDebInfo/xxxx/obj/arm64-v8a -dump crash.txt
输出会变成类似:
PlayerDecoder::drainOutput(PlayerDecoder.cpp:128)
PlayerController::decodeLoop(PlayerController.cpp:76)
这时你才能回到源码分析。
为什么线上必须保存符号
Release 包通常会 strip,去掉调试符号以减小体积和保护实现细节。
但你必须在 CI 中把对应版本的 unstripped .so 归档。
否则线上崩溃只能看到地址。
版本号 1.2.0
commit abc123
ABI arm64-v8a
unstripped libplayer_core.so
mapping 或符号归档位置
native 崩溃排查离不开“线上包”和“符号文件”一一对应。
根因分类
符号化后,先不要急着改代码。先分类。
空指针:崩溃地址接近 0x0
UAF:对象已释放后仍被线程使用
越界:访问地址接近数组边界外
竞态:多线程时序相关,复现不稳定
ABI/符号:启动或 loadLibrary 阶段失败
系统 API 误用:Surface、codec、JNIEnv 生命周期错误
播放器崩溃示例
现象:
用户退出播放页后偶发 native crash
符号化后:
RenderThread::renderOneFrame(RenderThread.cpp:94)
ANativeWindow_lock
推理:
render 线程还在使用 ANativeWindow
Activity 已触发 surfaceDestroyed
SurfaceSession 没有先 detach 或等待 render 线程停止
修复方向:
surfaceDestroyed 发送 DetachSurface 命令
render 线程观察 window=null 后停止提交
release 时先 stop render,再 release ANativeWindow
排障报告模板
现象:用户动作、设备、系统版本、ABI
信号:SIGSEGV/SIGABRT/...
崩溃线程:线程名
崩溃栈:符号化后前三层
最近状态:PlayerState、command serial
资源状态:Surface、Codec、Queue
根因分类:UAF/越界/竞态/...
修复策略:生命周期、锁、RAII、API 顺序
回归实验:怎样复现和证明修复
小白怎样读第一份 tombstone
第一次看到 tombstone 会很乱。可以先只看五行信息。
signal
fault addr
thread name
backtrace #00
backtrace #01
signal 告诉你崩溃类型。
fault addr 告诉你访问了哪个非法地址。
thread name 告诉你是哪条线程出事。
#00 通常是最接近崩溃的位置。
#01 能帮助你看到是谁调用了它。
不要一开始就读完整 memory map。先把崩溃栈符号化,再回到源码看生命周期。
工程风险与观测
native crash 处理必须标准化。
每个 native 线程设置可读线程名
每个状态转换记录 serial
每个 fatal error 记录 source
每个发布版本保存符号
每次 ndk-stack 还原都保存报告
播放器尤其要观测这些字段。
PlayerState
Surface attached
Codec started
PacketQueue size
FrameQueue size
last command serial
如果 tombstone 显示 render 线程崩溃,而日志显示 Surface 已 detached,基本可以推断是并发资源释放顺序错了。
修复后一定要做回归:
快速退出页面
后台前台切换
横竖屏旋转
连续 seek
这些动作最容易复现 native 生命周期问题。
本章结论
native crash 不是靠猜。你需要 tombstone 保存现场,unstripped .so 还原源码行,再根据信号、栈、状态和资源生命周期分类。这个流程越标准,线上 native 问题越可控。