写点什么

快速定位鸿蒙应用开发态崩溃问题

  • 2026-09-08
    北京
  • 本文字数:3227 字

    阅读完需:约 11 分钟

AI摘要

本文以鸿蒙应用中内存越界为例,系统演示HWASan与BinXO工具协同定位踩内存崩溃的完整实践路径。提出可复用的崩溃定位范式,覆盖越界访问、未初始化读、释放后访问、重复释放四类高频崩溃场景。

  • HWASan用于编译期插桩实现细粒度内存访问检测
  • BinXO支持符号化CppCrash日志,精准映射崩溃点到源码行
  • 四类踩内存场景具备共性检测逻辑,可迁移至其他Native崩溃排查

适合鸿蒙应用开发者、C++底层工程师、系统稳定性工程师阅读

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:快速定位鸿蒙应用开发态崩溃问题-华为开发者话题 | 华为开发者联盟

概述

踩内存是指程序向未分配、已释放或越界的内存地址写入数据,破坏正常内存布局,从而导致程序行为异常、数据错乱甚至崩溃。这类崩溃往往复现困难、定位耗时,属于系统稳定性中的高风险问题。

本文以内存越界访问为典型案例,构建数组越界示例工程,完整演示 HWASan(参考:使用HWASan检测内存错误 )与 BinXO 工具的使用流程,提供踩内存问题的定位范式,为其他高频崩溃场景的排查提供可复用的实践参考。

高频崩溃场景

• 内存越界访问:应用程序在读写内存时,超出了合法分配的内存区域边界,触发 CppCrash 日志,最终引发系统异常或应用崩溃。

• 内存未初始化读:应用程序在读取内存区域时,该内存之前没有被赋予确定的值(例如只分配了空间但没有赋值),最终引发系统异常或应用崩溃。

• 内存释放后访问:应用程序在将一块内存归还给系统(或分配器)之后,仍然通过之前保存的指针去读取、写入或释放该内存,最终引发系统异常或应用崩溃。

• 内存重复释放:应用程序对同一块已经释放过的内存再次调用释放函数(如 free 或 delete),最终引发系统异常或应用崩溃。

• 文件句柄非法关闭:应用程序对一个已经关闭、无效或从未成功打开的文件句柄(或套接字、管道等系统资源句柄)再次执行关闭操作,最终引发系统异常或应用崩溃。

• 内存释放地址异常:应用程序传递给内存释放函数(如 free、delete)的指针地址,不是之前由相同分配器(如 malloc、new)返回的合法地址,最终引发系统异常或应用崩溃。

• 未定义异常访问:应用程序访问了操作系统未允许其访问的内存地址,从而触发硬件层级的异常(如段错误 SIGSEGV),最终引发系统异常或应用崩溃。

标准化排查流程

1、复现与日志获取

触发数组越界访问,在 DevEco Studio 底部 FaultLog 模块中获取 CppCrash 日志,记录崩溃现场。

2、分析崩溃日志

解析 CppCrash 日志发现:堆栈与业务逻辑无明显关联,呈现随机性且为低概率事件;崩溃类型为"SIGSEGV(SEGV_MAPERR)",提示访问无效内存地址。

3、开启 HWASan

启用 HWASan 工具重新编译运行,工具立即捕获越界访问并输出详细日志。单击日志中蓝色字体的堆栈行,可直接跳转至越界发生的具体方法源码位置。

4、开启 BinXO

针对三方库 so 调用场景,启用 BinXO(HWASan 增强版)重新运行。工具精准捕获三方库 so 的数组越界,日志同样支持点击蓝色堆栈快速定位到调用 so 的具体方法。

5、修复与验证

根据定位信息优化越界代码,重新编译并重复上述复现步骤,确认应用不再崩溃,问题闭环。

通过上述流程,HWASan 与 BinXO 可高效定位内存越界等底层崩溃,为复杂崩溃问题的排查提供标准化操作路径,详细流程如下图所示:

数组越界案例

案例背景

通过一个长度为 8 的数组越界访问的示例工程,完整演示 HWASan 和 BinXO 工具使用步骤及日志分析方法,示例工程分为两个场景:

• 场景一:本地代码中数组越界访问(索引大于 7)。

• 场景二:三方库 so 中数组越界访问,本地方法去访问三方库 so 中的数组越界访问方法(索引小于 0)。

分析流程

步骤 1 触发场景一和场景二的数组越界访问

 1、设置 Build Mode 为 debug 模式打开 DevEco Studio,以 debug 模式运行示例工程,如下图所示:

2、运行应用,反复滑动图片触发索引值超出范围,索引值大于 7 或者小于 0 的时候会低概率使应用崩溃产生 CppCrash 日志,如下图所示:

