# 应用闪退-Native运行时崩溃（CppCrash）信号与定位指导

#### 问题现象

应用在使用过程中出现闪退，系统日志或FaultLog中生成以cppcrash命名的故障日志文件。

#### 背景知识

*   CppCrash进程崩溃检测基于操作系统信号机制，目前支持的崩溃信号参考Cpp Crash（进程崩溃）检测。
*   CppCrash日志规格说明可以参考日志规格。

| 崩溃信号 | 崩溃错误码 | 故障根因 | 关键字段 | 解决方案 |
| --- | --- | --- | --- | --- |
| SIGABRT | SI_TKILL | 断言检查失败 | LastFatalMessage:Assertion failed | 检查代码运行时的断言条件，修复业务逻辑异常。 |
| SIGABRT | SI_TKILL | 未处理抛出的异常 | terminating due to uncaught exception of type | 在抛出异常的位置使用try-catch捕获并处理异常。 |
| SIGABRT | SI_TKILL | 调用napi_fatal_error引发致命错误 | LastFatalMessage:[(xxx)(napi_fatal_error)] FATAL ERROR | 排查napi_fatal_error的调用逻辑是否符合预期。 |
| SIGSEGV | SEGV_MAPERR | 空指针解引用 | probably caused by NULL pointer dereference | 排查代码中的指针变量，是否存在使用未初始化、赋值为空或已释放的指针。 |
| SIGSEGV | SEGV_ACCERR | 访问越栈内存上界 | probably caused by stack-buffer-overflow | 排查递归调用时未设置递归终止条件导致栈内存耗尽；局部变量占用过多栈内存导致栈内存耗尽；在信号栈中使用超过系统限制的栈内存。 |

#### 问题定位

进程在Native层发生崩溃后，系统会抓取崩溃相关的信息，在faultlogger目录下生成cppcrash-应用包名-应用UID-发生时间.log崩溃日志并上报崩溃事件，以供开发者分析定位。CppCrash日志规格说明可以参考日志规格。

**步骤一：获取cppcrash-应用包名-应用UID-发生时间.log故障日志。**

*   开发态：DevEco Studio会自动收集到日志工具栏的FaultLog下，该工具有高亮关键信息、跳转到对应的代码行、以及结构化日志等方便开发者分析的功能，详情可见使用文档FaultLog。
*   运维态：
    *   直接导出故障日志目录：必须先打开开发者模式，再执行命令hdc file recv /data/log/faultlog/faultlogger/ \[本地目录\]。
    *   使用HiAppEvent故障订阅接口：可记录应用运行过程中的故障，为应用开发者提供的事件打点机制，详见HiAppEvent介绍和崩溃事件介绍。

**步骤二：分析故障原因Reason和崩溃堆栈Stack。**

**Reason字段规格如下：**

一般规格包含崩溃类型Signal、崩溃地址和信号发送方信息，Signal信号的含义可见基本概念和实现原理。

```
Reason:Signal:SIGABRT(SI_TKILL)@0x01317bc900000a02 from:2562:20020169
字段解释如下：
Reason:Signal:信号值(tkill()函数信号)@崩溃地址 from:发送信号的Pid:发送信号的Uid
```

**堆栈日志规格如下：**

崩溃堆栈搜索关键字Fault thread info。

堆栈日志打印上限为256个栈帧，超出部分不打印。

```
Fault thread info:
#00 pc 00000000001b0958 /system/lib/ld-musl-aarch64.so.1(raise+216)(52299a28d60f0bb4073bd788bc023a3a)
#<序号> pc <pc在段内的偏移> <pc属于的段名称(函数名+函数内偏移的字节数)(BuildID)>

pc：程序计数器（Program Counter）的缩写，作用是储存当前程序正在执行指令的地址。
BuildID：是用于标识二进制文件的唯一标识符。
```

**问题场景如下：**

1.  场景一：断言检查失败。
    *   关键字：LastFatalMessage:Assertion failed。
    *   定位：调试代码，检查断言条件中变量的值是否符合预期。
2.  场景二：未处理抛出的异常。
    *   关键字：terminating due to uncaught exception of type。
    *   定位：定位抛出异常的代码行，检查抛出异常的业务逻辑是否符合预期。
3.  场景三：调用napi\_fatal\_error引发致命错误。
    *   关键字：LastFatalMessage:\[(xxx)(napi\_fatal\_error)\] FATAL ERROR
    *   定位：排查napi\_fatal\_error的调用逻辑是否符合预期。
4.  场景四：空指针解引用。
    *   关键字：probably caused by NULL pointer dereference
    *   定位：排查代码中的指针变量，是否存在使用未初始化、赋值为空或已释放的指针。
5.  场景五：访问越栈内存上界。
    *   关键字：current thread stack low address = 0x0000005b479ef000, probably caused by stack-buffer-overflow
    *   定位：排查代码逻辑是否存在以下三种栈溢出场景。
        *   递归调用时未设置递归终止条件导致栈内存耗尽。
        *   局部变量占用过多栈内存导致栈内存耗尽。
        *   在信号栈中使用超过系统限制的栈内存。

**步骤三：结合业务代码分析根因。**

*   堆栈定位代码行：
    *   开发态环境下，DevEco Studio生成的CppCrash调用栈支持直接跳转到代码行，详情可见使用文档跳转至引起错误的代码行。
    *   运维态的日志，可以使用hstack工具将Release应用混淆的调用栈还原为源码对应调用栈。详情可见堆栈解析工具（hstack）。
    *   可能存在部分未能解析跳转到对应行号的栈帧，可以使用llvm-addr2line工具将函数地址解析成文件名或行号，详情参考C++堆栈解析流程。
*   代码调试：
    *   开发态：DevEco Studio提供了丰富的调试能力，支持跨语言和三方库调试，详情可见Native代码调试。
    *   运维态：建议使用HiAppEvent事件订阅和hilog日志打印，再次发生CppCrash时，打印关键信息。

**步骤四：异常崩溃场景排查地址越界(可选)。**

如果多次崩溃产生了随机的崩溃调用栈，或堆栈指向的代码不应该发生此崩溃信号，则可能发生了地址越界，此时的崩溃栈已非第一现场，需要使用HWASan等内存检测工具，调试压测抓取第一现场的堆栈，详情参考地址越界类问题分析方法。

#### 分析结论

*   断言检查失败：断言条件为false时，进程会直接终止。
*   未处理抛出的异常：未处理抛出的异常导致应用闪退。
*   调用napi\_fatal\_error引发致命错误：进程主动调用napi\_fatal\_error引发致命错误导致应用闪退。
*   空指针解引用：空指针解引用，访问到空内存地址导致崩溃。
*   访问越栈内存上界：进程栈内存溢出导致应用闪退。

#### 修改建议

*   业务逻辑异常：使用堆栈解析工具（hstack）解析崩溃栈后结合具体代码进行分析。
*   内存异常：使用HWASan检测内存错误，获取运行时的内存栈信息，分析并修复引起内存错误的代码。
*   多线程异常：启用方舟运行时检测，获取多线程安全问题产生的日志信息，跳转至对应代码分析并修复。