📝往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)
🚩 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
🚩 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
🚩 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
🚩 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
🚩 记录一场鸿蒙开发岗位面试经历~
📃 持续更新中……
简介
CppCrash是C/C++运行时崩溃,包括空指针异常、数组越界异常、栈溢出异常等。HarmonyOS系统针对这一类故障,基于系统级DFX能力,能够进行检测并生成故障日志,生成在/data/log/faultlog/faultlogger系统目录下,在DevEcoStudio中的Faultlog工具栏也能进行汇总显示。
CppCrash故障日志
日志格式和日志获取
CppCrash日志格式可参考 《日志格式》 。CppCrash故障根据报错场景可以分为运行态CppCrash故障和开发态CppCrash故障。
在开发态下,DevEco Studio会收集CppCrash、App Freeze、JS Crash、System Freeze、ASan的崩溃日志到FaultLog下,开发者可以通过FaultLog的CppCrash日志、ASAN日志定位问题的具体原因。此外,开发者可以自行获取/data/log/faultlog/faultlogger下的日志再进行分析。
在运行态下,开发者需要提前开通崩溃服务,收集运行状态下的CppCrash,具体步骤如下所示:
- 开发者需要在 AGC 提前创建项目和应用,详细创建步骤可以参考创建项目与应用。
- 在AGC上开通崩溃服务,详细的操作步骤可以参考开通HarmonyOS应用的服务。
- 添加配置文件,将配置文件添加到工程目录并集成AGC插件,AGC插件可以自动将您在AGC上的应用信息加载到开发环境,详情可参考添加配置文件。
- 添加配置文件后,需要在DevEco Studio项目中配置SDK依赖,详情可参考集成SDK。
- 配置完成后,需要测试崩溃服务是否正常运行,详情可以参考测试崩溃实现。
- 具体的崩溃日志,可以在“AGC->我的项目->崩溃”中找到详细日志,进而分析异常报错问题。
crash信号分类
进程崩溃基于Linux信号机制,目前主要支持对以下崩溃异常信号的处理:
表1 当前系统支持的崩溃异常信号表
信号值 | 信号 | 解释 | 触发原因 |
---|---|---|---|
4 | SIGILL | 非法指令 | 执行了非法指令、格式错误、未知或特权指令,通常是因为可执行文件本身出现错误,或者试图执行数据段,堆栈溢出时也有可能产生这个信号。 |
5 | SIGTRAP | 断点或陷阱异常 | 由断点指令或其它trap指令产生。 |
6 | SIGABRT | abort发出的信号 | 调用abort函数生成的信号。 |
7 | SIGBUS | 非法内存访问 | 非法地址,包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数,但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。 |
8 | SIGFPE | 浮点异常 | 在发生致命的算术运算错误时发出,不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。 |
11 | SIGSEGV | 无效内存访问 | 试图访问未分配给自己的内存,或试图往没有写权限的内存地址写数据。 |
16 | SIGSTKFLT | 栈溢出 | 堆栈溢出。 |
31 | SIGSYS | 系统调用异常 | 非法的系统调用(Seccomp 限制) |
CppCrash故障定位
辅助工具介绍
(1)反编译addr2line
Linux下addr2line命令用于将程序指令地址转换为所对应的函数名、以及函数所在的源文件名和行号。在问题定位时,可以使用llvm-addr2line.exe来替换addr2line,其对应的路径在"SDK安装路径\版本路径\base\native\llvm\bin"下,使用时可以参考以下命令。其中,libentry.so是报错的动态链接库,0000000000001c14是报错的内存地址。
llvm-addr2line.exe -a -C -i -f -e xxx\...\libentry.so 0000000000001c14
表2 参数说明
参数 | 功能描述 |
---|---|
-C | 去除名称修饰 |
-f | 在反汇编结果中显示函数名 |
-a | 显示内存地址 |
-i | 解开内联函数 |
-e | 指定待还原的so的路径 |
(2)反汇编objdump
objdump可以用来显示二进制文件的信息,它可以进行反汇编用于观察内存异常。在问题定位时,可以使用llvm-objdump.exe来替换objdump,其对应的路径在"SDK安装路径\版本路径\base\native\llvm\bin"下,使用时可以参考以下命令。其中,libentry.so是需要反汇编的动态链接库,text.txt用于存储反汇编的代码。
llvm-objdump.exe -S -d xxx\...\libentry.so > text.txt
表3参数说明
选项 | 功能说明 |
---|---|
-C | 将低级符号名称解码(解映射)为用户级名称。除了删除系统预置的任何初始下划线外,这还使C++函数名可读。不同的编译器有不同的篡改样式。可选的 demangling 样式参数可用于为编译器选择适当的 demangling style 。 |
-d | 显示 objfile 中机器指令的汇编助记符。此选项仅反汇编那些预期包含指令的部分。 |
-D | 像 -d 一样,但反汇编所有部分的内容,而不仅仅是那些预期包含指令的部分。 |
-F | 在反汇编 sections 时,无论何时显示符号,都要显示要转储的数据区域的文件偏移量。如果跳过了零,那么当反汇编恢复时,告诉用户跳过了多少个零,以及反汇编恢复位置的文件偏移量。转储节时,显示转储开始位置的文件偏移量。0000000000000665 (File Offset: 0x665): so 共享库的某个反汇编函数入口000000000040052d (File Offset: 0x52d): 可执行文件的某个反汇编函数入口 |
-l | 用与显示的目标代码或重定位相对应的文件名和源行号标记显示(使用调试信息) |
-S | 如果可能的话(有调试信息),显示混合了反汇编的源代码。 |
(3)ASan
ASan(Address-Sanitizer)是内存检测的工具,用于发现内存飞踩第一现场,DevEco Studio已集成ASan为开发者提供面向C/C++的地址越界检测能力。ASan可以解决一些踩内存导致的异常crash的补充手段,对于一些明显不可能crash的场景可以尝试开启ASan。
CppCrash故障定位方法
CppCrash常用故障定位分为本地环境定位和系统环境定位,其主要的定位思路类似,详细的定位方法如下所示:
(1)找出问题必现条件,复现问题场景;
(2)先开多线程检测配置,排查多线程安全问题;
先打开多线程检测配置,确认问题是否属于多线程安全问题,若打开多线程检测配置之后还是报当前错误栈,则进行下一步排查。打开开关的hdc指令为:
hdc shell param set persist.ark.properties 0x107c
hdc shell reboot
说明
persist.ark.properties的默认值为0x105c(即关闭状态)。
(3)根据信号和错误码初步推断crash原因。如下图所示,错误原因为SIGSEGV(SEGV_ACCERR),说明是xmlparser崩溃相关原因。
(4)根据堆栈信息找出报错的栈顶函数。
如下图所示,在开发态发生CppCrash时,在本地环境的DevEco Studio中,Native栈帧和JS栈帧可以通过蓝色链接直接跳转到对应的代码行。
在运行态下,开发者可以找到报错的日志,根据对应的报错日志使用llvm_addr2line解析出栈帧的对应的代码行号。案例如下所示。
说明
使用addr2line后,如果得出的行号看起来不是很正确,可以考虑对地址进行微调(如减1),或者考虑关闭一些编译优化,已知使用LTO(Link Time Optimization)的二进制可能无法正确获得行号。
(5)若无法定位出具体原因,可以使用llvm-objdump反汇编代码,再进一步进行分析。
分析反汇编可参考以下步骤:
- 执行反汇编命令,找到报错的汇编代码;
- 追踪汇编代码中,寄存器数据的来源,确定导致程序出错的那部分代码。
- 结合报错信息和寄存器数据,推断程序报错的原因。
- 根据对报错原因的分析,进行代码修正或优化,以解决程序出错的问题。
(6)如果有理论上不应该发生堆栈报错,可以考虑开启ASan,再根据ASan定位分析具体原因。
如下所示,发生crash后,根据ASan故障日志定位错误代码。
TIMESTAMP:20231228152114
Pid:24923
Uid:20020033
Process name:
Reason:AddressSanitizer:heap-use-after-free
Fault thread Info:
==appspawn==24923==ERROR: AddressSanitizer: heap-use-after-free on address 0x007f9653e338 bp 0x007fd4635ae0 sp 0x007fd46352d0
READ of size 4 at 0x006116401a10 thread T0 (id.commom.babel)
#0 0x7f9653e334 (/system/lib64/libclang_rt.asan.so+0x7e334)
#1 0x557fe4e8c8 (/system/lib64/ndk/libnative_rdb.ndk.z.so+0xe8c8)
#2 0x55800d3f50 (/data/storage/el1/bundle/libs/arm64/libbabel.so+0x253f50)
#3 0x557ff6ef1c (/data/storage/el1/bundle/libs/arm64/libbabel.so+0xeef1c)
#4 0x557ff5f394 (/data/storage/el1/bundle/libs/arm64/libbabel.so+0xdf394)
#5 0x55800d0a08 (/data/storage/el1/bundle/libs/arm64/libbabel.so+0x250a08)
#6 0x7f9dd68d5c (/system/lib64/platformsdk/libace_napi.z.so+0x28d5c)
0x006116401a10 is located 0 bytes inside of 46-byte region [0x006116404a10,0x006116404a40)
...
常见问题
应用开发时,发生CppCrash的常见场景有非法参数、多线程安全、napi_value生命周期、空指针等。常见的CppCrash的场景、原因汇总如下表所示。
表4 常见问题总结表
常见场景 | 常见信号及错误码 | 定位思路 | 崩溃原因 |
---|---|---|---|
非法参数 | SIGSEGV(SEGV_MAPERR)@0X00402109F85E8090(内存地址为极大的非正常地址) | 如果有cppcrash栈直接崩溃在libace_napi.z.so/libark_jsruntime.so/libace_napi_ark.z.so,并且libace_napi.z.so的栈帧位置较浅。这种问题往往需要napi模块的上层使用者优先去排查。可以参考案例 传入的napi_env的虚函数表指针为大地址、传入的napi_value异常、传入的NativeValue*为空指针。 | 传入的napi_env的虚函数表指针为大地址 |
SIGSEGV(SEGV_MAPERR)@0X000000000000001c(内存地址为较小的地址) | 可能原因为传入的napi_value异常 | ||
SIGSEGV(SEGV_MAPERR)@0X0000000000000008 | 传入的NativeValue*为空指针 | ||
SIGSEGV(SEGV_MAPERR)@0X0000000000000012 | 传入的napi_defer为undefined | ||
SIGSEGV(SEGV_MAPERR)@0X0000000000000022 | 返回的ArrayBufferRef为undefined | ||
多线程安全 | SIGSEGV(SEGV_MAPERR)@0X0000000000000028 | 崩溃栈从上往下看,略过libark_jsruntime.so/libace_napi.z.so/libace_napi_ark.z.so,看是从哪个上层模块的so调用过来的,该上层模块就存在多线程安全问题。 | napi_env释放后仍被使用 |
SIGSEGV(SEGV_MAPERR)@0X0000000000001f98 | static变量 | ||
napi_value生存期 | SIGSEGV(SEGV_MAPERR)@0X000000000000002a(报错为:Can not get Prototype on non ECMA Object) | 根据崩溃栈反编译找到出现问题的napi接口的上层接口,在上层接口内找到出问题的napi_value,检查napi_value的使用范围是否超出了napi_handle_scope的作用域范围。 | napi_value超出NAPI框架的scope |
xmlparser崩溃 | SIGSEGV(SEGV_ACCERR)@0x0000007f8e71d000 | 如果崩溃在libxml.z.so,需要检查xml解析的代码 | xmlparser崩溃 |
native层调用iconv函数崩溃 | SIGSEGV(SEGV_MAPERR)@0x000000808566e685 | 查看日志中是否有iconv关键字确定报错类型,并根据报错日志定位到错误代码 | native层调用iconv函数crash |
非法参数是CppCrash的常见场景之一。在napi接口中,所需参数一般有两种,一种是napi_env,一种是napi_value,实际上,这两种入参都是裸指针(非智能指针),napi接口中会对他们进行空指针拦截,然而,无法拦截其他的野指针(仅声明但未做初始化,且不为空的指针)。因此,如果有cppcrash栈直接崩溃在libace_napi.z.so/libark_jsruntime.so/libace_napi_ark.z.so,并且libace_napi.z.so的栈帧位置较浅。此类问题一般都是napi模块的上层模块在调用napi接口时传参有问题导致,这种问题往往需要napi模块的上层使用者优先去排查。
在多线程检测机制中,会判断当前线程的thread id与被使用的vm/env中的thread id是否一致,若不一致,则表明vm/env被跨线程使用,可能引发多线程安全问题,被拦截日志拦截(Fatal:ecma_vm cannot run in multi-thread!)。在定位多线程的崩溃时,需要从上往下看崩溃栈,略过libark_jsruntime.so/libace_napi.z.so/libace_napi_ark.z.so,看是从哪个上层模块的so调用过来的,该上层模块就存在多线程安全问题。
开发者在书写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就会访问已释放的内存,产生问题。 在产生napi_value生存期问题时,需要根据崩溃栈反编译找到出现问题的napi接口的上层接口,在上层接口内找到出问题的napi_value,检查napi_value的使用范围是否超出了napi_handle_scope的作用域范围。
libuv非法释放
问题描述
在测试hap包时,ASan出现以下报错:
定位分析
从堆栈中可知,应用在调用libuv.so之后直接报踩内存错误,报错地址为0x21304。
根据动态链接库libuv.so和报错内存地址0x21304,执行命令 addr2line.exe -fie libuv.so 0x21304 进行反编译,得到以下信息。
/.../../libuv/src/timer.c:59
根据反编译信息可知,报错发生在uv_timer_init函数中。
当前,报错定位到源码 uv__handle_init(loop, (uv_handle_t)handle, UV_TIMER),需要确认这个方法在调用中哪个地址被踩了,所以需要对0x21304进行反汇编;*
在汇编代码中,发现0x21304是由X23寄存器赋值给X0,继续追踪X23寄存器。
X23寄存器是 X19寄存器加上立即数 0x28。
X19寄存器是X1寄存器赋值,而X1寄存器则对应int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle)的第2个入参 handle(一般而言X0寄存器对应方法的第一个参数,依次下去)。
再返回到代码中进行下一步分析,首先分析uv_timer_t* handle的结构体,其代码如下:
typedef struct uv_timer_s uv_timer_t;
struct uv_timer_s {
UV_HANDLE_FIELDS
UV_TIMER_PRIVATE_FIELDS
};
#define UV_HANDLE_FIELDS
void* data;
uv_loop_t* loop;
uv_handle_type type;
uv_close_cb close_cb;
void* handle_queue[2];
union {
int fd;
void* reserved[4];
} u;
UV_HANDLE_PRIVATE_FIELDS
通过计算uv_timer_s的偏移,第40字节偏移应该是指向 handle_queue[1]处,接着分析uv_handle_init函数如下,可以看到该函数除了初始化之外,还有一个将handle放到loop->handle_queue的操作。
int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle) {
uv__handle_init(loop, (uv_handle_t*)handle, UV_TIMER);
handle->timer_cb = NULL;
handle->timeout = 0;
handle->repeat = 0;
return 0;
}
#define uv__handle_init(loop_, h, type_)
do {
(h)->loop = (loop_);
(h)->type = (type_);
(h)->flags = UV_HANDLE_REF; /* Ref the loop when active. */
QUEUE_INSERT_TAIL(&(loop_)->handle_queue, &(h)->handle_queue);
uv__handle_platform_init(h);
}
while (0)
#define QUEUE_INSERT_TAIL(h, q)
do {
QUEUE_NEXT(q) = (h);
QUEUE_PREV(q) = QUEUE_PREV(h);
QUEUE_PREV_NEXT(q) = (q);
QUEUE_PREV(h) = (q);
}
while (0)
#define QUEUE_NEXT(q) (*(QUEUE **) &((*(q))[0]))
#define QUEUE_PREV(q) (*(QUEUE **) &((*(q))[1]))
#define QUEUE_PREV_NEXT(q) (QUEUE_NEXT(QUEUE_PREV(q)))
#define QUEUE_PREV(h) (QUEUE_PREV(QUEUE_NEXT(q)))
通过代码流程分析,在uv_handle_init中确实有handle->handle_queue操作的动作,就是这个QUEUE_INSERT_TAIL; 现在故障现场已经找到了,但是不知道这里是如何被踩的。
**继续检查QUEUE_INSERT_TAIL,通过加日志测试可知参数 h 为无效指针(已经被释放),但是在这里又被使用到了,所以导致 uaf 问题。*一路回溯上去就确认是 uv__handle_init(loop, (uv_handle_t)handle, UV_TIMER) 中的第二个参数 handle 的问题。
最后检查代码确认,在 unschedule 函数中的 else 分支内,ptrRef 被提前 delete 掉了;此处应跟 if 分支里面的实现一样,采用libuv异步方式释放内存,用 uv_close 的形式交给 libuv 自己去管控 handle 的生命周期。
bool Timer::unschedule(shared_ptr<Loop> loop, shared_ptr<Source> source) {
...
if (uv_is_active((uv_handle_t *)_timer)) {
uv_close((uv_handle_t *)_timer, [](uv_handle_t *handle) {
auto ptrRef = static_cast<shared_ptr<Source> *>(handle->data);
handle->data = nullptr;
delete ptrRef;
});
} else {
auto ptrRef = static_cast<shared_ptr<Source> *>(handle->data);
handle->data = nullptr;
delete ptrRef;
}
}
传入的napi_env的虚函数表指针为大地址
问题描述
如果有cppcrash栈直接崩溃在libace_napi.z.so/libark_jsruntime.so/libace_napi_ark.z.so,并且libace_napi.z.so的栈帧位置较浅。此类问题一般都是napi模块的上层模块在调用napi接口时传参有问题导致,这种问题往往需要napi模块的上层使用者优先去排查。 案例崩溃栈如下所示:
Timestamp:2024-03-12 15:26:53.703
Pid:20330
Uid:0
Process name:com.example.xxx
Reason:Signal:SIGSEGV(SEGV_MAPERR)@0x00402109f85e8090
Fault thread Info:
Tid:20330, Name:m.example.xxx
#00 pc 00000000000184cc /system/lib64/platformsdk/libace_napi.z.so(napi_get_undefined+36)(555556bbed21a6ac076967a1f0c9786f)
#01 pc 000000000007a4a0 /data/storage/el1/bundle/libs/arm64/libesharehm.so(ScreenCastPlayer::onArkTsMessage(int)+196)(6bd7286a4b818945d585a2a67f9758a45339e587)
定位分析
在崩溃栈中,可以发现崩溃地址是一个极大的非正常地址。使用addr2line工具反编译解析代码行后看到,崩溃在46行,也就是尝试调用env里面的CreateUndefined方法挂掉了,而且是还没调用进去就挂了,CreateUndefined是engine上的虚方法,调用这个函数分为三步,一是从engine类上取出虚表,二是从虚表中拿出函数指针,三是跳转。第一步会对env解一次引用,拿出虚表指针,第二步会对虚表指针偏移后解引用,拿出函数指针。对于本案例来说,出问题的可能会出现在第一、二步,现在就要排查到底是env是个大地址,还是虚表是个大地址。
说明
如果是第三步出了问题,报的崩溃栈是not mapped pc错误
// Getters for defined singletons
NAPI_EXTERN napi_status napi_get_undefined(napi_env env, napi_value* result)
{
CHECK_ENV(env);
CHECK_ARG(env, result);
auto engine = reinterpret_cast<NativeEngine*>(env);
auto resultValue = engine->CreateUndefined();
*result = reinterpret_cast<napi_value>(resultValue);
return napi_clear_last_error(env);
}
接下来我们看带寄存器信息,x0是第一个入参,也就是env,我们可以看到,env落在libuv.so里面,这明显有问题,也就是libesharehm.so传入的env这个裸指针已经非法。从这个非法地址中取出第一个域(虚表)是一个大地址b9402109f85e8008,然后从这个虚表中偏移一定位数之后解引用,尝试取函数指针,b9402109f85e8008解引用导致崩溃,报SEGV_MAPERR错误。崩溃栈顶为@0x00402109f85e8090 (dfx将高位两0抹除)。
对于本案例,有两个上层的排查建议:
- 打开多线程检测后压测,看是否是多线程安全问题引起的问题。如果打开之后还是报这个栈,则进行第二步排查。
hdc shell param set persist.ark.properties 0x107c
hdc shell reboot
- 重点排查是否env非上层so自己保存下来了,注意,保存env这个行为很危险,因为保存者并不知道env什么时候被释放掉。可以在上层中加维测日志打印env地址,并且ArkNativeEngine析构时打印地址,可以看看是否是env被析构后还在使用。
传入的napi_value异常
问题描述
此类问题基本都是参数传的有问题,其崩溃栈如下所示:
定位分析
首先,使用addr2line工具反编译找到出现问题的接口及行号。
从崩溃原因 SIGSEGV(SEGV_MAPERR)@0x000000000000001c ,大致可以推断出 napi_typeof 的第二个参数 value 有问题。但是是其本身的值有问题,还是其指向的内存有问题,需要用objdump工具反汇编并结合寄存器的值进一步分析。
从cppcrash文件Registers可以看出value本身即 x1 的值000000558473f264 看起来没什么问题(不确定地址是否是8字节对齐的地址,这里暂且假设地址是正常的),所以我们继续分析其指向的内存。
出现问题的汇编代码是 ldr x8 [x8, #24] , x8 是 x1 值所指向的内容,其值为0x4 ,很明显 x8 加上立即数 #24 就是崩溃的地址 0x1c 。
另外,x8的值从Memory near registers也可以看出来,000000558473f264地址对应的值为0x4。
回到出现问题的行号,可以看出是在调用TypeOf函数,它是一个虚函数,因此x8就是虚函数表地址,24就是虚函数TypeOf在虚函数表中的偏移量。正常情况下,ldr x8 [x8, #24]执行完后,x8就是TypeOf函数地址,紧接着再执行blr x8就完成了TypeOf函数调用。
通常这类问题有两个原因:
- value本身地址不对,此类问题要排查传参赋值的地方有没有初始化。
- value地址正常,其指向的内存有问题,此类问题要排查内存是不是已经被GC回收了。
传入的NativeValue*为空指针
问题描述
调用用SetProperty发生崩溃,堆栈如下:
定位分析
通过addr2line工具,得知崩溃时的调用代码如下。
bool ArkNativeObject::SetProperty(const char* name, NativeValue* value)
{
auto vm = engine_->GetEcmaVm();
LocalScope scope(vm);
Global<ObjectRef> obj = value_;
Local<StringRef> key = StringRef::NewFromUtf8(vm, name);
Global<JSValueRef> val = *value;
return obj->Set(vm, key, val.ToLocal(vm));
}
但这里看不出具体原因,所以继续使用objdump反汇编看一下具体的汇编指令。
从堆栈上看,最后崩溃在 2bc0c 上,反汇编结果对应的指令如下:
上面的指令ldr x1, [x19, #8],其中x19为参数value(x19最近一次是通过x2赋值的,x2就是第二个参数value,x0为this,x1为第一个参数name),结合上面Registers可以看出,x19寄存器的值为0x0,加上了偏移8,也就是取地址0x8的内容。
到此,可以得出初步结论SetProperty的时候,传入的第二个参数value为nullptr。 另外一方面从日志也可以看出,创建JsResourceManager也发生了异常。
NativeValue* CreateJsBaseContext(NativeEngine& engine, std::shared_ptr<Context> context, bool keepContext)
{
NativeValue* objValue = engine.CreateObject();
NativeObject* object = ConvertNativeValueTo<NativeObject>(objValue);
if (object == nullptr) {
HILOG_WARN("invalid object");
return objValue;
}
auto jsContext = std::make_unique<JsBaseContext>(context);
SetNameNativePointer(engine, *object, BASE_CONTEXT_NAME, jsContext.release(), JsBaseContext::Finalizer);
auto appInfo = context->GetApplicationInfo();
if (appInfo!= nullptr) {
object->SetProperty("applicationInfo", CreateJsApplicationInfo(engine, *appInfo));
}
auto hapModuleInfo = context->GetApplicationInfo();
if (hapModuleInfo!= nullptr) {
object->SetProperty("currentHapModuleInfo", CreateJsApplicationInfo(engine, *hapModuleInfo));
}
auto resourceManager = context->GetApplicationInfo();
if (appInfo!= nullptr) {
object->SetProperty("resourceManager", CreateJsApplicationInfo(engine, resourceManager, context));
}
BindNativeProperty(*object, "cacheDir", JsBaseContext::GetCacheDir);
BindNativeProperty(*object, "tempDir", JsBaseContext::GetTempDir);
BindNativeProperty(*object, "filesDir", JsBaseContext::GetFilesDir);
BindNativeProperty(*object, "distributeFilesDir", JsBaseContext::GetDistributeFilesDir);
BindNativeProperty(*object, "databaseDir", JsBaseContext::GetDatabaseDir);
BindNativeProperty(*object, "preferencesDir", JsBaseContext::GetPreferencesDir);
BindNativeProperty(*object, "bundleCodeDir", JsBaseContext::GetBundleCodeDir);
BindNativeProperty(*object, "area", JsBaseContext::GetArea);
const char *moduleName = "JsBaseContext";
BindNativeFunction(engine, *object, "createBundleContext", moduleName, JsBaseContext::CreateBundleContext);
BindNativeFunction(engine, *object, "getApplicationContext", moduleName, JsBaseContext::GetApplicationContext);
BindNativeFunction(engine, *object, "switchArea", moduleName, JsBaseContext::SwitchArea);
BindNativeFunction(engine, *object, "getArea", moduleName, JsBaseContext::GetArea);
BindNativeFunction(engine, *object, "createModuleContext", moduleName, JsBaseContext::CreateModuleContext);
return objValue;
}
接下来,继续分析一下x19 需要加上#8的原因。下面3条指令是调用 GetHandleAddr(const EcmaVM *vm, uintptr_t localAddress) 方法,这样对应起来[x19, #8]保存的应该是address。对比代码,可以看出下面的指令对应的逻辑为val.ToLocal 。
2bc0c: f9400661 ldr x1, [x19, #8]
2bc10: aa1603e0 mov x0, x22
2bc14: 94000abf bl -0x2e710 <_ZN5panda6JSNApi13GetHandleAddrEPKNS_10ecmascript6EcmaVMEm@plt>
Local<T> ToLocal() const
{
if (IsEmpty()) {
return Local<T>();
}
return Local<T>(vm_, *this);
}
template<typename T>
Local<T>::Local(const EcmaVM *vm, const Global<T> ¤t)
{
address_ = JSNApi::GetHandleAddr(vm, reinterpret_cast<uintptr_t>(*current));
}
NativeValue到Global涉及了多层的转换, NativeValue* 指向的内容和Global是一样的,所以通过Global拿address等同于通过NativeValue*去取其第一个成员的值。因为NativeValue析构函数为virtual,所以取成员需要加上虚表偏移#8 。
class NativeValue {
public:
virtual ~NativeValue() {}
template<typename T> operator T()
{
return value_;
}
...
}
因此,问题根因已找到:CreateJsResourceManager返回了nullptr。
返回的ArrayBufferRef为undefined
问题描述
此类问题崩溃栈如下所示:
定位分析
造成上述现象的原因是因为this为undefined,undefined在运行时中的编码是0x02,0x22是undefined去取 ArrayBufferData 这个域导致的崩溃。
因此需要往上分析:
ArkNativeArrayBuffer::ArkNativeArrayBuffer(ArkNativeEngine* engine, uint8_t** value, size_t length)
: ArkNativeArrayBuffer(engine, JSValueRef::Undefined(engine->GetEcmaVm()))
{
auto vm = engine->GetEcmaVm();
LocalScope scope(vm);
value_ = Global<ArrayBufferRef>(vm, ArrayBufferRef::New(vm, length));
if (value != nullptr) {
Global<ArrayBufferRef> obj = value_;
*value = reinterpret_cast<uint8_t*>(obj->GetBuffer());
}
}
从构造函数可以看出,JSArrayBuffer为undefined说明value_为undefined。
此时有两种情况:
- 应用存在异常导致 ArrayBufferRef::New(vm, length) 返回了一个undefined,需要看流水日志确认是否有"print exception info: "打印,如果有,则根据日志提示的具体异常信息排查对应位置的代码。
- engine存在问题,需要上层根据代码去排查一下,该崩溃栈中的上层so是libnapi-adapter.so。
排查建议:
- 打开多线程检测开关后重新复现问题,验证是否是多线程安全问题。如果打开之后还是报相同的崩溃栈,则进行第二步排查。
hdc shell param set persist.ark.properties 0x107c
hdc shell reboot
- 重点排查env是否被非上层模块so(非libnapi-adapter.so)自己保存下来了,注意,保存env这个行为很危险,因为保存者并不知道env什么时候被释放掉。可以在上层模块中加维测日志打印env地址,并且ArkNativeEngine析构时打印地址,可以看看是否是env被析构后还在使用。
传入的argv与argc大小不一致
问题描述
napi_get_cb_info接口的入参包括argv和argc。argv表示存放一定数量的napi_value的数组,argc表示数组argv的长度。
在使用napi_get_cb_info接口时,应注意argc的值需要与argv的有效长度保持一致:
- 如果argc大于argv的实际长度,则会在后续遍历argv时造成数组越界访问,读溢出;
- 如果argc等于argv的实际长度,但argv中的有效元素个数小于argv的实际长度(例如声明了argv[2],但只对argv[0]和argv[1]进行了初始化),则napi接口内会将argv中的剩余元素全部设置成undefined(与node代码的规则是一致的)。此时若使用nullptr去判断argv中的某个napi_value是否有效,是不合理的,因为undefined一定不等于nullptr,那么这些绕开nullptr判空检查的napi_value(值为undefined)会被误认为是有效的napi_value,很有可能在后续使用时产生预料之外的问题。
定位分析
传入的argc,等于argv的实际长度,但大于argv的有效长度(例如声明了argv[2],但只对argv[0]和argv[1]进行了初始化,这种情况下,argv实际长度为3,有效长度为2):
size_t argc = 3;
napi_value argv[3]= {nullptr};
double v1 = 1.1;
napi_create_double(env, v1, &argv[0]);
double v2 = 2.1;
napi_create_double(env, v1, &argv[1]);
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
这种情况下,不能通过argv[i] != nullptr作为条件去判断元素是否有效,而应该通过napi_typeof去检查argv[i]的类型是否为napi_undefined去判断元素是否有效。在一些代码中,会先对argc参数个数进行检查,再对argv[i]参数类型进行检查,最后又检查argv[i]是否为nullptr,此时的判空逻辑是多余的,可以去除。以下列举多余判空的例子:
napi_value NapiShareManager::Cancel(napi_env env, napi_callback_info info)
{
SHARE_MANAGER_HILOGI(JS_NAPI, "NapiShareManager::Cancel enter.");
// argument count
size_t argc = ARG_1;
size_t expectArgc = ARG_1;
// argument vector
napi_value argv[ARG_1] = { 0 };
napi_value thisVar = nullptr;
void *data = nullptr;
napi_status status = napi_get_cb_info(env, info, &argc, argv, &thisVar,&data);
NAPI_ASSERT(env, status == napi_ok, "Bad parameters");
NAPI_ASSERT(env, argc == expectArgc, "Cancel requires 1 parameter");
napi_valuetype valueType = napi_null;
// argv[0] is obj which type is ShareDeviceInfo
napi_typeof(env, argv[ARG_0], &valueType);
NAPI_ASSERT(env, valueType == napi_object, "type mismatch for parameter 1");
napi_value ret;
// 多余判空,前面已经判断过type和参数个数
if (argv[ARG_0] == nullptr) {
napi_get_boolean(env, false, &ret);
return ret;
}
...
return ret;
}
TaskPool & Worker多线程问题
问题描述
部分应用在使用TaskPool或Worker时出现了多线程问题,主要的形式是底层使用了std::map<napi_env, napi_ref>等形式,直接或间接通过env地址作为key来存取napi_ref。
static std::shared_ptr<ClearCacheListener> g_clearCacheListener;
static std::unordered_map<Query, napi_ref, QueryHash> cache;
static std::string g_ownBundleName;
struct Query {
std::string bundleName_;
std::string interfaceType_;
int32_t flags_ = 0;
int32_t userId_ = Constants::UNSPECIFIED_USERID;
napi_env env_;
Query(const std::string &bundleName, const std::string &interfaceType, int32_t flags, int32_t userId, napi_env env)
: bundleName_(bundleName), interfaceType_(interfaceType), flags_(flags), userId_(userId), env_(env) {}
bool operator==(const Query &query) const
{
return bundleName_ == query.bundleName_ && interfaceType_ == query.interfaceType_ &&
flags_ == query.flags_ && userId_ == query.userId_ && env_ == query.env_;
}
};
定位分析
排查步骤如下所示:
- 多线程场景下TaskPool使能了负载均衡机制,而Worker也提供了 new Worker 及 terminate 接口。两种机制都能够新建和释放线程,这种情况下env的生命周期具有不确定性。
- 基于C++内存管理机制,env释放后该地址仍有重用的可能。因此,内存中某一个已经释放的env地址会被重新分配给新的TaskPool和Worker线程env。
- 新线程和已经释放线程的env地址可能是一样的,因此通过env作为key访问全局数据结构时,就可能取到了已经失效的ref value(含有已经析构的vm)。此时会出现多线程、非法内存访问等问题。
- 子系统需要排查是否存在对应的逻辑,并通过注册 napi_add_env_cleanup_hook 合理维护env及对应数据的生命周期。
- 此外,TaskPool无法感知底层是否保存env,因而在满足回收策略时会释放空闲线程。这种情况下,若通过IPC等线程再次使用env,会出现crash。但同时,TaskPool也提供了引用计数机制。
- 若某些场景下env确实不能被释放( On 等接口),可以在接口处调用IncreaseListeningCounter() 来增加引用计数,使线程池感知到env被引用,从而不会回收对应线程。
- IncreaseListeningCounter() 需要搭配 DecreaseListeningCounter() 合理使用( Off 等调用),保证一一对应,否则会造成线程长期释放不了,内存超基线。
napi_value超出NAPI框架的scope
问题描述
js侧通过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失效。
说明
NAPI框架的scope即napi_handle_scope,napi开发者可以通过napi_handle_scope来管理napi_value的生命周期。
框架层的scope嵌入在js call native的端到端流程中,即:进入开发者自己写的native方法前open scope,native方法结束后close scope。
通过使能asan定位heap-use-after-free问题
问题描述
开启Address Sanitizer后获取到的asan故障日志如下:
TIMESTAMP:20231228165420
Pid:9402Uid:20010132
Process name:com.xxxx.common.xxx
Reason:AddressSanitizer:heap-use-after-freeFault thread Info:
==appspawn==9402== ERROR: AddressSanitizer: heap-use-after-free on address0x0061043bd290 at pc 0x007f852fe338 bp 0x007febe69440 sp 0x007febe68c30
READ of size 4 at 0x0061043bd290 thread T0 (id.common.babel)
#0 0x7f852fe334 (/system/lib64/libclang_rt.asan.so+0x7e334)
#1 0x557f28e8c8 (/system/lib64/ndk/libnative_rdb_ndk.z.so+0xe8c8)
#2 0x557f513f50 (/data/storage/el1/bundle/libs/arm64/libbabel.so+0x253f50)
#3 0x557f3aef1c (/data/storage/el1/bundle/libs/arm64/libbabel.so+0xeef1c)
#4 0x557f39f394 (/data/storage/el1/bundle/libs/arm64/libbabel.so+0xdf394)
#5 0x557f510a08 (/data/storage/el1/bundle/libs/arm64/libbabel.so+0x250a08)
#6 0x7f855e8d5c (/system/lib64/platformsdk/libace_napi.z.so+0x28d5c)
...
SUMMARY: AddressSanitizer: heap-use-after-free(/system/lib64/libclang_rt.asan.so+0x7e334)
Shadow bytes around the buggy address:
0x001c20877a00: fa fa 00 00 00 00 00 00 fa fa fd fd fd fd fa fa
0x001c20877a10: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fa fa
...
定位分析
根据错误日志定位分析,发现错误发生在如下代码:
int HMDBManager::openDB() {
OH_Rdb_Config config;
config.dataBaseDir = this->dbPath.c_str();
config.storeName = "xxx.db";
config.bundleName = BabelDataManager::GetInstance() -> getInitConfiguration().appName.c_str();
config.moduleName = "xxx";
config.isEncrypt = false;
this->config = config;
int errCode;
OH_Rdb_Store *store = OH_Rdb_GetOrOpen(&config, &errCode);
this->store = store;
return errCode;
}
直接获取appName并将其转换为C风格的字符串会存在一个问题。因为appName.c_str()返回的是一个临时字符串的指针。这个临时字符串在表达式结束后就会被销毁。这就以为着config.bundleName可能会指向一个已经被销毁的字符串,这将导致未定义的行为。
修改错误代码如下所示,这种方式中,首先获取了appName的一个副本,然后将其转换为C风格的字符串。在这种情况下,appName的生命周期会持续到当前的代码块结束。因此,config.bundleName指向的字符串在当前代码块内是有效的。
string appName = BabelDataManager::GetInstance() -> getInitConfiguration().appName;
config.bundleName = appName.c_str();
锁范围不足导致的Crash问题
问题描述
设备开关机压测时,崩溃在libcesfwk_core.z.so,崩溃栈如下:
Timestamp:1970-11-28 13:44:49.206
Pid:2906
Uid:10006
Process name:com.ohos.xxx
Reason:Signal:SIGSEGV(SEGV_MAPERR)@0x00492e6b7766746e
Fault thread Info:
Tid:2978, Name:com.ohos.system
#00 pc 000000000003fea8 /system/lib64/libipc_core.z.so(OHOS::PeerHolder::Remote()+12)
#01 pc 000000000001c5a4 /system/lib64/libcesfwk_core.z.so(OHOS::EventFwk::CommonEventProxy::SendRequest(OHOS::EventFwk::ICommonEvent::Message, OHOS::MessageParcel&,OHOS::MessageParcel&)+168)
#02 pc 000000000001cff8 /system/lib64/libcesfwk_core.z.so(OHOS::EventFwk::CommonEventProxy::SubscribeCommonEvent(OHOS::EventFwk::CommonEventSubscribeInfo const&,OHOS::sptr<OHOS::IRemoteObject> const&)+540)
#03 pc 0000000000016518 /system/lib64/libcesfwk_core.z.so(OHOS::EventFwk::CommonEvent::SubscribeCommonEvent(std::__1::shared_ptr<OHOS::EventFwk::CommonEventSubscriber> const&)+468)
#04 pc 0000000000012c20 /system/lib64/libcesfwk_innerkits.z.so(OHOS::EventFwk::CommonEventManager::SubscribeCommonEvent(std::__1::shared_ptr<OHOS::EventFwk::CommonEventSubscriber> const&)+56)
#05 pc 000000000003253c /system/lib64/module/libcommonevent.z.so
#06 pc 0000000000019808 /system/lib64/libace_napi.z.so(NativeAsyncWork::AsyncWorkCallback(uv_work_s*)+316)
#07 pc 000000000001156c /system/lib64/libuv.so
#08 pc 00000000000d02a0 /system/lib64/libc.so(__pthread_start(void*)+40)
#09 pc 0000000000072128 /system/lib64/libc.so(__start_thread+68)
...
定位分析
根据Reason可知为野指针,根据 #01 定位到具体的代码行有:
$ addr2line -Cpie ./notification/common_event_service/libcesfwk_core.z.so 000000000001c5a4
/mnt/disk/jenkins/ci/workspace/zidane_system_pipeline_release/compile/component_code/out/baltimore/../../base/notification/common_event_service/frameworks/core/src/common_event_proxy.cpp:385
对应的代码如下所示:
bool CommonEventProxy::SendRequest(ICommonEvent::Message code, MessageParcel &data, MessageParcel &reply) {
EVENT_LOGD("start");
sptr <IRemoteObject> remote = Remote();
if (remote == nullpte) {
EVENT_LOGD("Remote is NULL, %{public}d", code);
}
MessageOption option(MessageOption::TF_SYNC);
int32_t result = remote->SendRequest(static_cast<uint32_t>(code), data, reply, option);
if (result != OHOS::NO_ERROR) {
return false;
}
EVENT_LOGD("end");
return true;
}
Remote的代码如下所示:
PeerHolder::PeerHolder(const sptr<IRemoteObject> &object) : remoteObject_(object)
{}
sptr<IRemoteObject> PeerHolder::Remote()
{
return remoteObject_;
}
继续查找remoteObject_的来源,可以知道CommonEventProxy有如下继承关系:
IRemoteObject 有如下构造函数:
template <typename INTERFACE>
IRemoteProxy<INTERFACE>::IRemoteProxy(const sptr<IRemoteObject> &object) :
PeerHolder(object)
{
}
CommonEventProxy 有如下构造函数:
CommonEventProxy::CommonEventProxy(const sptr<IRemoteObject> &object) :
IRemoteProxy<ICommonEvent>(object)
{
EVENT_LOGD("CommonEventProxy instance created");
}
继续查看CommonEventProxy,CommonEventProxy 的构造函数在文件 common_event.cpp 中:
bool CommonEvent::GetCommonEventProxy()
{
EVENT_LOGI("enter");
if (!commonEventProxy_ || !isProxyValid_) {
std::lock_guard<std::mutex> lock(mutex_);
if (!commonEventProxy_ || !isProxyValid_) {
sptr<ISystemAbilityManager> systemAbilityManager = SystemAbilityManagerClient::GetInstance().GetSystemAbilityManager();
if (!systemAbilityManager) {
EVENT_LOGE("Failed to get system ability mgr.");
return false;
}
sptr<IRemoteObject> remoteObject = systemAbilityManager -> GetSystemAbility(COMMON_EVENT_SERVICE_ID);
if (!remoteObject) {
EVENT_LOGE("Failed to get COMMON Event Manager.");
return false;
}
commonEventProxy_ = iface_cast<ICommonEvent>(remoteObject);
if ((!commonEventProxy_) || (!commonEventProxy_ -> AsObject())) {
EVENT_LOGE("Failed to get COMMON Event Manager's proxy");
return false;
}
recipient_ = new CommonEventDeathRecipient();
if (!recipient_) {
EVENT_LOGE("Failed to create death Recipient ptrCommonEventDeathRecipient!");
return false;
}
commonEventProxy_ -> AsObject() -> AddDeathRecipient(recipient_);
}
}
isProxyValid_ = true;
return true;
}
可以看到在如下代码中,构造了CommonEvent 实例,但是isProxyValid_并不在锁保护的范围内,可能锁所保护的代码段会重入,从而导致崩溃。锁实际上需要保护的是commonEventProxy_和isProxyValid_,但实际上只保护了commonEventProxy_,存在重入的可能,因此使用锁的时候,需要明确具体到底需要保护的是什么数据。这里将isProxyvalid_加入到锁保护的范围内即可解决问题。
commonEventProxy_ = iface_cast(remoteObject);