1# 分析CppCrash(进程崩溃)
2
3进程崩溃指C/C++运行时崩溃。FaultLogger模块提供进程崩溃故障检测、日志采集、日志存储、日志上报的能力,为开发者提供详细的维测日志以辅助故障定位。
4
5本文将分别介绍进程崩溃检测能力、崩溃问题定位分析思路,以及具体的案例分析。在使用本指导分析处理崩溃日志前,需要开发者了解C/C++程序堆栈信息的基础知识。
6
7## Cpp Crash异常检测能力
8
9进程崩溃基于posix信号机制,目前主要支持对以下崩溃异常信号的处理:
10
11| 信号值(signo) | 信号 | 解释 | 触发原因 |
12| -------- | -------- | -------- | -------- |
13| 4 | SIGILL | 非法指令。 | 进程执行了非法、格式错误、未知或特权指令。 |
14| 5 | SIGTRAP | 断点或陷阱异常。 | 异常或trap指令发生。 |
15| 6 | SIGABRT | 进程终止。 | 进程异常终止,通常为进程自身调用标准函数库的abort()函数。 |
16| 7 | SIGBUS | 非法内存访问。 | 进程访问了对齐或者不存在的物理地址。 |
17| 8 | SIGFPE | 浮点异常。 | 进程执行了错误的算术运算,如除数为0、浮点溢出、整数溢出等。 |
18| 11 | SIGSEGV | 无效内存访问。 | 进程访问了无效内存引用。 |
19| 16 | SIGSTKFLT | 栈错误。 | 处理器执行了错误的栈操作,如栈空时弹出、栈满时压入。 |
20| 31 | SIGSYS | 错误的系统调用。 | 系统调用时使用了错误或非法参数。 |
21
22以上部分故障信号,根据具体的场景还有二级分类(code):
23SIGILL是一个在Unix和类Unix操作系统中的信号,它表示非法指令异常。SIGILL信号通常由以下几种类型的问题场景引起:
24| 二级分类 | 信号字符串 | 解释 | 触发原因 |
25| -------- | -------- | -------- | -------- |
26| 1 | ILL_ILLOPC | 非法操作码异常 | 这种异常通常发生在执行不被CPU支持的指令时,或者在尝试执行特权指令时。 |
27| 2 | ILL_ILLOPN | 非法操作数异常 | 这种异常通常发生在指令使用了不正确的操作数,或者是操作数的类型不正确时。|
28| 3 | ILL_ILLADR | 非法地址异常 | 这种异常通常发生在程序尝试访问无效的内存地址时,或者是在尝试执行未对齐的内存访问时。|
29| 4 | ILL_ILLTRP | 非法陷阱异常 | 这种异常通常发生在程序尝试执行一个非法的陷阱指令时,或者是在尝试执行一个未定义的操作时。|
30| 5 | ILL_PRVOPC | 特权操作码异常 | 这种异常通常发生在普通用户尝试执行特权指令时。|
31| 6 | ILL_PRVREG | 特权寄存器异常 | 这种异常通常发生在普通用户尝试访问特权寄存器时。|
32| 7 | ILL_COPROC | 协处理器异常 | 这种异常通常发生在程序尝试使用未定义的协处理器指令时。|
33| 8 | ILL_BADSTK | 无效的堆栈异常 | 这种异常通常发生在程序尝试在无效的堆栈地址上执行操作时,或者是在堆栈溢出时。|
34
35SIGTRAP信号通常用于调试和跟踪程序的执行。下面是上面列出的四种SIGTRAP信号类别的问题场景介绍:
36| 二级分类 | 信号字符串 | 解释 | 触发原因 |
37| -------- | -------- | -------- | -------- |
38| 1 | TRAP_BRKPT | 软件断点 | 这个信号是由软件断点引起的,当程序执行到设置的断点时会触发该信号。软件断点通常用于调试程序,可以在程序的关键位置设置断点,以便在调试时暂停程序的执行并检查变量值等信息。|
39| 2 | TRAP_TRACE | 单步调试 | 这个信号是由单步执行引起的,当程序执行单个指令时会触发该信号。单步执行通常用于调试程序,可以逐步执行程序并检查每个指令的执行结果。|
40| 3 | TRAP_BRANCH | 分支跟踪 | 这个信号是由分支指令引起的,当程序执行分支指令时会触发该信号。分支指令通常用于控制程序的执行流程,例如if语句和循环语句等。|
41| 4 | TRAP_HWBKPT | 硬件断点 | 这个信号是由硬件断点引起的,当程序执行到设置的硬件断点时会触发该信号。硬件断点通常用于调试程序,可以在程序的关键位置设置断点,以便在调试时暂停程序的执行并检查变量值等信息。与软件断点不同的是,硬件断点是由CPU硬件实现的,因此可以在程序执行过程中实时检测断点是否被触发。|
42
43SIGBUS是一种由操作系统向进程发送的信号,通常表示内存访问错误。其中,不同的信号类别表示不同的错误场景:
44| 二级分类 | 信号字符串 | 解释 | 触发原因 |
45| -------- | -------- | -------- | -------- |
46| 1 | BUS_ADRALN | 内存地址对齐错误 | 这种错误通常发生在尝试访问未对齐的内存地址时,例如尝试访问一个4字节整数的非偶数地址。|
47| 2 | BUS_ADRERR | 非法内存地址错误 | 这种错误通常发生在尝试访问不属于进程地址空间的内存地址时,例如尝试访问一个空指针。|
48| 3 | BUS_OBJERR | 对象访问错误 | 这种错误通常发生在尝试访问一个已经被删除或未初始化的对象时。|
49| 4 | BUS_MCEERR_AR | 硬件内存校验错误 | 发生在访问内存时检测到校验和错误。|
50| 5 | BUS_MCEERR_AO | 硬件内存校验错误 | 发生在访问内存时检测到地址和校验和错误。|
51
52SIGFPE是一个信号,它表示浮点异常或算术异常。下面是这些SIGFPE信号类别的问题场景:
53| 二级分类 | 信号字符串 | 解释 | 触发原因 |
54| -------- | -------- | -------- | -------- |
55| 1 | FPE_INTDIV | 整数除法错误 | 这个信号表示整数除法中的除数为零的情况。当一个程序尝试进行整数除法,但除数为零时,会发出这个信号。|
56| 2 | FPE_INTOVF | 整数溢出错误 | 这个信号表示整数除法中的除数为负数的情况。当一个程序尝试进行整数除法,但除数为负数时,会发出这个信号。|
57| 3 | FPE_FLTDIV | 浮点除法错误 | 这个信号表示浮点数除法中的除数为零的情况。当一个程序尝试进行浮点数除法,但除数为零时,会发出这个信号。|
58| 4 | FPE_FLTOVF | 浮点溢出错误 | 这个信号表示浮点数除法中的除数为负数的情况。当一个程序尝试进行浮点数除法,但除数为负数时,会发出这个信号。|
59| 5 | FPE_FLTUND | 浮点下溢错误 | 这个信号表示浮点数除法中的除数为零的情况。当一个程序尝试进行浮点数除法,但除数为零时,会发出这个信号。|
60| 6 | FPE_FLTRES | 浮点结果未定义错误 | 这个信号表示浮点数除法中的除数为正数的情况。当一个程序尝试进行浮点数除法,但除数为正数时,会发出这个信号。|
61| 7 | FPE_FLTINV | 无效浮点操作错误 | 这个信号表示浮点数除法中的除数为负数的情况。当一个程序尝试进行浮点数除法,但除数为负数时,会发出这个信号。|
62| 8 | FPE_FLTSUB | 浮点陷阱错误 | 这个信号表示浮点数除法中的除数为零的情况。当一个程序尝试进行浮点数除法,但除数为零时,会发出这个信号。|
63
64SIGSEGV是一种信号,它表示进程试图访问一个不属于它的内存地址,或者试图访问一个已被操作系统标记为不可访问的内存地址。SIGSEGV信号通常是由以下两种情况引起的:
65| 二级分类 | 信号字符串 | 解释 | 触发原因 |
66| -------- | -------- | -------- | -------- |
67| 1 | SEGV_MAPERR | 不存在的内存地址 | 进程试图访问一个不存在的内存地址,或者试图访问一个没有映射到进程地址空间的内存地址。这种情况通常是由于程序中的指针错误或内存泄漏引起的。|
68| 2 | SEGV_ACCERR | 不可访问的内存地址 | 进程试图访问一个已被操作系统标记为不可访问的内存地址,例如只读内存或没有执行权限的内存。这种情况通常是由于程序中的缓冲区溢出或者试图修改只读内存等错误引起的。|
69
70二级分类(code)除了以上根据信号值(signo)维度分类,还可以根据信号产生的原因维度分类。其中根据信号值(signo)维度分类是每个信号值(signo)特有的,根据信号产生的原因维度分类是所有信号值(signo)共有的,当前已有信号产生原因分类的code值如下:
71| 二级分类 | 信号字符串 | 解释 | 触发原因 |
72| -------- | -------- | -------- | -------- |
73| 0 | SI_USER | 用户空间信号 |该信号是由用户空间的进程发送给另一个进程的,通常是通过 kill() 系统调用发送的。例如,当用户在终端中按下Ctrl+C时,会发送一个SIGINT信号给前台进程组中的所有进程。|
74| 0x80 | SI_KERNEL | 内核信号 |该信号是由内核发送给进程的,通常是由内核检测到某些错误或异常情况时发出的。例如,当进程访问无效的内存地址或者执行非法指令时,内核会发送一个SIGSEGV信号给进程。|
75| -1 | SI_QUEUE | sigqueue()函数信号 |该信号是由sigqueue()系统调用发送的,可以携带一个附加的整数值和一个指针。通常用于进程间高级通信,例如传递数据或者通知进程某个事件已经发生。|
76| -2 | SI_TIMER | 定时器信号 |该信号是由定时器发送的,通常用于定时任务或者周期性任务的执行。例如,当一个定时器到期时,内核会向进程发送一个SIGALRM信号。|
77| -3 | SI_MESGQ | 消息队列信号 |该信号是由消息队列发送的,通常用于进程间通信。例如,当一个进程向一个消息队列发送消息时,内核会向接收进程发送一个SIGIO信号。|
78| -4 | SI_ASYNCIO | 异步I/O信号 |该信号是由异步I/O操作发送的,通常用于非阻塞I/O操作。例如,当一个文件描述符上的I/O操作完成时,内核会向进程发送一个SIGIO信号。|
79| -5 | SI_SIGIO | 同步I/O信号 |该信号是由异步I/O操作发送的,通常用于非阻塞I/O操作。例如,当一个文件描述符上的I/O操作完成时,内核会向进程发送一个SIGIO信号。|
80| -6 | SI_TKILL | tkill()函数信号 |该信号是由tkill()系统调用发送的,与kill()系统调用类似,但是可以指定发送信号的线程ID。通常用于多线程程序中,向指定线程发送信号。|
81
82## 问题定位步骤与思路
83
84### 崩溃日志获取
85
86进程崩溃日志是一种故障日志,与应用无响应日志、JS应用崩溃等都由FaultLogger模块进行管理,可通过以下方式获取:
87
88- 方式一:通过DevEco Studio获取日志
89
90    DevEco Studio会收集设备`/data/log/faultlog/faultlogger/`路径下的进程崩溃故障日志到FaultLog下,根据进程名和故障和时间分类显示。获取日志的方法参见:<!--RP1-->[DevEco Studio使用指南-FaultLog](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-fault-log-V5)<!--RP1End-->。
91
92- 方式二:通过hiAppEvent接口订阅
93
94    hiAppEvent 提供了故障订阅接口,可以订阅各类故障打点,详见[HiAppEvent介绍](hiappevent-intro.md)。
95
96<!--Del-->
97- 方式三:设备ROOT模式下通过shell获取日志
98
99    1. 进程崩溃后,系统会在设备`/data/log/faultlog/temp/`路径下的故障日志,其文件名格式为`cppcrash-进程PID-系统毫秒级时间戳`,日志内容包含进程崩溃调用栈,进程崩溃现场寄存器、栈内存、maps,进程文件句柄列表等信息。
100
101        ![cppcrash-temp-log](figures/20230407111853.png)
102
103    2. CppCrash故障会同步在`/data/log/faultlog/faultlogger/`路径下生成一份完善日志,故障日志文件名格式为`cppcrash-进程名-进程UID-秒级时间`,日志内容较`/data/log/faultlog/temp`下日志更加完善,增加有设备名,系统版本,进程流水日志等信息。
104
105        ![cppcrash-faultlogger-log](figures/20230407112159.png)
106
107<!--DelEnd-->
108**日志格式 - 空指针故障场景**
109该场景会在日志中打印出提示信息,表明故障很有可能是因为空指针解引用导致
110以下是一份DevEco Studio归档在FaultLog的进程崩溃日志的核心内容,与`设备/data/log/faultlog/faultlogger`下归档的日志内容相同。
111
112```
113Generated by HiviewDFX@OpenHarmony
114================================================================
115Device info:OpenHarmony 3.2        <- 设备信息
116Build info:OpenHarmony 5.0.0.23    <- 版本信息
117Fingerprint:cdf52fd0cc328fc432459928f3ed8edfe8a72a92ee7316445143bed179138073 <- 标识故障特征
118Module name:crasher_cpp            <- 模块名
119Timestamp:2024-05-06 20:10:51.000  <- 故障发生时间戳
120Pid:9623   <- 进程号
121Uid:0         <- 用户ID
122Process name:./crasher_cpp         <- 进程名称
123Process life time:1s               <- 进程存活时间
124Reason:Signal:SIGSEGV(SEGV_MAPERR)@0x00000004  probably caused by NULL pointer dereference   <- 故障原因和空指针提示
125Fault thread info:
126Tid:9623, Name:crasher_cpp         <- 故障线程号,线程名
127#00 pc 00008d22 /system/bin/crasher_cpp(TestNullPointerDereferenceCrash0()+22)(adfc673300571d2da1e47d1d12f48b44)  <- 调用栈
128#01 pc 000064d1 /system/bin/crasher_cpp(DfxCrasher::ParseAndDoCrash(char const*) const+160)(adfc673300571d2da1e47d1d12f48b44)
129#02 pc 00006569 /system/bin/crasher_cpp(main+92)(adfc673300571d2da1e47d1d12f48b44)
130#03 pc 00072b98 /system/lib/ld-musl-arm.so.1(libc_start_main_stage2+56)(d820b1827e57855d4f9ed03ba5dfea83)
131#04 pc 00004e28 /system/bin/crasher_cpp(_start_c+84)(adfc673300571d2da1e47d1d12f48b44)
132#05 pc 00004dcc /system/bin/crasher_cpp(adfc673300571d2da1e47d1d12f48b44)
133Registers:   <- 故障现场寄存器
134r0:ffffafd2 r1:00000004 r2:00000001 r3:00000000
135r4:ffd27e39 r5:0096e000 r6:00000a40 r7:0096fdfc
136r8:f7ba58d5 r9:f7baea86 r10:f7cadd38
137fp:ffd27308 ip:f7cb2078 sp:ffd272a0 lr:f7c7ab98 pc:0096ad22
138Memory near registers:  <-  故障现场寄存器附近内存
139r4([stack]):
140    ffd27e30 72656873
141    ffd27e34 7070635f
142    ...
143    ffd27eac 3d73746f
144r5(/system/bin/crasher_cpp):
145    0096dff8 00000000
146    0096dffc 0096717d
147    ...
148    0096e074 00000000
149r7(/system/lib/ld-musl-arm.so.1):
150    f7cabb58 00000000
151    f7cabb5c 0034ba00
152    ...
153    f7cabbd4 00000000
154r8(/system/lib/ld-musl-arm.so.1):
155    f7ba58cc 63637573
156    f7ba58d0 2e737365
157    ...
158    f7ba5948 70206269
159r9(/system/lib/ld-musl-arm.so.1):
160    f7baea7c 20746f6e
161    f7baea80 6e756f66
162    ...
163    f7baeaf8 25206e69
164r10([anon:ld-musl-arm.so.1.bss]):
165    f7cadd30 00000000
166    f7cadd34 00000000
167    ...
168    f7caddac 00000000
169r12([anon:ld-musl-arm.so.1.bss]):
170    f7cb2070 56726562
171    f7cb2074 65756c61
172    ...
173    f7cb20ec 00000000
174sp([stack]):
175    ffd27328 00000000
176    ffd2732c 00966dd0
177    ...
178    ffd273a4 00000004
179pc(/system/bin/crasher_cpp):
180    00966dc8 e1a0d00c
181    00966dcc eb000000
182    ...
183    00966e44 e5907008
184pc(/system/bin/crasher_cpp):
185    00966dc8 e1a0d00c
186    00966dcc eb000000
187    ...
188    00966e44 e5907008
189FaultStack:   <- 崩溃线程的栈地址空间
190    ffd27260 00000000
191    ffd27264 f7cac628
192    ...
193    ffd2729c 0096ad1f
194sp0:ffd272a0 0096fdfc <- #00栈顶
195    ffd272a4 009684d3
196sp1:ffd272a8 00000001
197    ffd272ac 73657408
198    ffd272b0 f7590074
199    ...
200    ffd272dc 0096856d
201sp2:ffd272e0 ffd27334
202    ffd272e4 ffd27334
203    ffd272e8 00000002
204    ....
205    ffd272f4 f7bfbb9c
206sp3:ffd272f8 00000000
207    ffd272fc ffd27334
208
209Maps:   <-  故障时进程maps
210962000-966000 r--p 00000000 /system/bin/crasher_cpp
211966000-96c000 r-xp 00003000 /system/bin/crasher_cpp
21296c000-96f000 r--p 00008000 /system/bin/crasher_cpp
21396f000-970000 rw-p 0000a000 /system/bin/crasher_cpp
214149f000-14a0000 ---p 00000000 [heap]
21514a0000-14a2000 rw-p 00000000 [heap]
216...
217f7b89000-f7be1000 r--p 00000000 /system/lib/ld-musl-arm.so.1
218f7be1000-f7ca9000 r-xp 00057000 /system/lib/ld-musl-arm.so.1
219f7ca9000-f7cab000 r--p 0011e000 /system/lib/ld-musl-arm.so.1
220f7cab000-f7cad000 rw-p 0011f000 /system/lib/ld-musl-arm.so.1
221f7cad000-f7cbc000 rw-p 00000000 [anon:ld-musl-arm.so.1.bss]
222ffd07000-ffd28000 rw-p 00000000 [stack]
223ffff0000-ffff1000 r-xp 00000000 [vectors]
224OpenFiles:   <-  故障时进程打开文件Fd信息
2250->/dev/pts/1 native object of unknown type 0
2261->/dev/pts/1 native object of unknown type 0
2272->/dev/pts/1 native object of unknown type 0
2283->socket:[67214] native object of unknown type 0
229...
23011->pipe:[67219] native object of unknown type 0
23112->socket:[29074] native object of unknown type 0
23225->/dev/ptmx native object of unknown type 0
23326->/dev/ptmx native object of unknown type 0
234
235HiLog:   <-  故障时的Hilog日志
23605-06 20:10:51.301  9623  9623 E C03f00/MUSL-SIGCHAIN: signal_chain_handler call 2 rd sigchain action for signal: 11
23705-06 20:10:51.306  9623  9623 I C02d11/DfxSignalHandler: DFX_SigchainHandler :: sig(11), pid(9623), tid(9623).
23805-06 20:10:51.307  9623  9623 I C02d11/DfxSignalHandler: DFX_SigchainHandler :: sig(11), pid(9623), processName(./crasher_cpp), threadName(crasher_cpp).
23905-06 20:10:51.389  9623  9623 I C02d11/DfxSignalHandler: processdump have get all resgs
240
241```
242
243<!--Del-->
244通过Shell获取的`/data/log/faultlog/temp`获取到的日志内容格式如下:
245
246```
247Timestamp:2024-05-06 20:10:51.000  <- 故障发生时间戳
248Pid:9623                           <- 进程号
249Uid:0                              <- 用户ID
250Process name:./crasher_cpp         <- 进程名称
251Process life time:1s               <- 进程存活时间
252Reason:Signal:SIGSEGV(SEGV_MAPERR)@0x00000004  probably caused by NULL pointer dereference   <- 故障原因和空指针提示
253Fault thread info:
254Tid:9623, Name:crasher_cpp         <- 故障线程号,线程名
255#00 pc 00008d22 /system/bin/crasher_cpp(TestNullPointerDereferenceCrash0()+22)(adfc673300571d2da1e47d1d12f48b44)  <- 调用栈
256#01 pc 000064d1 /system/bin/crasher_cpp(DfxCrasher::ParseAndDoCrash(char const*) const+160)(adfc673300571d2da1e47d1d12f48b44)
257#02 pc 00006569 /system/bin/crasher_cpp(main+92)(adfc673300571d2da1e47d1d12f48b44)
258#03 pc 00072b98 /system/lib/ld-musl-arm.so.1(libc_start_main_stage2+56)(d820b1827e57855d4f9ed03ba5dfea83)
259#04 pc 00004e28 /system/bin/crasher_cpp(_start_c+84)(adfc673300571d2da1e47d1d12f48b44)
260#05 pc 00004dcc /system/bin/crasher_cpp(adfc673300571d2da1e47d1d12f48b44)
261Registers:   <- 故障现场寄存器
262r0:ffffafd2 r1:00000004 r2:00000001 r3:00000000
263r4:ffd27e39 r5:0096e000 r6:00000a40 r7:0096fdfc
264r8:f7ba58d5 r9:f7baea86 r10:f7cadd38
265fp:ffd27308 ip:f7cb2078 sp:ffd272a0 lr:f7c7ab98 pc:0096ad22
266Memory near registers:  <-  故障现场寄存器附近内存
267r4([stack]):
268    ffd27e30 72656873
269    ffd27e34 7070635f
270    ...
271    ffd27eac 3d73746f
272r5(/system/bin/crasher_cpp):
273    0096dff8 00000000
274    0096dffc 0096717d
275    ...
276    0096e074 00000000
277r7(/system/lib/ld-musl-arm.so.1):
278    f7cabb58 00000000
279    f7cabb5c 0034ba00
280    ...
281    f7cabbd4 00000000
282r8(/system/lib/ld-musl-arm.so.1):
283    f7ba58cc 63637573
284    f7ba58d0 2e737365
285    ...
286    f7ba5948 70206269
287r9(/system/lib/ld-musl-arm.so.1):
288    f7baea7c 20746f6e
289    f7baea80 6e756f66
290    ...
291    f7baeaf8 25206e69
292r10([anon:ld-musl-arm.so.1.bss]):
293    f7cadd30 00000000
294    f7cadd34 00000000
295    ...
296    f7caddac 00000000
297r12([anon:ld-musl-arm.so.1.bss]):
298    f7cb2070 56726562
299    f7cb2074 65756c61
300    ...
301    f7cb20ec 00000000
302sp([stack]):
303    ffd27328 00000000
304    ffd2732c 00966dd0
305    ...
306    ffd273a4 00000004
307pc(/system/bin/crasher_cpp):
308    00966dc8 e1a0d00c
309    00966dcc eb000000
310    ...
311    00966e44 e5907008
312pc(/system/bin/crasher_cpp):
313    00966dc8 e1a0d00c
314    00966dcc eb000000
315    ...
316    00966e44 e5907008
317FaultStack:   <- 崩溃线程的栈地址空间
318    ffd27260 00000000
319    ffd27264 f7cac628
320    ...
321    ffd2729c 0096ad1f
322sp0:ffd272a0 0096fdfc <- #00栈顶
323    ffd272a4 009684d3
324sp1:ffd272a8 00000001
325    ffd272ac 73657408
326    ffd272b0 f7590074
327    ...
328    ffd272dc 0096856d
329sp2:ffd272e0 ffd27334
330    ffd272e4 ffd27334
331    ffd272e8 00000002
332    ....
333    ffd272f4 f7bfbb9c
334sp3:ffd272f8 00000000
335    ffd272fc ffd27334
336
337Maps:   <-  故障时进程maps
338962000-966000 r--p 00000000 /system/bin/crasher_cpp
339966000-96c000 r-xp 00003000 /system/bin/crasher_cpp
34096c000-96f000 r--p 00008000 /system/bin/crasher_cpp
34196f000-970000 rw-p 0000a000 /system/bin/crasher_cpp
342149f000-14a0000 ---p 00000000 [heap]
34314a0000-14a2000 rw-p 00000000 [heap]
344...
345f7b89000-f7be1000 r--p 00000000 /system/lib/ld-musl-arm.so.1
346f7be1000-f7ca9000 r-xp 00057000 /system/lib/ld-musl-arm.so.1
347f7ca9000-f7cab000 r--p 0011e000 /system/lib/ld-musl-arm.so.1
348f7cab000-f7cad000 rw-p 0011f000 /system/lib/ld-musl-arm.so.1
349f7cad000-f7cbc000 rw-p 00000000 [anon:ld-musl-arm.so.1.bss]
350ffd07000-ffd28000 rw-p 00000000 [stack]
351ffff0000-ffff1000 r-xp 00000000 [vectors]
352OpenFiles:   <-  故障时进程打开文件Fd信息
3530->/dev/pts/1 native object of unknown type 0
3541->/dev/pts/1 native object of unknown type 0
3552->/dev/pts/1 native object of unknown type 0
3563->socket:[67214] native object of unknown type 0
357...
35811->pipe:[67219] native object of unknown type 0
35912->socket:[29074] native object of unknown type 0
36025->/dev/ptmx native object of unknown type 0
36126->/dev/ptmx native object of unknown type 0
362```
363<!--DelEnd-->
364**日志格式 - 栈溢出故障场景**
365该场景会在日志中打印出提示信息,表明故障很有可能是因为栈溢出导致。核心日志如下:
366
367```
368Generated by HiviewDFX@OpenHarmony
369================================================================
370Device info:OpenHarmony 3.2            <- 设备信息
371Build info:OpenHarmony 5.0.0.23        <- 版本信息
372Fingerprint:8bc3343f50024204e258b8dce86f41f8fcc50c4d25d56b24e71fe26c0a23e321  <- 标识故障特征
373Module name:crasher_cpp                <- 模块名
374Timestamp:2024-05-06 20:18:24.000      <- 故障发生时间戳
375Pid:9838                               <- 进程号
376Uid:0                                  <- 用户ID
377Process name:./crasher_cpp             <- 进程名称
378Process life time:2s                   <- 进程存活时间
379Reason:Signal:SIGSEGV(SEGV_ACCERR)@0xf76b7ffc  current thread stack low address = 0xf76b8000, probably caused by stack-buffer-overflow    <- 故障原因和栈溢出提示
380...
381```
382
383**日志格式 - 栈覆盖故障场景**
384在栈覆盖场景下,由于栈上内存被踩,无法成功回溯栈帧,该场景会在日志中打印出提示信息,说明回栈失败并尝试从线程栈里解析获取不可靠的调用栈,尽可能提供开发者信息以分析问题。核心日志如下:
385
386```
387Generated by HiviewDFX@OpenHarmony
388================================================================
389Device info:OpenHarmony 3.2               <- 设备信息
390Build info:OpenHarmony 5.0.0.23           <- 版本信息
391Fingerprint:79b6d47b87495edf27135a83dda8b1b4f9b13d37bda2560d43f2cf65358cd528    <- 标识故障特征
392Module name:crasher_cpp                   <- 模块名
393Timestamp:2024-05-06 20:27:23.2035266415  <- 故障发生时间戳
394Pid:10026                                 <- 进程号
395Uid:0                                     <- 用户ID
396Process name:./crasher_cpp                <- 进程名称
397Process life time:1s                      <- 进程存活时间
398Reason:Signal:SIGSEGV(SEGV_MAPERR)@0000000000  probably caused by NULL pointer dereference      <- 故障原因
399LastFatalMessage: Failed to unwind stack, try to get unreliable call stack from #02 by reparsing thread stack   <- 尝试从线程栈里获取不可靠的堆栈
400Fault thread info:
401Tid:10026, Name:crasher_cpp               <- 故障线程号,线程名
402#00 pc 00000000 Not mapped
403#01 pc 00008d22 /system/bin/crasher_cpp(TestNullPointerDereferenceCrash0()+22)(adfc673300571d2da1e47d1d12f48b44)  <- 调用栈
404#02 pc 000064d1 /system/bin/crasher_cpp(DfxCrasher::ParseAndDoCrash(char const*) const+160)(adfc673300571d2da1e47d1d12f48b44)
405#03 pc 00006569 /system/bin/crasher_cpp(main+92)(adfc673300571d2da1e47d1d12f48b44)
406#04 pc 00072b98 /system/lib/ld-musl-arm.so.1(libc_start_main_stage2+56)(d820b1827e57855d4f9ed03ba5dfea83)
407...
408```
409
410**日志格式 - 异步线程场景故障**
411(目前支持ARM64架构,且在调试应用(HAP_DEBUGGABLE)下开启)
412当异步线程发生崩溃后,把提交该异步任务的线程的栈也打印出来,帮助定位由于异步任务提交者造成的崩溃问题。崩溃线程的调用栈和其提交线程的调用栈用SubmitterStacktrace分割开。核心日志如下:
413
414```
415Generated by HiviewDFX@OpenHarmony
416================================================================
417Device info:OpenHarmony 3.2                 <- 设备信息
418Build info:OpenHarmony 5.0.0.23             <- 版本信息
419Fingerprint:8bc3343f50024204e258b8dce86f41f8fcc50c4d25d56b24e71fe26c0a23e321  <- 标识故障特征
420Module name:crasher_cpp                     <- 模块名
421Timestamp:2024-05-06 20:28:24.000           <- 故障发生时间戳
422Pid:9838                                    <- 进程号
423Uid:0                                       <- 用户ID
424Process name:./crasher_cpp                  <- 进程名称
425Process life time:2s                        <- 进程存活时间
426Reason:Signal:SIGSEGV(SI_TKILL)@0x000000000004750  from:18256:0  <- 故障原因
427Fault thread info:
428Tid:18257, Name:crasher_cpp                 <- 故障线程号,线程名
429#00 pc 000054e6 /system/bin/ld-musl-aarch64.so.l(raise+228)(adfc673300571d2da1e47d1d12f48b44)  <- 调用栈
430#01 pc 000054f9 /system/bin/crasher_cpp(CrashInSubThread(void*)+56)(adfc673300571d2da1e47d1d12f48b50)
431#02 pc 000054f9 /system/bin/ld-musl-aarch64.so.l(start+236)(adfc673300571d2da1e47d1d12f48b44)
432========SubmitterStacktrace========       <- 任务异常时打印任务提交者调用栈
433#00 pc 000094dc /system/bin/crasher_cpp(DfxCrasher::AsyncStacktrace()+36)(adfc673300571d2da1e47d1d12f48b50)
434#01 pc 00009a58 /system/bin/crasher_cpp(DfxCrasher::ParseAndDoCrash(char const*) const+232)(adfc673300571d2da1e47d1d12f48b50)
435#02 pc 00009b40 /system/bin/crasher_cpp(main+140)(adfc673300571d2da1e47d1d12f48b50)
436#03 pc 0000a4e1c /system/bin/ld-musl-aarch64.so.l(libc_start_main_stage2+68)(adfc673300571d2da1e47d1d12f48b44)
437...
438```
439
440### 基于崩溃栈定位行号
441
442#### 方式一:DevEco Studio 开发者环境下,支持调用栈直接跳转到对应行号
443
444在应用开发场景,对于应用自身的动态库,生成的cppcrash堆栈可直接跳转到代码行处,支持Native栈帧和JS栈帧,无需开发者自行进行解行号操作。对于部分未能解析跳转到对应行号的栈帧,可参考方式二解析。
445
446![cppcrash-addr2line1](figures/cppcrash_image_002.png)
447
448#### 方式二:通过SDK llvm-addr2line 工具定位行号
449
4501. 获取符号表
451    获取崩溃栈中so文件对应的带符号版本,保证与应用/系统内运行时的so文件版本一致。
452    对于应用自身的动态库,经DevEco编译构建,生成在工程的 /build/default/intermediates/libs 目录下,默认是带符号的版本。可通过Linux file 命令查询二进制文件的 BuildID 以核对是否匹配。其中,BuildID 是用于标识二进制文件的唯一标识符,通常由编译器在编译时生成,not stripped 表示该动态库是包含符号表的。
453
454    ```
455    $ file libbabel.so
456    libbabel.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=fdb1b5432b9ea4e2a3d29780c3abf30e2a22da9d, with debug_info, not stripped
457    ```
458
459    **说明:**对于系统动态库符号表,随版本进行归档。
460
4612. 通过 llvm-addr2line 工具定位行号
462    llvm-addr2line 工具归档在:`[SDK DIR PATH]\OpenHarmony\11\native\llvm\bin` 路径下。根据实际的SDK版本路径略有不同,开发者请自行识别或在路径下搜索。
463    例如有堆栈如下(有省略):
464
465    ```
466    Generated by HiviewDFX@OpenHarmony
467    ================================================================
468    Device info:OpenHarmony 3.2
469    Build info:OpenHarmony 5.0.0.22
470    Fingerprint:50577c0a1a1b5644ac030ba8f08c241cca0092026b59f29e7b142d5d4d5bb934
471    Module name:com.samples.recovery
472    Version:1.0.0
473    VersionCode:1000000
474    PreInstalled:No
475    Foreground:No
476    Timestamp:2017-08-05 17:03:40.000
477    Pid:2396
478    Uid:20010044
479    Process name:com.samples.recovery
480    Process life time:7s
481    Reason:Signal:SIGSEGV(SEGV_MAPERR)@0000000000  probably caused by NULL pointer dereference
482    Tid:2396, Name:amples.recovery
483    # 00 pc 00003510 /data/storage/el1/bundle/libs/arm/libentry.so(TriggerCrash(napi_env__*, napi_callback_info__*)+24)(446ff75d3f6a518172cc52e8f8055650b02b0e54)
484    # 01 pc 0002b0c5 /system/lib/platformsdk/libace_napi.z.so(panda::JSValueRef ArkNativeFunctionCallBack<true>(panda::JsiRuntimeCallInfo*)+448)(a84fbb767fd826946623779c608395bf)
485    # 02 pc 001e7597 /system/lib/platformsdk/libark_jsruntime.so(panda::ecmascript::EcmaInterpreter::RunInternal(panda::ecmascript::JSThread*, unsigned char const*, unsigned long long*)+14710)(106c552f6ce4420b9feac95e8b21b792)
486    # 03 pc 001e0439 /system/lib/platformsdk/libark_jsruntime.so(panda::ecmascript::EcmaInterpreter::Execute(panda::ecmascript::EcmaRuntimeCallInfo*)+984)(106c552f6ce4420b9feac95e8b21b792)
487    ...
488    # 39 pc 00072998 /system/lib/ld-musl-arm.so.1(libc_start_main_stage2+56)(5b1e036c4f1369ecfdbb7a96aec31155)
489    # 40 pc 00005b48 /system/bin/appspawn(_start_c+84)(cb0631260fa74df0bc9b0323e30ca03d)
490    # 41 pc 00005aec /system/bin/appspawn(cb0631260fa74df0bc9b0323e30ca03d)
491    Registers:
492    r0:00000000 r1:ffc47af8 r2:00000001 r3:f6555c94
493    r4:00000000 r5:f4d90f64 r6:bd8434f8 r7:00000000
494    r8:00000000 r9:ffc48808 r10:ffc47b70
495    fp:f7d8a5a0 ip:00000000 sp:ffc47aac lr:f4d6b0c7 pc:bd843510
496    ```
497
498    基于SDK llvm-addr2line解析行号如下所示:
499
500    ```
501    [SDK DIR PATH]\OpenHarmony\11\native\llvm\bin> .\llvm-addr2line.exe -Cfie libentry.so 3150
502    TrggerCrash(napi_env__*, napi_callback_info__*)
503    D:/code/apprecovery-demo/entry/src/main/cpp/hello.cpp:48
504    ```
505
506    llvm-addr2line 逐行解析的命令为:`llvm-addr2line.exe -fCpie libutils.z.so 偏移量`,偏移量可以多个一起解:`llvm-addr2line.exe -fCpie libxxx.so 0x1bc868 0x1be28c xxx`。使用llvm-addr2line后,如果得出的行号看起来不是很正确,可以考虑对 地址进行微调(如减1),或者考虑关闭一些编译优化。
507
508#### 方式三:通过 DevEco Studio hstack 工具解析堆栈信息
509
510hstack是DevEco Studio为开发人员提供的用于将release应用混淆后的crash堆栈还原为源码对应堆栈的工具,支持Windows、Mac、Linux三个平台。[DevEco Studio hstack使用指南](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-command-line-hstack-V5)
511
512### 结合业务检视代码
513
514根据基于崩溃栈定位行号章节中介绍的三种方式获取到栈顶对应的行号后,回到代码中,检视上下文。如下图所示,hello.cpp中的48行是一个空指针解引用的代码问题。
515
516![cppcrash-demo1](figures/cppcrash_image_004.png)
517
518本场景是一个故障构造的应用,实际的场景往往不会这么简单,需要结合实际业务进行分析。
519
520### 反汇编(可选)
521
522一般而言,如果是比较明确的问题,反编译定位到代码行就能够定位;较少数的情况,比如定位到某一行里面调用的方法有多个参数,参数又涉及到结构体等,就需要借助反汇编来进一步分析。
523
524参考案例
525
526CPPCRASH日志头部信息如下:
527
528```text
529Process name:com.ohos.medialibrary.medialibrarydata
530
531Process life time:13402s
532
533Reason:SIGSEGV(SEGV_MAPERR)@0x0000005b3b46c000
534
535Fault thread info:
536
537Tid:48552, Name:UpradeTask
538
539#00 pc 00000000000a87e4 /system/lib/ld-musl-aarch64.so.1(memcpy+356)(3c3e7fb27680dc2ee99aa08dd0f81e85)
540
541...
542```
543
544分析步骤:
545
5461. 根据pc寄存器地址找到对应的汇编指令,根据汇编指令找到当前操作
547
548    在CPPCRASH日志文件中找到栈顶的PC地址,并反汇编对应的ELF(使用unstrip的so,llvm-objdump -d -l xxx.so)。
549
550    例如参考案例在执行00000000000a87e4地址对应的指令时发生data_abort,反编译对应buildId(3c3e7fb27680dc2ee99aa08dd0f81e85)的libc.so,
551
552    反汇编查看a87e4偏移地址显示的信息:
553
554    ```text
555    xxx/../../third_party/optimized-routines/string/aarch64/memcpy.S:175
556
557    a87e4:a94371aa         ldp x10, x11, [x1, #48]
558    ```
559
560    根据反汇编显示的源码文件位置175行,查看对应memcpy.S源文件代码:
561
562    ```text
563    L(loop64):
564
565    line 170   stp A_l, A_h, [dst, 16]
566
567    line 171   ldp A_l, A_h, [src, 16]
568
569    line 172   stp B_l, B_h, [dst, 32]
570
571    line 173   ldp B_l, B_h, [src, 32]
572
573    line 174   stp C_l, C_h, [dst, 48]
574
575    line 175   ldp C_l, C_h, [src, 48]      ---->  崩溃处指令
576
577    line 176   stp D_l, D_h, [dst, 64]
578
579    line 177   ldp D_l, D_h, [src, 64]
580
581    line 178   subs count, count, 64
582
583    line 179   b.hi L(loop64)
584    ```
585
5862. 根据寄存器值,结合上下文推测当前操作的代码对象
587
588    通常x0寄存器为函数的第一个参数,x1为第二个参数,x2为第三个,依次类推;如果为类的方法,x0为对象的地址指针,其后x1、x2、x3为依次类推,注意函数参数超过5个会压入堆栈中。
589
590    栈顶函数void* memcpy(void* restrict dest, void* restrict src, size_t n)的调用参数,x0为目的地址dest, x1为源地址,x2为拷贝字节数;
591
592    在CPPCRASH日志文件中找到对应的三个寄存器值,结合错误访问地址0x0000005b3b46c000,判断出问题的参数为x1对应的src源地址参数:
593
594    ```text
595    Register:
596
597    x0:000005b50c3e3c4 x1:000005b3b46bfcc x2:0000000000007e88 x3:000005b50c42380
598
599    ...
600    ```
601
6023. 判断代码对象的故障类型
603
604    通过CPPCRASH日志中Memory near registers查看寄存器附近内存地址值:
605
606    ```text
607    x1(/data/medialibrary/database/kvdb/3ddb6fb8b2fcb38d2f431e86bfb806dab771637860d6e86bb9430fa15df04248/single_ver/main/gen_natural_st):
608
609        0000005b21bb1fb8 8067d0f2e727f00a
610
611        0000005b21bb1fc0 1b10e1e9a1079f7a
612
613        0000005b21bb1fc8 83906d9c18cdb9c1
614
615        0000005b21bb1fd0 627dd75ab9335eb0
616
617        0000005b21bb1fd8 aabe2bb1b00f2c03
618
619        0000005b21bb1fe0 f981e4acb716cbc1
620
621        0000005b21bb1fe8 806b3d5730d281ee
622
623        0000005b21bb1ff0 3e99fedbc0a9b5e9
624
625        0000005b21bb1ff8 a91ab9d327969682
626
627        0000005b21bb2000 ffffffffffffffff       -----> 读取越界
628
629        0000005b21bb2008 ffffffffffffffff
630
631        0000005b21bb2010 ffffffffffffffff
632
633        0000005b21bb2018 ffffffffffffffff
634
635        0000005b21bb2020 ffffffffffffffff
636
637        0000005b21bb2028 ffffffffffffffff
638
639        0000005b21bb2030 ffffffffffffffff
640    ```
641
642    由上判断是一个读取越界的问题,出问题的参数为memcpy的buf和bufSize,
643
644    此时只需要分析代码中调用memcpy时传入的参数逻辑即可。
645
6464. 持续跟踪出问题对象的参数来源,结合代码与流水日志排查问题
647
648    排查方向一:排查参数对象的有效性、范围是否合法,例如buf的实际大小是否与传入的bufSize一致;
649
650    排查方向二:参数对象的生命周期是否合法,例如buf是否已被释放,是否存在多线程操作被踩内存;
651
652    排查方向三:通过参数对象访问函数的上下文,排查参数的不合理操作逻辑,例如跟踪buf和bufsize的操作逻辑,增加调试信息,锁定不合理操作逻辑。
653
654    代码片段:
655
656    ```text
657    static StatusInter xxxFunc(..., const uint8_t *buf, uint32_t bufSize)
658
659    ...
660
661    uint32_t srcSize = bufSize;
662
663    uint32_t srcOffset = cache->appendOffset - bufSize;
664
665    errno_t ret = memcpy_s(cache->buffer + srcOffset, srcSize, buf, bufSize);
666
667    if (ret != EOK) {
668
669        return MEMORY_OPERATE_FAILED_INTER;
670
671    }
672
673    ...
674    ```
675
676    通过持续追踪buf和bufSize的来源,最终来确定buf与bufSize在连续拷贝后不匹配,bufSize最终大于实际buf大小导致越界读取。
677
678### CppCrash 常见问题分类与原因
679
680- 空指针解引用 NULL pointer dereference
681    形如 SIGSEGV(SEGV_MAPERR)@0x00000000 或 cppcrash日志的Register中打印的r0,r1 等传参寄存器的值为0时,应首先考虑调用时是否传入了空指针。
682    形如 SIGSEGV(SEGV_MAPERR)@0x0000000c 或 cppcrash日志Register中打印的r1 等传参寄存器的值为一个很小的值时应考虑调用入参的结构体成员是否包含空指针。
683- 程序主动终止SIGABRT
684    一般为用户/框架/C库主动触发,大部分场景下跳过C库/abort发起的框架库的第一帧即为崩溃原因,这里主要检测的是资源使用类的问题,如线程创建,文件描述符使用,接口调用时序等。
685- SIGSEGV无效内存访问
686  - 多线程操作集合,std库的集合为非线程安全,如果多线程添加删除,容易出现SIGSEGV类崩溃,如果使用 llvm-addr2line 后的代码行与集合相关,可以考虑这个原因。
687  - 不匹配的对象生命周期,比如使用裸指针(不含有封装、自动内存管理等特性的指针)保存sptr类型以及shared_ptr类型,会导致内存泄漏和悬空指针问题。裸指针是指不含有封装、自动内存管理等特性的指针。它只是一个指向内存地址的简单指针,没有对指针指向的内存进行保护或管理。裸指针可以直接访问指向的内存,但也容易出现内存泄漏、空指针引用等问题。因此,在使用裸指针时需要特别小心,避免出现潜在的安全问题;推荐使用智能指针来管理内存。
688- use after free问题
689    返回临时变量、野指针:比如返回栈变量的引用,释放后未置空继续访问。
690
691    ```
692    # include <iostream>
693
694    int& getStackReference() {
695        int x = 5;
696        return x;  // 返回 x 的引用
697    }
698
699    int main() {
700        int& ref = getStackReference();  // 获取 x 的引用
701        // x 在 getStackReference 函数返回后被释放
702        // ref 现在是悬空引用,继续访问会导致未定义行为
703        std::cout << ref << std::endl;  // 试图输出 x 的值,这是未定义行为
704        return 0;
705    }
706    ```
707
708- 栈溢出:如递归调用,析构函数相互调用,特殊的栈(信号栈)中使用大块栈内存。
709    ```
710    # include <iostream>
711
712    class RecursiveClass {
713    public:
714        RecursiveClass() {
715            std::cout << "Constructing RecursiveClass" << std::endl;
716        }
717
718        ~RecursiveClass() {
719            std::cout << "Destructing RecursiveClass" << std::endl;
720            // 在析构函数中递归调用
721            RecursiveClass obj;
722        }
723    };
724
725    int main() {
726        RecursiveClass obj;
727        return 0;
728    }
729    ```
730    创建一个 RecursiveClass 对象时,它的构造函数被调用。销毁这个对象时,它的析构函数被调用。在析构函数中,创建了一个新的RecursiveClass对象,这会导致递归调用,直到栈溢出。递归调用导致了无限的函数调用,最终导致栈空间耗尽,程序崩溃。
731- 二进制不匹配:通常由ABI(应用程序二进制接口)不匹配引起,如自己编译二进制与实际运行的二进制接口存在差异,数据结构定义存在差异,这种一般会产生随机的崩溃栈。
732- 踩内存:使用有效的野指针,并修改了其中的内存为非法值,访问越界,覆盖了正常的数据这种一般会产生随机的崩溃栈。
733- SIGBUS (Aligment)考虑对指针进行强转之后地址是否已经处于非对齐状态。
734
735## 分析案例
736
737本章节从信号分类、问题场景分类和维测工具分类三个维度来对CppCrash典型问题进行分析和归纳。
738信号分类,侧重对常见崩溃信号覆盖介绍,各类信号提供一个典型案例。
739问题场景分类,侧重归纳目前高频问题背后的通用场景,各类场景提供一个典型案例。
740维测工具分类,侧重总结各类维测工具如果使用类分析相应的问题,各类工具提供一个典型案例。
741
742### 从信号维度分析问题
743
744#### 类型一:SIGSEGV类崩溃问题
745
746SIGSEGV信号伴随着程序发生段错误(Segmentation Fault)故障,其故障场景为`当程序试图访问不被允许访问的内存区域(比如,尝试写一块属于操作系统的内存),或以错误的类型访问内存区域(比如,尝试写一块只读内存)`。概括有如下几点:
747
748- SIGSEGV是在访问内存时发生的错误,它属于内存管理的范畴。
749- SIGSEGV是一个用户态的概念,是操作系统在用户态程序错误访问内存时所做出的处理。
750- 当用户态程序访问(访问表示读、写或执行)不允许访问的内存时,产生SIGSEGV。
751- 当用户态程序以错误的方式访问允许访问的内存时,产生SIGSEGV。
752
753SIGSEGV在很多时候是由于指针越界引起的,但并不是所有的指针越界都会引发SIGSEGV。如果不解引用越界指针,是不会引起SIGSEGV崩溃的。而且即使解引用了一个越界的指针,也不一定会引起SIGSEGV。SIGSEGV涉及到操作系统、C库、编译器、链接器各方面的内容,以如下具体的例子来说明。
754
7551. 错误的访问类型
756    样例代码如下:
757
758    ```
759    static napi_value TriggerCrash(napi_env env, napi_callback_info info)
760    {
761        char *s = "hello world";
762        s[1] = 'H';
763        return 0;
764    }
765    ```
766
767    这是最常见的一个例子。此例中,"hello world" 作为一个常量字符串,在编译后会被放在 .rodata 节(GCC),最后链接生成目标程序时 .rodata 节会被合并到 text segment 与代码段放在一起,故其所处内存区域是只读的。这就是错误的访问类型引起的 SIGSEGV(SEGV_ACCERR) 崩溃。
768
769    ![cppcrash-demo2](figures/cppcrash_image_005.png)
770
7712. 访问不属于进程地址空间的内存
772
773    样例代码如下:
774
775    ```
776    static napi_value TriggerCrash(napi_env env, napi_callback_info info)
777    {
778        uint64_t* p = (uint64_t*)0xffffffcfc42ae6f4;
779        *p = 10;
780        return 0;
781    }
782    ```
783
784    在这个例子中,我们访问了一个属于内核的地址。当然很少会有人这样写程序,但程序可能在不经意的情况下做出这样的行为,产生SIGSEGV(SEGV_MAPERR)@0xffffffcfc42ae6f4的崩溃。本例中的CppCrash故障日志(仅展示核心日志内容)如下:
785
786    ```
787    Device info:xxxxxx xxxx xx xxx
788    Build info:xxxxxxx
789    Fingerprint:73a5dcdf3e509605563aa11ac8cb4f3d7f99b9946dc142212246b53b741c4129
790    Module name:com.samples.recovery
791    Version:1.0.0
792    VersionCode:1000000
793    PreInstalled:No
794    Foreground:Yes
795    Timestamp:2024-04-29 14:07:12.082
796    Pid:21374
797    Uid:20020144
798    Process name:com.samples.recovery
799    Process life time:8s
800    Reason:Signal:SIGSEGV(SEGV_MAPERR)@0xffffffcfc42ae6f4  <-崩溃地址,即进程在
801    Fault thread info:
802    Tid:21374, Name:amples.recovery
803    # 00 pc 0000000000001ccc /data/storage/el1/bundle/libs/arm64/libentry.so(TriggerCrash(napi_env__*, napi_callback_info__*)+36)(4dd115fa8b8c1b3f37bdb5b7b67fc70f31f0dbac)
804    # 01 pc 0000000000033678 /system/lib64/platformsdk/libace_napi.z.so(ArkNativeFunctionCallBack(panda::JsiRuntimeCallInfo*)+372)(7d6f229764fdd4b72926465066bc475e)
805    # 02 pc 00000000001d7f38 /system/lib64/module/arkcompiler/stub.an(RTStub_PushCallArgsAndDispatchNative+40)
806    # 03 at doTriggerException (entry/src/main/ets/pages/FaultTriggerPage.ets:72:7)
807    # 04 at triggerNativeException (entry/src/main/ets/pages/FaultTriggerPage.ets:79:5)
808    # 05 at anonymous (entry/src/main/ets/pages/FaultTriggerPage.ets:353:19)
809    # 06 pc 000000000048e024 /system/lib64/platformsdk/libark_jsruntime.so(panda::FunctionRef::Call(panda::ecmascript::EcmaVM const*, panda::Local<panda::JSValueRef>, panda::Local<panda::JSValueRef> const*, int)+1040)(9fa942a1d42bd4ae607257975fbc1b77)
810    ...
811    # 38 pc 00000000000324b0 /system/bin/appspawn(AppSpawnRun+172)(c992404f8d1cf03c84c067fbf3e1dff9)
812    # 39 pc 00000000000213a8 /system/bin/appspawn(main+956)(c992404f8d1cf03c84c067fbf3e1dff9)
813    # 40 pc 00000000000a4b98 /system/lib/ld-musl-aarch64.so.1(libc_start_main_stage2+64)(ff4c94d996663814715bedb2032b2bbc)
814    ```
815
8163. 访问不存在的内存
817    样例代码如下:
818
819    ```
820    static napi_value TriggerCrash(napi_env env, napi_callback_info info)
821    {
822        int *a = NULL;
823        *a = 1;
824        return 0;
825    }
826    ```
827
828    在实际情况中,此例中的空指针可能指向用户态地址空间,但其所指向的页面实际不存在,便是最常见的空指针解引用的场景,这类场景CppCrash日志会识别出来,并在Reason字段打印推断信息 `Reason:Signal:SIGSEGV(SEGV_MAPERR)@000000000000000000  probably caused by NULL pointer dereference`,如下图所示:
829
830    ![cppcrash-demo3](figures/cppcrash_image_006.png)
831
8324. 重复free
833    样例代码如下:
834
835    ```
836    static napi_value TriggerCrash(napi_env env, napi_callback_info info)
837    {
838        void *pc = malloc(1024);
839        free(pc);
840        free(pc);  // 重复free
841        printf("free ok!\n");
842        return 0;
843    }
844    ```
845
846    重复释放内存的场景,系统会抛出 SIGSEGV(SI_TKILL) 类故障提示为非法的内存操作,如下图所示:
847
848    ![cppcrash-demo3](figures/cppcrash_image_007.png)
849
850    以上是 SIGSEGV 类崩溃比较常见的原因,除此之外还有栈溢出内存访问、堆溢出内存访问、访问全局区野指针、函数跳转到一个非法的地址上执行,以及非法的系统调用参数等一些场景都有可能触发 SIGSEGV 。SIGSEGV和操作系统栈分配回收、编译器有着密切的联系。
851
852#### 类型二:SIGABRT类崩溃问题
853
854SIGABRT信号被发送到进程,告诉进程中止。既可以进程自己调用C标准库的abort()函数,信号通常由进程本身发起,也可以跟其他信号一样从外部发送给进程。
855
8561. 执行abort函数
857    样例代码如下:
858
859    ```
860    static napi_value TriggerCrash(napi_env env, napi_callback_info info)
861    {
862        OH_LOG_FATAL(LOG_APP, "test fatal log.");
863        abort();
864        return 0;
865    }
866    ```
867
868    该场景是主动调用 abort() 函数构造,对应的场景是各基础库可能会存在一些安全校验,对于识别为会导致进程无法安全运行性的场景,会主动 abort。对应如下场景如下图所示,会将进程退出前的最后一条fatal级别日志打印到崩溃日志中。
869
870    ![cppcrash-demo4](figures/cppcrash_image_008.png)
871
8722. 执行assert函数
873    样例代码如下:
874
875    ```
876    static napi_value TriggerCrash(napi_env env, napi_callback_info info)
877    {
878    # if 0  //该值为0,则报错;为1,则正常
879        void *pc = malloc(1024);
880    # else
881        void *pc = nullptr;
882    # endif
883        assert(pc != nullptr);
884        return 0;
885    }
886    ```
887
888    除了调用 abort() 函数外,C++中的另一个异常处理机制还包括 assert() 函数,其他的还有 exit() 函数,异常捕获机制(try-catch)、exception类等。assert用于校验当前函数执行流程中的一些数据,校验失败进程会主动 abort。对应的故障场景如下图所示:
889
890    ![cppcrash-demo5](figures/cppcrash_image_009.png)
891
892### 从场景维度分析问题
893
894#### 类型一:内存访问类崩溃问题
895
896**问题背景**
897每次崩溃地址0x7f82764b70都在libace_napi_ark.z.so的可读可执行段上。崩溃原因是需要对地址进行写操作,而对应的maps段只有可读、可执行权限没有写权限,当进程试图访问不被允许访问的内存区域时,进程发生内存访问类崩溃。
898
899```
9007f82740000-7f8275c000 r--p 00000000 /system/lib64/libace_napi_ark.z.so
9017f8275c000-7f8276e000 r-xp 0001b000 /system/lib64/libace_napi_ark.z.so <-崩溃地址落在该地址区间
9027f8276e000-7f82773000 r--p 0002c000 /system/lib64/libace_napi_ark.z.so
9037f82773000-7f82774000 rw-p 00030000 /system/lib64/libace_napi_ark.z.so
904```
905
906崩溃调用栈如下图:
907
908![cppcrash-demo6](figures/cppcrash_image_010.png)
909
910**定位思路**
911每次地址出错都很有规律,但node地址不应该落在libace_napi_ark.z.so,从此类问题的现象来看,很有可能是踩内存问题。踩内存问题可使用[ASAN工具](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-asan-V5)排查问题。于是后续使用ASAN版本进行压测复现,也找到了稳定必现的场景。ASAN版本检测出来的问题也和上面崩溃栈反映的问题一致。堆栈报的是heap-use-after-free,实际上是对同一个address进行重复释放,只是在重复释放那次操作时,使用该地址去访问了其对象成员,进而报出了UAF问题。
912ASAN核心日志如下:
913
914```
915=================================================================
916==appspawn==2029==ERROR: AddressSanitizer: heap-use-after-free on address 0x003a375eb724 at pc 0x002029ba8514 bp 0x007fd8175710 sp 0x007fd8175708
917READ of size 1 at 0x003a375eb724 thread T0 (thread name)
918    # 0 0x2029ba8510  (/system/asan/lib64/platformsdk/libark_jsruntime.so+0xca8510) panda::ecmascript::Node::IsUsing() const at arkcompiler/ets_runtime/ecmascript/ecma_global_storage.h:82:16
919(inlined by) panda::JSNApi::DisposeGlobalHandleAddr(panda::ecmascript::EcmaVM const*, unsigned long) at arkcompiler/ets_runtime/ecmascript/napi/jsnapi.cpp:749:67 BuildID[md5/uuid]=9a18e2ec0dc8a83216800b2f0dd7b76a
920    # 1 0x403ee94d30  (/system/asan/lib64/libace.z.so+0x6194d30) panda::CopyableGlobal<panda::ObjectRef>::Free() at arkcompiler/ets_runtime/ecmascript/napi/include/jsnapi.h:1520:9
921(inlined by) panda::CopyableGlobal<panda::ObjectRef>::Reset() at arkcompiler/ets_runtime/ecmascript/napi/include/jsnapi.h:189:9
922(inlined by) OHOS::Ace::Framework::JsiType<panda::ObjectRef>::Reset() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/jsi/jsi_types.inl:112:13
923(inlined by) OHOS::Ace::Framework::JsiWeak<OHOS::Ace::Framework::JsiObject>::~JsiWeak() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/jsi/jsi_ref.h:167:16
924(inlined by) OHOS::Ace::Framework::ViewFunctions::~ViewFunctions() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/jsview/js_view_functions.h:44:5 BuildID[md5/uuid]=1330f8b9be73bdb76ae18107c2a60ca1
925    # 2 0x403ee9296c  (/system/asan/lib64/libace.z.so+0x619296c) OHOS::Ace::Framework::ViewFunctions::~ViewFunctions() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/jsview/js_view_functions.h:42:5
926(inlined by) OHOS::Ace::Framework::ViewFunctions::~ViewFunctions() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/jsview/js_view_functions.h:42:5 BuildID[md5/uuid]=1330f8b9be73bdb76ae18107c2a60ca1
927    # 3 0x403ed9b130  (/system/asan/lib64/libace.z.so+0x609b130) OHOS::Ace::Referenced::DecRefCount() at foundation/arkui/ace_engine/frameworks/base/memory/referenced.h:76:13
928(inlined by) OHOS::Ace::RefPtr<OHOS::Ace::Framework::ViewFunctions>::~RefPtr() at foundation/arkui/ace_engine/frameworks/base/memory/referenced.h:148:22 BuildID[md5/uuid]=1330f8b9be73bdb76ae18107c2a60ca1
929    # 4 0x403ed9b838  (/system/asan/lib64/libace.z.so+0x609b838) OHOS::Ace::RefPtr<OHOS::Ace::Framework::ViewFunctions>::Reset() at foundation/arkui/ace_engine/frameworks/base/memory/referenced.h:163:9
930(inlined by) OHOS::Ace::Framework::JSViewFullUpdate::~JSViewFullUpdate() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/jsview/js_view.cpp:159:21 BuildID[md5/uuid]=1330f8b9be73bdb76ae18107c2a60ca1
931    # 5 0x403ed9bf24  (/system/asan/lib64/libace.z.so+0x609bf24) OHOS::Ace::Framework::JSViewFullUpdate::~JSViewFullUpdate() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/jsview/js_view.cpp:157:1
932(inlined by) OHOS::Ace::Framework::JSViewFullUpdate::~JSViewFullUpdate() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/jsview/js_view.cpp:157:1 BuildID[md5/uuid]=1330f8b9be73bdb76ae18107c2a60ca1
933...
934freed by thread T0 (thread name) here:
935    # 0 0x2024ed3abc  (/system/asan/lib64/libclang_rt.asan.so+0xd3abc)
936    # 1 0x2029ba8424  (/system/asan/lib64/platformsdk/libark_jsruntime.so+0xca8424) std::__h::__function::__value_func<void (unsigned long)>::operator()[abi:v15004](unsigned long&&) const at prebuilts/clang/ohos/linux-x86_64/llvm/bin/../include/libcxx-ohos/include/c++/v1/__functional/function.h:512:16
937(inlined by) std::__h::function<void (unsigned long)>::operator()(unsigned long) const at prebuilts/clang/ohos/linux-x86_64/llvm/bin/../include/libcxx-ohos/include/c++/v1/__functional/function.h:1197:12
938(inlined by) panda::ecmascript::JSThread::DisposeGlobalHandle(unsigned long) at arkcompiler/ets_runtime/ecmascript/js_thread.h:604:9
939(inlined by) panda::JSNApi::DisposeGlobalHandleAddr(panda::ecmascript::EcmaVM const*, unsigned long) at arkcompiler/ets_runtime/ecmascript/napi/jsnapi.cpp:752:24 BuildID[md5/uuid]=9a18e2ec0dc8a83216800b2f0dd7b76a
940    # 2 0x403ee94b68  (/system/asan/lib64/libace.z.so+0x6194b68) panda::CopyableGlobal<panda::FunctionRef>::Free() at arkcompiler/ets_runtime/ecmascript/napi/include/jsnapi.h:1520:9
941(inlined by) panda::CopyableGlobal<panda::FunctionRef>::Reset() at arkcompiler/ets_runtime/ecmascript/napi/include/jsnapi.h:189:9
942(inlined by) OHOS::Ace::Framework::JsiType<panda::FunctionRef>::Reset() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/jsi/jsi_types.inl:112:13
943(inlined by) OHOS::Ace::Framework::JsiWeak<OHOS::Ace::Framework::JsiFunction>::~JsiWeak() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/engine/jsi/jsi_ref.h:167:16
944(inlined by) OHOS::Ace::Framework::ViewFunctions::~ViewFunctions() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/jsview/js_view_functions.h:44:5 BuildID[md5/uuid]=1330f8b9be73bdb76ae18107c2a60ca1
945    # 3 0x403ee9296c  (/system/asan/lib64/libace.z.so+0x619296c) OHOS::Ace::Framework::ViewFunctions::~ViewFunctions() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/jsview/js_view_functions.h:42:5
946(inlined by) OHOS::Ace::Framework::ViewFunctions::~ViewFunctions() at foundation/arkui/ace_engine/frameworks/bridge/declarative_frontend/jsview/js_view_functions.h:42:5 BuildID[md5/uuid]=1330f8b9be73bdb76ae18107c2a60ca1
947    # 4 0x403ed9b130  (/system/asan/lib64/libace.z.so+0x609b130) OHOS::Ace::Referenced::DecRefCount() at foundation/arkui/ace_engine/frameworks/base/memory/referenced.h:76:13
948(inlined by) OHOS::Ace::RefPtr<OHOS::Ace::Framework::ViewFunctions>::~RefPtr() at foundation/arkui/ace_engine/frameworks/base/memory/referenced.h:148:22 BuildID[md5/uuid]=1330f8b9be73bdb76ae18107c2a60ca1
949...
950previously allocated by thread T0 (thread name) here:
951    # 0 0x2024ed3be4  (/system/asan/lib64/libclang_rt.asan.so+0xd3be4)
952    # 1 0x2029ade778  (/system/asan/lib64/platformsdk/libark_jsruntime.so+0xbde778) panda::ecmascript::NativeAreaAllocator::AllocateBuffer(unsigned long) at arkcompiler/ets_runtime/ecmascript/mem/native_area_allocator.cpp:98:17 BuildID[md5/uuid]=9a18e2ec0dc8a83216800b2f0dd7b76a
953    # 2 0x2029a39064  (/system/asan/lib64/platformsdk/libark_jsruntime.so+0xb39064) std::__h::enable_if<!std::is_array_v<panda::ecmascript::NodeList<panda::ecmascript::WeakNode>>, panda::ecmascript::NodeList<panda::ecmascript::WeakNode>*>::type panda::ecmascript::NativeAreaAllocator::New<panda::ecmascript::NodeList<panda::ecmascript::WeakNode>>() at arkcompiler/ets_runtime/ecmascript/mem/native_area_allocator.h:61:19
954(inlined by) unsigned long panda::ecmascript::EcmaGlobalStorage<panda::ecmascript::Node>::NewGlobalHandleImplement<panda::ecmascript::WeakNode>(panda::ecmascript::NodeList<panda::ecmascript::WeakNode>**, panda::ecmascript::NodeList<panda::ecmascript::WeakNode>**, unsigned long) at arkcompiler/ets_runtime/ecmascript/ecma_global_storage.h:565:34
955(inlined by) panda::ecmascript::EcmaGlobalStorage<panda::ecmascript::Node>::SetWeak(unsigned long, void*, void (*)(void*), void (*)(void*)) at arkcompiler/ets_runtime/ecmascript/ecma_global_storage.h:455:26 BuildID[md5/uuid]=9a18e2ec0dc8a83216800b2f0dd7b76a
956    # 3 0x2029ba5620  (/system/asan/lib64/platformsdk/libark_jsruntime.so+0xca5620) std::__h::__function::__value_func<unsigned long (unsigned long, void*, void (*)(void*), void (*)(void*))>::operator()[abi:v15004](unsigned long&&, void*&&, void (*&&)(void*), void (*&&)(void*)) const at prebuilts/clang/ohos/linux-x86_64/llvm/bin/../include/libcxx-ohos/include/c++/v1/__functional/function.h:512:16
957(inlined by) std::__h::function<unsigned long (unsigned long, void*, void (*)(void*), void (*)(void*))>::operator()(unsigned long, void*, void (*)(void*), void (*)(void*)) const at prebuilts/clang/ohos/linux-x86_64/llvm/bin/../include/libcxx-ohos/include/c++/v1/__functional/function.h:1197:12
958(inlined by) panda::ecmascript::JSThread::SetWeak(unsigned long, void*, void (*)(void*), void (*)(void*)) at arkcompiler/ets_runtime/ecmascript/js_thread.h:610:16
959(inlined by) panda::JSNApi::SetWeak(panda::ecmascript::EcmaVM const*, unsigned long) at arkcompiler/ets_runtime/ecmascript/napi/jsnapi.cpp:711:31 BuildID[md5/uuid]=9a18e2ec0dc8a83216800b2f0dd7b76a
960...
961```
962
963根据堆栈继续分析,
964JsiWeak析构或重置的时候会触发其成员(类型为JsiObject/JsiValue/JsiFunction)父类JsiType中CopyableGlobal被释放,如下图:
965
966![cppcrash-demo5](figures/cppcrash_image_011.png)
967
968运行时在GC过程中IterateWeakEcmaGlobalStorage,会对无callback的WeakNode调用DisposeGlobalHandle操作,也对其进行释放,如下图:
969
970![cppcrash-demo6](figures/cppcrash_image_012.png)
971
972因此,对于同一个WeakNode,可能会存在两个入口释放。如果是GC过程中IterateWeakEcmaGlobalStorage先释放,因为无callback回调通知到JsiWeak进行清理,JsiWeak那边仍保存一个对已释放的WeakNode引用,即CopyableGlobal;当前面讲的WeakNode所在的NodeList被整体释放,归还给操作系统后,JsiWeak处保留的CopyableGlobal再释放,就会存在double-free问题。
973
974![cppcrash-demo7](figures/cppcrash_image_013.png)
975
976**修改方法**
977JsiWeak调用SetWeakCallback,传入callback,在GC过程中IterateWeakEcmaGlobalStorage释放WeakNode时,通知JsiWeak对其保存的CopyableGlobal进行重置,确保同一个地址不被double-free。
978
979**建议与总结**
980使用内存时应考虑是否存在重复释放或者未释放的可能,另外定位内存访问类崩溃问题(一般是SIGSEGV类型问题)时,如果根据崩溃栈分析问题无头绪时,应优先考虑跑ASAN版本复现问题。
981
982#### 类型二:多线程类问题
983
984**问题背景**
985napi_env释放后仍被使用。
986
987**问题场景**
988napi接口的env传入非法,崩溃栈直接挂在NativeEngineInterface::ClearLastError()中,根据日志打印env地址定位,发现是env被释放后仍然被使用。
989
990![cppcrash-demo9](figures/cppcrash_image_015.png)
991
992核心崩溃栈如下:
993
994![cppcrash-demo8](figures/cppcrash_image_014.png)
995
996**修改方法**
997一个线程的创建的env,不要传给另一个线程使用。
998
999**建议与总结**
1000对于多线程类问题可以打开方舟多线程检测功能,能够更加方便定位问题,见工具类方舟多线程检测章节。
1001
1002注:napi接口中的env,是引擎创建时候的arkNativeEngine。
1003
1004#### 类型三:生命周期类问题
1005
1006**问题背景**
1007开发者在写native代码创建napi_value时,需要配合napi_handle_scope一起使用。napi_handle_scope的作用是管理napi_value的生命周期,napi_value只能在napi_handle_scope的作用域范围内进行使用,离开napi_handle_scope作用域范围后,napi_value及它所持有的js对象的生命周期不再得到保护,一旦引用计数为0,就会被GC回收掉,此时再去使用napi_value就会访问已释放的内存,产生问题。
1008
1009**问题场景**
1010napi_value其实是个裸指针(结构体指针),其作用是持有js对象,用于保持js对象的生命周期,保证js对象不被GC当成垃圾对象回收。napi_handle_scope用来管理napi_value,离开napi_handle_scope作用域之后,napi_value由GC回收,napi_value不再持有js对象(不再保护js对象生命周期)。
1011
1012**定位思路**
1013根据崩溃栈反编译找到出现问题的napi接口的上层接口,在上层接口内找到出问题的napi_value,检查napi_value的使用范围是否超出了napi_handle_scope的作用域范围。
1014
1015**案例**
1016napi_value超出NAPI框架的scope,如下:
1017
1018![cppcrash-demo9](figures/cppcrash_image_016.png)
1019
1020js侧通过Add接口添加数据,native侧以napi_value保存到vector,js侧通过get接口获取添加的数据,native侧将保存的napi_value以数组形式返回回去,然后js侧读取数据的属性。出现报错:Can not get Prototype on non ECMA Object。跨napi的native_value未使用napi_ref保存,导致native_value失效。
1021注:NAPI框架的scope即napi_handle_scope,napi开发者可以通过napi_handle_scope来管理napi_value的生命周期。框架层的scope嵌入在js call native的端到端流程中,即进入开发者自己写的native方法前open scope,native方法结束后close scope。
1022
1023#### 类型四:指针类问题
1024
1025**问题背景**
1026智能指针使用之前未判空,造成进程运行时发生空指针解引用崩溃问题。
1027
1028**问题影响**
1029进程发生崩溃,影响进程的稳定运行,非预期退出。
1030
1031**定位思路**
1032![cppcrash-demo10](figures/cppcrash_image_017.png)
1033
1034空指针类型崩溃可以从故障原因得到提示信息。通过llvm-addr2line解行号发现业务代码中在使用智能指针之前未对智能指针判空,对空地址进行访问导致崩溃产生。
1035
1036**修复方法**
1037对所有使用该指针的地方进行保护性判空。
1038
1039**建议与总结**
1040指针在使用之前应该要进行判空处理,防止访问空指针造成进程崩溃退出。
1041
1042### 配合工具分析问题
1043
1044#### 工具一:ASAN
1045
1046[ASAN使用指南](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-asan-V5)
1047
1048#### 工具二:方舟多线程检测
1049
1050**基本原理**
1051js是单线程的,操作js对象只允许发生在创建该js线程上,否则将会有多线程安全问题(主线程创建的js对象只能在主线程上操作,worker创建的js对象只能在worker线程上操作)。napi接口会直接涉及到对象的操作,因此绝大部分(95%)的napi接口只允许在js线程上使用。多线程检测机制检测的是:当前线程和使用的vm/env中的js thread id是否一致,若不一致,则表明vm/env被跨线程使用,存在多线程安全问题。常见问题有:1. 非js线程使用napi接口,2. napi接口使用其他线程的env。
1052
1053**使用方法**
1054![cppcrash-demo13](figures/cppcrash_image_020.png)
1055
1056DevEco勾选Multi Thread Check选项即可开启方舟多线程检测功能。
1057
1058**使用场景**
1059如果crash日志的堆栈难以分析,出现概率也相对比较高,对于此类问题,应该考虑开启多线程检测。 开启多线程检测之后,如果cpp_crash日志中fatal信息为Fatal: ecma_vm cannot run in multi-thread! thread:3096 currentThread:3550,则发生了多线程安全问题,意思是当前线程号为3550,而使用的js thread却是3096线程创建出来的,跨线程使用vm。
1060
1061**案例**
1062打开后重新触发崩溃,如果是多线程问题,会显示fatal 信息,参考如下:
1063
1064```
1065Fatal: ecma_vm cannot run in multi-thread! thread:xxx currentThread:yyy
1066```
1067
1068该信息意思是当前线程号为17585,而使用的 js thread 却是17688 线程创建出来的,跨线程使用 vm。vm 就是 js thread 的 napi_env__* ,运行线程代码的环境,一个线程使用一个 vm。
1069崩溃日志核心部分如下所示:
1070
1071```
1072
1073Reason:Signal:SIGABRT(SI_TKILL)@0x01317b9f000044b1 from:17585: 20020127
1074LastFatalMessage: [default] CheckThread:177 Fatal: ecma_vm cannot run in multi-thread! thread:17688 currentThread:17585
1075Fault thread Info:
1076Tid:17585, Name:xxxxx
1077# 00 pc 00000000000f157c /system/lib/ld-musl-aarch64-asan.so.1(__restore_sigs+52)(38eb4ca904ae601d4b4dca502e948960)
1078# 01 pc 00000000000f1800 /system/lib/ld-musl-aarch64-asan.so.1(raise+112) (38eb4ca904aeó01d4b4dca502e948960)
1079# 02 pc 00000000000adc74 /system/lib/ld-musl-aarch64-asan.so.1(abort.+20) (38eb4ca904ae601d4b4dca502e948960)
1080# 03 pc 0000000000844fdc /system/asan/libó4/platformsdk/libark_jsruntime.so(panda::ecmascript::EcmaVM::CheckThread() const+2712)(1df055932338c14060b864435aec88ab)
1081# 04 pc 0000000000f3d930 /system/asan/libó4/platformsdk/libark_jsruntime.so(panda::0bjectRef:: New(panda::ecmascript::EcmaVM const*)+908)(1df055932338c14060b864435aec88
1082# 05 pC 0000000000095048 /sYstem/asan/lib64/platformsdk/libace_napi.z.so(napi_create_object+80)(efc1b3d1378f56b4b800489fb30dcded)
1083# 06 pc 00000000005d9770 /data/ storage/el1/bundle/libs/arm64/xxxxx.so (c0f1735eada49fadc5197745f5afOc0a52246270)
1084```
1085
1086多线程问题分析步骤:
1087i. 检查 libace_napi.z.so 下面的第一个栈帧,上图为`xxxxx.so`,判断是否把 17688 线程的 napi_env 传给了 17585 线程;
1088ii. 如果 libace_napi.z.so 下面的栈帧没有明显的 napi_env 参数传递,需要检查是否以结构体成员变量的方式传递;
1089
1090#### 工具三:objdump
1091
1092**使用方法**
1093objdump二进制是系统侧工具,开发者需要具备OpenHarmony编译环境,项目代码在gitee上可获取,命令如下:
1094
1095```
1096repo init -u git@gitee.com:openharmony/manifest.git -b master --no-repo-verify --no-clone-bundle --depth=1
1097repo sync -c
1098./build/prebuilts_download.sh
1099```
1100
1101工具在工程目录下`prebuilts/clang/ohos/linux-x86_64/llvm/bin/llvm-objdump`,命令如下:
1102
1103```
1104prebuilts/clang/ohos/linux-x86_64/llvm/bin/llvm-objdump -d libark_jsruntime.so > dump.txt
1105```
1106
1107**使用场景**
1108有些情况下,通过addr2line只能看出代码某一行有问题,无法确认具体是哪个变量异常,此时可以通过objdump反汇编并结合cppcrash寄存器内容,进一步确认具体崩溃原因。
1109
1110**案例**
1111日志内容如下:
1112
1113```
1114Tid:6655, Name:GC_WorkerThread
1115# 00 pc 00000000004492d4 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::NonMovableMarker::MarkObject(unsigned int, panda::ecmascript::TaggedObject*)+124)(21cf5411626d5986a4ba6383e959b3cc)
1116# 01 pc 000000000044b580 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::NonMovableMarker::MarkValue(unsigned int, panda::ecmascript::ObjectSlot&, panda::ecmascript::Region*, bool)+72)(21cf5411626d5986a4ba6383e959b3cc)
1117# 02 pc 000000000044b4e8 /system/lib64/platformsdk/libark_jsruntime.so(std::__h::__function::__func<panda::ecmascript::NonMovableMarker::ProcessMarkStack(unsigned int)::$_2, std::__h::allocator<panda::ecmascript::NonMovableMarker::ProcessMarkStack(unsigned int)::$_2>, void (panda::ecmascript::TaggedObject*, panda::ecmascript::ObjectSlot, panda::ecmascript::ObjectSlot, panda::ecmascript::VisitObjectArea)>::operator()(panda::ecmascript::TaggedObject*&&, panda::ecmascript::ObjectSlot&&, panda::ecmascript::ObjectSlot&&, panda::ecmascript::VisitObjectArea&&)+256)(21cf5411626d5986a4ba6383e959b3cc)
1118# 03 pc 0000000000442ac0 /system/lib64/platformsdk/libark_jsruntime.so(void panda::ecmascript::ObjectXRay::VisitObjectBody<(panda::ecmascript::VisitType)1>(panda::ecmascript::TaggedObject*, panda::ecmascript::JSHClass*, std::__h::function<void (panda::ecmascript::TaggedObject*, panda::ecmascript::ObjectSlot, panda::ecmascript::ObjectSlot, panda::ecmascript::VisitObjectArea)> const&)+216)(21cf5411626d5986a4ba6383e959b3cc)
1119# 04 pc 0000000000447ccc /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::NonMovableMarker::ProcessMarkStack(unsigned int)+248)(21cf5411626d5986a4ba6383e959b3cc)
1120# 05 pc 0000000000438588 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::Heap::ParallelGCTask::Run(unsigned int)+148)(21cf5411626d5986a4ba6383e959b3cc)
1121# 06 pc 00000000004e31c8 /system/lib64/platformsdk/libark_jsruntime.so(panda::ecmascript::Runner::Run(unsigned int)+144)(21cf5411626d5986a4ba6383e959b3cc)
1122# 07 pc 00000000004e3780 /system/lib64/platformsdk/libark_jsruntime.so(void* std::__h::__thread_proxy[abi:v15004]<std::__h::tuple<std::__h::unique_ptr<std::__h::__thread_struct, std::__h::default_delete<std::__h::__thread_struct>>, void (panda::ecmascript::Runner::*)(unsigned int), panda::ecmascript::Runner*, unsigned int>>(void*)+64)(21cf5411626d5986a4ba6383e959b3cc)
1123# 08 pc 000000000014d894 /system/lib/ld-musl-aarch64.so.1
1124# 09 pc 0000000000085d04 /system/lib/ld-musl-aarch64.so.1
1125```
1126
1127首先先用addr2line查看出错的行,如下:
1128
1129![cppcrash-demo14](figures/cppcrash_image_021.png)
1130
1131能看出的信息是判断IsYongSpace的时候访问到了空指针挂了,能够大概猜测出来是Region是空指针。
1132继续使用objdump反汇编,搜索出错地址4492d4 , 对应的汇编指令如下。
1133
1134![cppcrash-demo15](figures/cppcrash_image_022.png)
1135
1136查看x20寄存器,发现为0x000000000000000,x20从上面可以看出是基于x2做位运算(清除掉后18位,典型的Region::ObjectAddressToRange操作)。这样分析之后,就清楚了,x2为MarkObject函数的第二个参数object,x20为变量objectRegion,如下:
1137
1138```
1139Registers: x0:0000007f0fe31560 x1:0000000000000003 x2:0000000000000000 x3:0000005593100000
1140        x4:0000000000000000 x5:0000000000000000 x6:0000000000000000 x7:0000005596374fa0
1141        x8:0000000000000000 x9:0000000000000000 x10:0000000000000000 x11:0000007f9cb42bb8
1142        x12:000000000000005e x13:000000000061f59e x14:00000005d73d60fb x15:0000000000000000
1143        x16:0000007f9cc5f200 x17:0000007f9f201f68 x18:0000000000000000 x19:0000000000000000
1144        x20:0000000000000000 x21:0000000000000000 x22:0000000000000000 x23:000000559313f860
1145        x24:000000559313f868 x25:0000000000000003 x26:00000055a0e19960 x27:0000007f9cc57b38
1146        x28:0000007f9f21a1c0 x29:00000055a0e19700 lr:0000007f9cb4b584 sp:00000055a0e19700 pc:0000007f9cb492d4
1147```
1148
1149上面ldrb w8, [x20]对应 packedData_.flags_.spaceFlag_ 是因为,packedData_是region的第一个域,flags_是packedData_的第一个域,spaceFlag_是flags_的第一个域,所以直接取objectRegion地址对应的第一个字节。
1150查看汇编代码需要熟悉常见的汇编指令,以及传参规则,例如对于c++非inline的成员函数r0一般保存的是this指针。另外,由于编译器优化,源码和汇编代码对应关系可能不是很直观,我们可以根据代码中的一些特征值(常量),较快地找到对应关系。