3、分析 CppCrash 日志,如下图所示:

应用出现“SIGSEGV(SEGV_MAPERR)”信号,表示访问了无效内存地址。该类崩溃通常由以下内存操作错误引发:

• 数组越界访问

• 野指针解引用

• 重复释放内存

• 访问已释放内存(Use-After-Free)

程序实际崩溃点仅为“受害者”——越界写入破坏了堆上关键对象,直到该对象被访问或释放时才触发其他库的安全检查或访问异常。因此,当崩溃堆栈指向系统库且与应用进程无直接关联时,应高度怀疑为踩内存问题。

步骤 2 使用 HWASan 工具分析

1、开启 HWASan 工具,如下图所示:

2、运行应用,左滑图片超出索引(>7),应用必崩溃(HWASan 工具会立即捕获本地代码数组越界访问),触发 HWASan 日志,如下图所示:

3、分析 HWASan 日志,日志中显示堆栈信息,并且蓝色链接单击跳转到具体踩内存的方法中,方便开发者定位修改,如下图所示:

4、重新运行应用,右滑图片超出索引(<0),应用低概率崩溃(HWASan 工具不会立即捕获三方库 so 中的数组越界访问),崩溃产生 HWASan 日志,如下图所示:

5、分析 HWASan 日志时发现,其输出的堆栈信息无法有效定位到具体的越界访问代码行。原因在于,HWASan 工具对第三方动态库(so)中的数组越界错误,往往不能即时捕获;即便捕获到异常,生成的日志也因堆栈信息不完整或缺乏符号解析,难以协助开发者追溯问题源头,具体现象如下图所示:

步骤 3 使用 BinXO 工具分析

1、在 HWASan 基础上打开 BinXO 开关,如下图所示:

2、重新运行应用,右滑图片超出索引(<0),应用必崩溃(BinXO 工具会立即捕获三方库 so 中数组越界访问),触发 HWASan 日志,如下图所示:

3、分析 HWASan 日志,日志中显示堆栈信息,并且蓝色链接单击跳转到具体踩内存的方法中,方便开发者定位修改,堆栈第 0 帧是调用的三方库 so 信息,第 1 帧蓝色链接单击跳转到具体踩内存的方法,如下图所示:

代码分析与优化

场景一:本地代码实现数组越界访问,imageAccessTime[index]未做边界检查,索引大于 7 会触发数组越界访问问题。需要在 imageAccessTime[index]之前做索引 index 检查(如下注释代码),防止越界。

static napi_value GetImageNameNotFromSo(napi_env env, napi_callback_info info){  std::int64_t *imageAccessTime = new std::int64_t[8]{0};    size_t argc = 1;  napi_value args[1] = {nullptr};  napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);    int index;  napi_get_value_int32(env, args[0], &index);    /*  if (index < 0 || index >= g_count) {    napi_throw_error(env, "", G_WARN_IMAGE);    return nullptr;  }  */    imageAccessTime[index] = std::time(nullptr);    if (index < 0 || index >= g_count) {    napi_throw_error(env, "", G_WARN_IMAGE);    return nullptr;  }    napi_value imageName;  napi_create_string_utf8(env, G_IMAGE_NAMES[index], strlen(G_IMAGE_NAMES[index]), &imageName);    delete[] imageAccessTime;  return imageName;}
复制代码

场景二:引入三方库 so 实现数组越界访问,GetImageName()函数入参 index 未做边界检查,索引小于 0 会触发数组越界访问问题。需要在 GetImageName()之前做索引 index 检查(如下注释代码),防止越界。

extern "C" const char *GetImageName(int index);static napi_value GetImageNameFromSo(napi_env env, napi_callback_info info){  size_t argc = 1;  napi_value args[1] = {nullptr};  napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);    int index;  napi_get_value_int32(env, args[0], &index);    /*  在访问数组之前对索引做检查  if (index < 0 || index >= g_count) {    napi_throw_error(env, "", G_WARN_IMAGE);    return nullptr;  }  */    const char *imageNameStr = GetImageName(index);    if (index < 0 || index >= g_count) {    napi_throw_error(env, "", G_WARN_IMAGE);    return nullptr;  }  napi_value imageName;  napi_create_string_utf8(env, imageNameStr, strlen(imageNameStr), &imageName);    return imageName;}
复制代码

示例代码

• 数组越界访问示例


🔗 官网开发者学堂视频:https://developer.huawei.com/consumer/cn/training/result?type2List=201783644516849879&orderBy=1&courseType=5

🔗 社区 DFX 专题文章: https://developer.huawei.com/consumer/cn/forum/subject/2101218731402391001

【扫码加入 HarmonyOS DFX 技术交流群】