05-鸿蒙 faultlog 崩溃日志分析
鸿蒙 faultlog 崩溃日志分析实战:cppcrash / jscrash / appfreeze 三类故障怎么读
本文为「鸿蒙系统测试新人笔记」系列第 5 弹。适用对象:需要分析鸿蒙应用/系统崩溃、卡死根因的新人。
文档定位:崩溃日志在哪 + 三类故障怎么看 + 真实日志长啥样 + 直接抄的获取/分析命令。读完能独立完成一次崩溃/冻屏根因定位。
系列导航:① hdc 环境配置 → ② wukong 压测 → ③ hilog 日志 → ④ keycodeType 控键 → ⑤ faultlog 崩溃分析(本文) → ⑥ uitest 自动化 → ⑦ DevEco 图形化 → ⑧ 性能功耗 → ⑨ 专项平台 → ⑩ DFX 全家桶 → ⑪ 分布式协同 → ⑫ 合规红线 → ⑬ 方法论
本文关联篇:hdc 配置 · wukong 测试 · hilog 指南 · keycodeType · DevEco 图形化 · DFX 全家桶。系列文章均已在个人主页发布,可在主页目录查看。
摘要:faultlog 是鸿蒙系统自动生成的崩溃/冻屏现场记录,相当于 Android 的 tombstone/ANR。本文系统讲解 faultlog 落盘路径、获取方式,并以真实日志样例逐段拆解 cppcrash(Native 崩溃)、jscrash(ArkTS 崩溃)、appfreeze(应用冻屏)三类故障的分析方法,附首次分析决策树、常见崩溃模式速查表、符号化步骤、术语表与 FAQ,新人照着做即可独立完成根因定位。
一、faultlog 是什么(30 秒理解)
faultlog(故障日志)是鸿蒙 DFX 子系统自动生成的崩溃/卡死现场记录。当应用或进程发生异常(崩溃、冻屏)时,系统会把"案发现场"存成文件,开发者拿来分析根因。
一句话记忆法:faultlog = 鸿蒙版 tombstone / ANR 日志。wukong 压测发现的崩溃,最终证据都落在这里。
1.1 日志是怎么生成的(知其所以然)
进程崩溃/卡死
│
├─ Native 崩溃(信号触发) ──→ 内核捕获信号 ──→ ProcessDump 守护进程
│ ├─ dump 寄存器/栈/内存
│ ├─ 先写 /data/log/faultlog/temp(临时)
│ └─ 转存 /data/log/faultlog/faultlogger(正式)
│
├─ JS 崩溃(未捕获异常) ──→ ArkTS 运行时 ──→ 同上落盘
│
└─ 应用冻屏(watchdog 检测) ──→ AppFreeze 模块 ──→ 落盘
关键点:崩溃发生 → 进程已被系统杀死,日志是"死后现场"。所以分析时看到的是"尸体报告",不是"病程记录"。要看病程得配 hilog(见第3弹)。
1.2 三类核心故障(对应 FaultType 枚举,来自 @ohos.faultLogger)
| FaultType | 值 | 名称 | 含义 | 日常叫法 |
|---|---|---|---|---|
NO_SPECIFIC | 0 | 未区分 | 通用故障 | — |
CPP_CRASH | 2 | C++ 进程崩溃 | Native 层崩溃(信号崩) | cppcrash |
JS_CRASH | 3 | JS 进程崩溃 | ArkTS/JS 未捕获异常 | jscrash |
APP_FREEZE | 4 | 应用冻屏 | 主线程卡死 / 输入无响应 | appfreeze |
注意:值没有 1,1 是预留位,代码里别写错。
还有SYSTEM_FREEZE(系统冻屏)、ASAN(地址越界检测)等,在 DevEco Studio 的 FaultLog 面板可见(见第7弹)。
二、日志落在哪(必记路径)
| 目录 | 内容 | 何时看 |
|---|---|---|
/data/log/faultlog/faultlogger | 正式故障日志(cppcrash / jscrash / appfreeze 都在这) | 分析时看这 |
/data/log/faultlog/temp | 崩溃原始临时文件(ProcessDump 先写这,再转存正式目录) | 极少直接看 |
文件命名规则:
<故障类型>-<进程名>-<进程UID>-<毫秒级时间戳>.log
示例:
cppcrash-com.example.myapplication-20020205-1705812215000.log
jscrash-com.example.myapplication-20020205-1705812215000.log
appfreeze-com.example.myapplication-20020205-1705812215000.log
注意:同一进程最多保留 10 份 cppcrash,超出从最早的一批开始删。抓日志要趁早,压测完立刻导出,别等第二天。
2.1 一份 cppcrash 日志的内部结构(打开后从头到尾长这样)
┌─ Build info ← 系统版本
├─ Module name ← 崩溃应用的包名
├─ Version / VersionCode
├─ Pid / Uid ← 进程号 / 用户号(UID 20020205 = 三方应用)
├─ ProcessName
├─ Reason ← 【重点1】信号类型 + 可能原因提示
├─ Fault thread Info ← 【重点2】崩溃线程的调用栈(#00 是崩溃点)
│ Tid / Name
│ #00 pc ... so(func+offset)
│ #01 pc ... so(func+offset)
│ ...
├─ Registers ← 【重点3】崩溃瞬间 CPU 寄存器值(查空指针)
├─ Memory near registers ← 寄存器附近的内存(看是否乱码/全0)
├─ Stack ← 栈内存 dump
├─ Other thread info ← 其他线程堆栈(看锁竞争/死锁时有用)
└─ Fingerprint ← 【重点4】标准化栈帧指纹(去重/聚类用)
新手分析顺序:Reason → #00 帧 → Registers → Fingerprint(详见第四章)。
三、获取方式(三种任选)
3.0 环境准备清单(崩溃发生前就该备好)
| 准备项 | 说明 | 没备会怎样 |
|---|---|---|
| hdc 已配通 | 见第 1 弹《hdc 环境配置》 | 导不出日志 |
| 设备开 USB 调试 | 开发者选项里 | hdc 连不上 |
| 带符号的 so / 符号表 | debug 包自带;release 包需单独保留构建产物 | cppcrash 只有偏移,看不到行号 |
| debug 包(查 jscrash 行号时) | 含 sourcemap | jscrash 只能 dump raw stack |
| release 包(查 appfreeze 时) | AppFreeze 仅 release 生效 | appfreeze 抓不到 |
| DevEco Studio(可选) | 5.1.0+ 支持自动符号化 | 只能手工 addr2line |
经验:正式测试前,把带符号的构建产物(so/sourcemap)存档。等崩溃了再去翻构建产物,常常已被 CI 清掉。
3.1 方式 1:DevEco Studio FaultLog 面板(最省事,推荐)
开发态下,Studio 自动收集 CppCrash / App Freeze / JS Crash / System Freeze / ASAN 到 FaultLog 面板,按进程和故障类型分好类,还能自动符号化。新手首选。
入口:
View → Tool Windows → FaultLog。版本门槛:符号化需 5.1.0+,AppFreeze 结构化需 6.0.0 Beta2+。详见第 7 弹《DevEco Studio 图形化测试》。
3.2 方式 2:hdc 导出(命令行/无 Studio 时)
# 导出整个故障日志目录到本地
hdc file recv /data/log/faultlog/faultlogger ./faultlogs
# 只看某一类
ls ./faultlogs | grep cppcrash
ls ./faultlogs | grep jscrash
ls ./faultlogs | grep appfreeze
# 先确认目录在不在(找不到时的第一动作)
hdc shell ls /data/log/faultlog/
3.3 方式 3:HiAppEvent 订阅(代码内实时捕获)
应用里订阅崩溃事件,从 external_log 字段读取日志文件路径,再读内容:
import { hiAppEvent } from '@kit.PerformanceAnalysisKit';
hiAppEvent.addWatcher({
name: 'crashWatcher',
appEventFilters: [{ eventType: hiAppEvent.EventType.FAULT }],
onReceive: (domain: string, stage: number, eventInfos: hiAppEvent.AppEventInfo[]) => {
for (const info of eventInfos) {
// params.external_log 是数组,元素是崩溃日志文件的绝对路径
const logs: string[] = info.params.external_log ?? [];
console.info(`捕获到故障: ${info.params.event_name}, 日志路径: ${logs}`);
// 可按需读取文件内容上报到自建后台
}
}
});
用途:线上应用的崩溃监控、自建埋点上报。本地测试一般用方式 1/2 即可。详见第 10 弹《DFX 全家桶》。
四、首次分析决策树(拿到日志先看什么)
新手最怕的是打开一个几百行的日志不知道从哪下眼。按下面顺序走,3 分钟锁定方向。
打开 .log 文件
│
├─ 第 1 步:看文件名前缀 → 确定故障类型
│ cppcrash-* → Native 崩溃 → 走【五、CPP_CRASH】
│ jscrash-* → ArkTS 崩溃 → 走【六、JS_CRASH】
│ appfreeze-*→ 应用冻屏 → 走【七、APP_FREEZE】
│
├─ 第 2 步:找 "Reason:" 行 → 一句话定性
│ Signal:SIGSEGV(SEGV_MAPERR)@0x0 → 空指针解引用
│ Signal:SIGABRT → 主动 abort()/断言失败
│ TypeError / RangeError → JS 异常类型
│ THREAD_BLOCK_6S → 主线程卡死 6 秒
│
├─ 第 3 步:找崩溃点
│ cppcrash → "#00 pc ..." → 崩溃所在 so + 函数 + 偏移
│ jscrash → "Stacktrace:" 块 → 文件:行:列 直接定位
│ appfreeze→ "main thread stack"→ 主线程卡在哪一行
│
├─ 第 4 步:补全行号(cppcrash 才需要)
│ 有符号 so + llvm-addr2line → 还原源码行号
│ 或丢进 DevEco FaultLog 面板自动符号化
│
└─ 第 5 步:复测验证
修完后用相同 wukong 种子(见第2弹)复测,确认不再复现
90% 的崩溃靠前 3 步就能定位到方向。第 4 步是 Native 崩溃的"临门一脚"。
五、CPP_CRASH 详解(Native 崩溃)
5.1 完整日志样例(先看真实长啥样)
Build info: OpenHarmony 5.0.0
Module name: com.example.myapplication
Version: 1.0.0
VersionCode: 1000000
PreInstall: No
RestartTimes: 0
Pid: 579
Uid: 20020205
ProcessName: com.example.myapplication
Reason: Signal:SIGSEGV(SEGV_MAPERR)@0x0000000000000000 probably caused by NULL pointer dereference
Fault thread Info:
Tid: 579, Name: com.example.myapp
#00 pc 0x0000000000008a4c /data/app/el1/bundle/public/com.example.myapplication/.../libs/arm64/libentry.so(OHOS_NativeCrash+84)(BuildId: a40044d0acb68107cfc4adb5049c0725)
#01 pc 0x0000000000008b0c /data/app/el1/bundle/public/com.example.myapplication/.../libs/arm64/libentry.so(NativeCrashCall+32)(BuildId: a40044d0acb68107cfc4adb5049c0725)
#02 pc 0x0000000000008bc8 /data/app/el1/bundle/public/com.example.myapplication/.../libs/arm64/libentry.so(main+120)(BuildId: a40044d0acb68107cfc4adb5049c0725)
#03 pc 0x0000000000004a8c /system/lib/ld-musl-aarch64.so.1(__libc_start_main+88)(BuildId: ...)
#04 pc 0x00000000000049fc /system/lib/ld-musl-aarch64.so.1(__libc_start_init+80)(BuildId: ...)
Registers:
x0: 0x0000000000000000 x1: 0x0000000000000001 x2: 0x0000007ffefff4e0 x3: 0x0000007ffefff528
x4: 0x0000000000000000 x5: 0x0000000000000000 x6: 0x0000000000000000 x7: 0x000000000000006f
x8: 0x0000000000000000 x9: 0x0000000000000000 x10: 0x0000000000000001 x11: 0x0000000000000000
...
pc: 0x0000007fa6b08a4c sp: 0x0000007ffefff4d0 pstate: 0x60000000
Memory near registers:
x0:[+0]: 0x0 0x0 0x0 0x0
[+8]: 0x0 0x0 0x0 0x0
[+16]: 0x0 0x0 0x0 0x0
Stack:
0x0000007ffefff4d0: 0x0000007fa6b08b0c 0x0000000000000001 ...
...
Other thread info:
Tid: 581, Name: os.async.hiapp
#00 pc 0x... /system/lib/...
Fingerprint: @0x0000000000008a4c 0x0000000000008b0c 0x0000000000008bc8...
真实日志可能比这长 2-3 倍(栈深、其他线程多),但骨架就是这些。
5.2 系统支持的崩溃信号表(看 Reason 时对着查)
| signo | 信号 | 含义 | 常见触发原因 |
|---|---|---|---|
| 4 | SIGILL | 非法指令 | 执行未知/特权指令、代码损坏 |
| 5 | SIGTRAP | 断点/陷阱 | 调试断点、单步 |
| 6 | SIGABRT | 进程终止 | 调用 abort()、断言失败 |
| 7 | SIGBUS | 非法内存访问 | 地址未对齐、访问不存在物理地址 |
| 8 | SIGFPE | 浮点异常 | 除 0、算术溢出 |
| 11 | SIGSEGV | 无效内存访问 | 空指针解引用、越界、UAF(最常见) |
| 16 | SIGSTKFLT | 栈错误 | 栈操作异常(不支持 minidump) |
| 31 | SIGSYS | 错误系统调用 | 非法/错误参数的 syscall |
常见二级分类:
SIGSEGV:SEGV_MAPERR(访问不存在地址,多半空指针)、SEGV_ACCERR(无权限地址)SIGABRT:通常由abort()引发,常见于 C++ 异常未 catch、断言失败SIGFPE:FPE_INTDIV(整数除 0)
经验:90% 的 cppcrash 是 SIGSEGV。看到
@0x0000000000000000基本就是空指针。
5.3 调用栈(Backtrace)帧格式
#00 pc 000e8400 /system/lib/ld-musl-arm.so.1(raise+176)(a40044d0acb68107cfc4adb5049c0725)
| 段 | 含义 |
|---|---|
#00 | 栈帧序号,#00 是崩溃点,向上回溯调用链(#01 调用了 #00,以此类推) |
pc 000e8400 | 程序计数器在该文件内的偏移字节数 |
/system/lib/...so | so 库名 / 二进制文件 |
(raise+176) | 函数名 + 函数内偏移(无符号表时只有偏移无函数名) |
(a40044...) | BuildID(二进制唯一标识,符号化时要匹配) |
JS/Native 混合栈帧(64 位支持跨语言):
#05 at onPageShow (entry|har1|1.0.0|src/main/ets/pages/Index.ts:7:13)
→ 函数名 + 模块 + 源文件 行:列号。
5.4 寄存器怎么读(判断空指针的利器)
看 Registers 段,重点盯崩溃指令用到的寄存器:
| 现象 | 大概率原因 |
|---|---|
某寄存器值 = 0x0 或极小(如 0x18) | 空指针/野指针解引用 |
pc 指向的 so 不在进程加载范围 | 代码被破坏/JIT 异常 |
寄存器值像合法地址但 Memory near 全是 0x0/乱码 | 访问已释放内存(UAF) |
配合 Memory near registers 段看:若该地址附近内存全 0 或不可读 → 进一步佐证空指针/野指针。
新手不用全看懂 30 个寄存器,只看有没有 0 就能定性 80% 的 SIGSEGV。
5.5 符号化详细步骤(偏移 → 源码行号)
cppcrash 默认只有偏移(pc 0x8a4c),看不到源码行。两种方式还原:
方式 A:DevEco Studio 自动符号化(推荐)
- 把带符号的构建产物(so)准备好(debug 包自带)
- FaultLog 面板 → 上传符号表 → 自动还原行号
方式 B:命令行 llvm-addr2line(无 Studio 时)
# 工具在 HarmonyOS SDK 的 llvm/bin 目录下,需配 PATH
# -e 指定带符号的 so,-f 显示函数名,-C 显示完整符号,最后是偏移地址(去掉 pc 前缀)
llvm-addr2line -e libentry.so -f -C 0x8a4c
# 输出示例:
# OHOS_NativeCrash
# /path/to/src/native_crash.cpp:42
注意:so 的 BuildID 必须和日志里的一致,否则符号化结果错位。日志里
(BuildId: a40044...)要和本地 so 的 BuildID 对上。
验证 BuildID:llvm-readelf -n libentry.so | grep "Build ID"
5.6 定位步骤小结
- 看 Reason:信号类型(如
SIGSEGV(SEGV_MAPERR)),提示里常带probably caused by NULL pointer dereference。 - 看 #00 帧:崩溃所在函数与偏移。
- 符号化:用带符号的 so +
llvm-addr2line还原行号(或交给 DevEco Studio 自动做)。 - 看 Registers / Memory near registers:确认是否空指针(寄存器值为 0 或极小值)。
- 相同 Fingerprint(标准化栈帧)表示同一故障,可用于聚类去重、统计崩溃率。
六、JS_CRASH 详解(ArkTS 崩溃)
6.1 完整日志样例
Build info: OpenHarmony 5.0.0
Module name: com.example.myapplication
Version: 1.0.0
VersionCode: 1000000
PreInstall: No
Pid: 579
Uid: 0
Reason: TypeError
Error message: Cannot read property c of undefined
Cannot get SourceMap info, dump raw stack:
SourceCode: var a = b.c;
^
Stacktrace:
at onPageShow (entry/src/main/ets/pages/Index.ets:7:13)
at Index (entry/src/main/ets/pages/Index.ets:5:1)
at ... (框架内部栈)
| 字段 | 含义 |
|---|---|
Reason | 异常类型(TypeError / RangeError / ReferenceError / SyntaxError 等) |
Error message | 具体错误信息(如"Cannot read property c of undefined") |
SourceCode | 出错的源码行,箭头 ^ 指到具体出错列 |
Stacktrace | 调用栈,文件:行:列直接定位(最关键) |
6.2 常见 JS 异常类型对照
| Reason | 含义 | 典型场景 |
|---|---|---|
TypeError | 类型错误 | undefined/null 上取属性、调非函数 |
RangeError | 范围错误 | 数组越界、递归过深栈溢出 |
ReferenceError | 引用错误 | 用了未定义的变量 |
SyntaxError | 语法错误 | eval() 执行非法代码 |
Error | 通用错误 | 主动 throw new Error() |
典型根因:
undefined/null上取属性、数组越界、异步回调里访问已释放对象。
注意:release 包不含 sourcemap,Error message可能只能 dump raw stack(行列号对不上源码)。调试用 debug 包更易看行号,或用 DevEco FaultLog 面板解析。
七、APP_FREEZE 详解(应用冻屏)
7.1 完整日志样例
Build info: OpenHarmony 5.0.0
Module name: com.example.myapplication
Version: 1.0.0
Pid: 579
Uid: 20020205
Reason: THREAD_BLOCK_6S
EVENTNAME: THREAD_BLOCK_6S
eventHandler:
runner thread id: 0
queue size: 8
pending task count: 5
duration of longest event in queue: 6234ms
main thread stack:
#00 pc 0x... /system/lib/...so(func+...)
#01 pc 0x... /data/app/.../libentry.so(HeavyWork+...)
...
binder call trace:
...
7.2 两类故障(看 EVENTNAME 字段)
| EVENTNAME | 含义 | 阈值 |
|---|---|---|
THREAD_BLOCK_6S | 主线程卡死超时 | watchdog 3 秒警告、6 秒触发 |
APP_INPUT_BLOCK | 用户输入响应超时 | 输入事件长时间未处理 |
检测原理:watchdog 线程定期向主线程插入"判活检测任务",主线程若被阻塞则无法按时完成 → 触发故障。触发后系统会杀死应用以恢复。
重要约束:AppFreeze 检测仅对 release 版本应用生效,debug 版本不生效。开发自测冻屏问题必须用 release 包。
7.3 日志字段解读
| 字段 | 看什么 |
|---|---|
EVENTNAME | 故障子类型(THREAD_BLOCK_6S / APP_INPUT_BLOCK) |
eventHandler.queue size | 主线程任务队列堆积数(越大越堵) |
pending task count | 待处理任务数 |
duration of longest event | 队列里最耗时的任务时长(定位元凶) |
main thread stack | 主线程当前卡在哪一行(最关键) |
binder call trace | 跨进程调用链(看是否卡在系统服务) |
7.4 常见根因 & 解决方向
| 根因 | 表现 | 方向 |
|---|---|---|
| 主线程耗时操作 | 磁盘 I/O、网络请求、大 JSON 解析、锁竞争、过度绘制 | 挪到 TaskPool / Worker 异步执行 |
| 系统 API 未捕获异常 | 调用系统接口挂起 | 加 try/catch、超时处理 |
| 生命周期回调阻塞 | onCreate/onPageShow 干重活 | 拆分、延迟、异步 |
| 死锁 | 主线程等子线程锁、子线程等主线程锁 | 避免主线程持锁、用无锁结构 |
定位重点:日志里的 eventHandler 任务队列 + 主线程堆栈 + binder 调用链,对比 3 秒与 6 秒的任务堆积。
八、常见崩溃模式速查表(典型签名 → 根因 → 方向)
把这几张表贴墙上,看到签名就能条件反射出方向。
8.1 CPP_CRASH 常见模式
| 日志特征 | 根因 | 修复方向 |
|---|---|---|
SIGSEGV(SEGV_MAPERR)@0x0 + 寄存器全 0 | 空指针解引用 | 加空判断;检查对象是否已释放 |
SIGSEGV(SEGV_ACCERR) + 内存乱码 | 野指针/UAF | 检查生命周期;用智能指针 |
SIGABRT + abort() | C++ 异常未 catch / 断言失败 | 加 try-catch;查 ASSERT 触发点 |
SIGFPE FPE_INTDIV | 整数除 0 | 除数加非 0 判断 |
SIGILL | 代码损坏/跳转到非代码区 | 检查函数指针、回调签名 |
崩在 ld-musl/libc | 内存被踩坏 | 用 ASAN 复测定位越界点 |
8.2 JS_CRASH 常见模式
| Reason + Error message | 根因 | 修复方向 |
|---|---|---|
TypeError: Cannot read property X of undefined | 链式取属性遇 undefined | 加可选链 ?. 或空判断 |
TypeError: X is not a function | 调了非函数/this 丢失 | 箭头函数绑 this;检查导入 |
RangeError: Maximum call stack | 无限递归 | 加终止条件 |
ReferenceError: X is not defined | 变量未声明/导入错 | 检查 import/export |
| 异步回调里崩 | 访问已释放对象 | 检查组件是否已销毁、加 flag |
8.3 APP_FREEZE 常见模式
| 主线程栈特征 | 根因 | 修复方向 |
|---|---|---|
卡在文件读写(read/write) | 同步 I/O | 挪到 TaskPool/Worker |
| 卡在 JSON.parse 大字符串 | 大数据解析 | 分块、流式、子线程 |
卡在 binder 调用 | 系统服务响应慢 | 加超时、减少调用频率 |
卡在锁(pthread_mutex_lock) | 死锁/锁竞争 | 避免主线程持锁 |
| 队列堆积数百任务 | 短时高频任务过载 | 合并、节流、降频 |
九、实战模板(直接抄)
# 1. 跑 wukong 压测时,另开终端实时盯崩溃/冻屏
hdc shell hilog -L E | grep -iE "jscrash|cppcrash|appfreeze|fault"
# 2. 压测结束,导出全部故障日志
hdc file recv /data/log/faultlog/faultlogger ./faultlogs
# 3. 分类查看
ls ./faultlogs | grep -E "cppcrash|jscrash|appfreeze"
# 4. 看某个 cppcrash 的崩溃点与信号
grep -E "Reason|#00|#01" ./faultlogs/cppcrash-*.log
# 5. 看某个 jscrash 的异常信息与调用栈
grep -E "Reason|Error message|Stacktrace" ./faultlogs/jscrash-*.log
# 6. 看 appfreeze 的故障类型与卡死线程
grep -E "EVENTNAME|THREAD_BLOCK|APP_INPUT_BLOCK" ./faultlogs/appfreeze-*.log
# 7. 找不到目录时的确认动作
hdc shell ls /data/log/faultlog/
# 8. cppcrash 符号化(把偏移还原行号)
llvm-addr2line -e <带符号的.so> -f -C <偏移地址,如 0x8a4c>
十、与 wukong / hilog 闭环(完整排障流)
1. hdc 配通(见第1弹)
2. 跑 wukong 稳定性压测(见第2弹)
hdc shell
wukong exec -b com.example.myapp -a 0.3 -t 0.7 -T 30
3. 另开终端实时盯异常
hdc shell hilog -L E | grep -iE "crash|freeze"
4. 发现崩溃 → 导出 faultlog
hdc file recv /data/log/faultlog/faultlogger ./faultlogs
5. 按类型定位:
- CPP_CRASH → 看 Reason 信号 + #00 帧 + 符号化
- JS_CRASH → 看 Error message + SourceCode + Stacktrace
- APP_FREEZE→ 看 EVENTNAME + 主线程堆栈(release 包才生效)
6. 修复后用相同 wukong 种子复测(同种子=可复现,见第2弹)
十一、新手必踩的 8 个坑
- 找不到 faultlog 目录 → 默认路径是
/data/log/faultlog/faultlogger(不是/data/faultlog,后者老版本可能用)。先hdc shell ls /data/log/faultlog/确认。 - appfreeze 怎么都抓不到 → 用的是 debug 包,AppFreeze 只对 release 包生效。改打 release 包再测。
- cppcrash 只有偏移没有行号 → 需要带符号的 so +
llvm-addr2line符号化,或直接在 DevEco Studio 里看(自动符号化)。 - jscrash 看不到源码行 → release 包无 sourcemap,换 debug 包跑,或用 DevEco FaultLog 面板解析。
- 同一崩溃反复刷屏 → 同一进程最多留 10 份,旧的全被顶掉;分析要趁早导出。
- FaultType 值对不上 →
CPP_CRASH=2 / JS_CRASH=3 / APP_FREEZE=4(没有 1,1 是预留),代码里别写错。 - 符号化行号对不上/错位 → so 的 BuildID 和日志不一致。重新用崩溃发生时那次构建的带符号产物;验证:
llvm-readelf -n xxx.so | grep "Build ID"。 - 崩溃栈全是
ld-musl/libc看不懂 → 大概率是内存被踩坏导致栈乱。换 ASAN 包复测(DevEco FaultLog 面板可看 ASAN 报告),能精确报越界点。
记忆口诀:崩溃落 faultlogger,cpp看信号、js看栈、freeze看主线程;debug 不出 freeze,release 才灵;符号化对 BuildID。
十二、标准作业流(SOP)
1. 设备开 USB 调试,hdc 连上(第1弹)
2. 【测试前】备好带符号构建产物 + 确认包类型(debug 查 jscrash/release 查 freeze)
3. 跑压测/复现异常
4. hdc shell hilog | grep -iE "crash|freeze" 实时观察
5. hdc file recv /data/log/faultlog/faultlogger ./faultlogs 导出
6. 按文件前缀分类型:cppcrash / jscrash / appfreeze
7. cppcrash → Reason(信号) + #00帧 + 符号化(对BuildID)
jscrash → Error message + SourceCode + Stacktrace
appfreeze→ EVENTNAME + 主线程堆栈(release 包)
8. 查【第八章 常见模式速查表】对号入座
9. 修复 → 同种子 wukong 复测验证
十三、一句话速记卡(贴显示器上)
faultlog = 鸿蒙版 tombstone/ANR
路径: /data/log/faultlog/faultlogger
类型: CPP_CRASH=2 JS_CRASH=3 APP_FREEZE=4(无1)
获取:
DevEco FaultLog 面板(自动符号化,推荐)
hdc file recv /data/log/faultlog/faultlogger ./logs
HiAppEvent 订阅 FAULT 事件(线上监控)
分析顺序: 文件名→Reason→崩溃点→符号化→复测
cppcrash → Reason(信号SIGSEGV=11/SIGABRT=6) + #00帧 + addr2line
寄存器有0=空指针; BuildID要对上
jscrash → Error message + SourceCode + Stacktrace(文件:行:列)
release无sourcemap, 查行号用debug包
appfreeze→ EVENTNAME(THREAD_BLOCK_6S/APP_INPUT_BLOCK) + 主线程栈
⚠ 仅 release 包生效!
盯日志: hdc shell hilog -L E | grep -iE "crash|freeze"
口诀: cpp看信号 js看栈 freeze看主线程; debug不出freeze release才灵
十四、术语表(看日志遇到不懂的词来这查)
| 术语 | 含义 |
|---|---|
| faultlog | 鸿蒙故障日志统称,对应 Android 的 tombstone/ANR |
| ProcessDump | 系统守护进程,崩溃时由它 dump 现场(寄存器/栈/内存)并落盘 |
| FaultType | 故障类型枚举,@ohos.faultLogger 定义,CPP=2/JS=3/FREEZE=4 |
| Reason | 日志里的"故障原因"行,cppcrash 是信号,jscrash 是异常类型,appfreeze 是事件名 |
| Backtrace / 调用栈 | 崩溃瞬间的函数调用链,#00 是崩溃点,向上是调用者 |
| pc | 程序计数器,崩溃指令在 so 内的偏移地址 |
| BuildID | 二进制文件的唯一标识,符号化时 so 的 BuildID 必须和日志一致 |
| Fingerprint | 标准化栈帧指纹,相同指纹=同一崩溃,用于去重/统计崩溃率 |
| watchdog | 看门狗线程,定期向主线程发判活任务,超时未响应→appfreeze |
| sourcemap | JS 源码映射表,release 包不含,导致 jscrash 看不到行列号 |
| SEGV_MAPERR | SIGSEGV 子类,访问不存在的地址(多半空指针) |
| SEGV_ACCERR | SIGSEGV 子类,访问无权限地址 |
| UAF | Use-After-Free,使用已释放内存,常表现为野指针崩 |
| ASAN | AddressSanitizer,地址越界检测工具,DevEco FaultLog 面板可看报告 |
| binder | 鸿蒙跨进程调用机制,appfreeze 日志里的 binder trace 看是否卡在系统服务 |
| THREAD_BLOCK_6S | 主线程卡死 6 秒触发的 appfreeze 事件名 |
| APP_INPUT_BLOCK | 输入事件响应超时触发的 appfreeze 事件名 |
十五、FAQ(新人高频疑问)
Q1:wukong 跑了半小时没崩,但用户反馈偶发崩溃,怎么复现?
A:faultlog 是"死后现场",要复现得靠 hilog 看"病程"。建议:① 用相同种子重跑 wukong(见第2弹);② 加大压测时长/提高点击比例;③ 接 HiAppEvent 订阅做线上监控,收集真实用户崩溃。
Q2:cppcrash 日志里 #00 帧是 libc/ld-musl,不是我的代码,怎么定位?
A:往上看 #01、#02……找到第一个你的业务 so(如 libentry.so)。系统库崩溃多半是被业务代码踩坏了内存,建议用 ASAN 包复测。
Q3:同一进程崩了好几次,日志只剩最新 10 份,前面的怎么找回?
A:找不回。同进程最多保留 10 份 cppcrash,超出的会被删。压测完立刻 hdc file recv 导出,别攒着。
Q4:jscrash 的 Stacktrace 行列号和我的源码对不上?
A:大概率是 release 包无 sourcemap,显示的是编译后的行列号。换 debug 包跑,或用 DevEco FaultLog 面板上传 sourcemap 解析。
Q5:appfreeze 日志里主线程栈是空的/只有几行系统调用?
A:说明卡在系统调用里(如 binder、I/O)。看 binder call trace 找是哪个系统服务没及时返回;也可能是死锁,看其他线程是否持有主线程需要的锁。
Q6:FaultLog 面板里看到 ASAN 报告,和 cppcrash 啥区别?
A:ASAN 是编译期插桩的内存检测,能精确报"在哪一行越界/UAF",比 cppcrash 的 SIGSEGV 信息更准。复测内存问题时优先用 ASAN 包。但 ASAN 有性能开销,只用于测试,不能上线。
Q7:崩溃日志里 UID 是 20020205,是啥意思?
A:鸿蒙里 20020205 开头的 UID 表示三方应用;系统应用 UID 较小(如 1000 段)。看到这个值说明是用户态三方应用崩溃。
Q8:修完 bug 怎么验证确实修好了?
A:用相同 wukong 种子复测(见第2弹),跑相同或更长时间,确认不再复现同一 Fingerprint 的崩溃。
参考来源
本文为本人实践整理的原创笔记,技术内容参考华为官方文档与社区实践,日志样例为脱敏/示意格式。
- 华为开发者文档:Cpp Crash(进程崩溃)检测
- 华为设备开发文档:AppFreeze(应用冻屏)检测
- 华为开发者文档:@ohos.faultLogger(故障日志获取) · HiAppEvent 应用冻屏事件
- 华为开发者文档:JS/CPP Crash 故障定位(FaultLog 面板)
- 实践参考:JS Crash 日志字段格式(社区验证)、faultlog 落盘路径
/data/log/faultlog/faultlogger、ProcessDump 转存机制
版权与转载声明
本文为原创内容,首发于 CSDN,作者保留全部权利。
- 转载请注明出处并附上原文链接,禁止删改本声明。
- 文中所有日志样例均为脱敏/示意格式,不含任何真实业务代码与生产环境数据。
- 系统命令与 API 以华为官方文档为准,本文基于 OpenHarmony 5.0 版本整理,后续版本如有差异请以官方文档为准。
- 配套系列其余文章(共 13 篇)已在个人主页发布,欢迎关注获取更新。
CSDN 发布标签建议:鸿蒙 · HarmonyOS · faultlog · 崩溃日志 · 移动开发 · 软件测试 · OpenHarmony
推荐专栏分类:移动开发 / 软件测试 / HarmonyOS
觉得有帮助就点个赞 + 收藏,遇到问题欢迎评论区交流。系列持续更新中。
更多推荐




所有评论(0)