使用GWP-ASan定位地址越界问题

0 评论 129 浏览 0 收藏 20 分钟

地址越界泛指程序对内存的错误访问,例如堆内存释放后重用、栈内存退栈后重用、堆或栈的越界访问等。 GWP-ASan 正是用于解决这类问题的轻量级地址越界检测工具。

本原创文章帖发布在华为开发者联盟社区,欢迎开发者前往访问评论交流,更多与该内容相关讨论,请点击原帖查看:

使用GWP-ASan定位地址越界问题-华为开发者话题 | 华为开发者联盟

一、地址越界的定义与危害

       地址越界泛指程序对内存的错误访问,例如堆内存释放后重用、栈内存退栈后重用、堆或栈的越界访问等。这类问题本质上是程序访问了“不该访问”或“已经失效”的内存区域,属于典型的内存安全问题。地址越界问题的危害较大,大部分情况下,它会直接表现为应用闪退;极个别情况下,也可能表现为功能异常、数据异常、界面显示异常等功能性问题。相比闪退类问题,功能性问题通常更容易被隐藏和忽略,导致问题长时间潜伏在线上。

       同时,地址越界问题具有很强的随机性。普通 Crash 日志或 Coredump 捕获到的往往不是内存被破坏的第一现场,而是内存被破坏后,在后续访问、释放或系统检测时暴露出来的二次现场。因此,日志中看到的崩溃点不一定是真正的问题根因,难以还原内存从申请、释放到异常访问的完整过程,定位成本较高。

       GWP-ASan 正是用于解决这类问题的轻量级地址越界检测工具。它通过对堆内存分配进行采样,在较低性能开销下检测释放后使用、堆越界访问、重复释放等典型内存错误触发的第一现场。相较于 HWASan/ASan,GWP-ASan 无需额外插桩适配,性能开销低于 5%,更适合在大规模现网用户场景下运行,用于发现难复现的地址越界类问题。目前GWP-ASan能检测的错误类型有:

故障类型 含义
Use After Free 释放后使用
Heap Buffer Overflow / Underflow 堆上/下越界访问
Double Free 重复释放
Invalid Left / Right Free 指针非法释放

       详细检测原理可参考官网文档:GWP-ASan检测原理  

二、GWP-ASan使用指南

2.1 开启GWP-ASan检测

       方式一:默认参数开启

       在app.json5中添加”GWPAsanEnabled”: true配置,如下图所示:

       开启GWP-ASan检测后,如果应用发生地址越界问题,且该内存块正好被GWP-ASan采样监控,GWP-ASan会记录地址越界事件。 

       方式二:三方灰度,支持开发者定制化开启GWP-ASan

       从API20(6.0版本)开始,开发者可通过如下参数去定制化开启GWP-ASan。从API24(6.1版本)开始,提供了可恢复模式,在该模式下,系统检测到地址越界故障后,避免因检测机制本身导致进程崩溃。

名称

默认值

是否必填

说明

API

ROM

alwaysEnabled

false

true:100%开启GWP-ASan,与app.json5中GWPAsanEnabled标签功能一致。

false:1/128概率开启GWP-ASan,在应用冷启动时候会判断是否开启。

注意:若在app.json5中设置了 GWPAsanEnabled,将会覆盖该参数。

20

6.0

sampleRate

2500

GWP-ASan采样频率。1/sampleRate的概率对分配的内存进行采样。

建议值:≥1000,默认参数下性能开销小于5%。采样频率过小会显著影响性能,若调整参数请开发者自行保证用户体验。

20

6.0

maxSimutaneousAllocations

1000

最大分配的插槽数。当插槽用尽时,新分配的内存将不再受监控。释放已使用的内存后,其占用的插槽将自动复用,以便于后续内存的监控。

建议值:≤20000,每个插槽会额外占用约4.5KB内存,默认参数下约占4.5MB,过大可能导致VMA超限崩溃。

20

6.0

duration

7

开启GWP-ASan检测天数,默认值为7天。 20 6.0

isRecover(api24 6.1版本)

false

该参数从 API 24 开始支持。用于控制应用在100% 开启 GWP-ASan 时,是否以可恢复模式运行。

true:当 GWP-ASan 以 100% 概率开启时,应用以可恢复模式运行。在该模式下,系统检测到地址越界故障后,避免因检测机制本身导致进程崩溃;但对于已造成非法内存访问的错误,应用仍可能发生崩溃。

false:当 GWP-ASan 以 100% 概率开启时,应用以不可恢复模式运行。

注意:该参数只在“100% 开启 GWP-ASan”场景下生效;1/128 概率开启场景默认为可恢复,不受isRecover控制。

默认值:false。

24

6.1

       接口具体使用方式,可查看@ohos.hidebug (Debug调试) 

三、使用场景

3.1 开发态

       开发调试阶段以问题发现和复现效率优先。建议将应用配置为100%开启GWP-ASan检测,以稳定暴露潜在内存问题。

       示例:

import { hidebug } from '@kit.PerformanceAnalysisKit';
import { taskpool } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';

@Concurrent
function enableGwpAsanTask(): void {
  let options: hidebug.GwpAsanOptions = {
    alwaysEnabledtrue,
    sampleRate2500,
    maxSimutaneousAllocations5000,
    isRecoverfalse// 不可恢复模式
  };
  hidebug.enableGwpAsanGrayscale(options, duration);
}

taskpool.execute(enableGwpAsanTask).then(() => {
  console.info(`Succeeded in enabling GWP-ASan.`);
}).catch((error: BusinessError) => {
  const errBusinessError = error as BusinessError;
  console.error(`Failed to enable GWP-ASan. Code: ${err.code}, message: ${err.message}`);
})

       该场景下:每次应用启动均会使能GWP-Asan检测,其中采样率为1/2500,槽位数为5000,检测到地址越界问题后应用会崩溃,不会继续运行。若默认参数下问题仍较难复现,可适当减小 sampleRate,或增大maxSimutaneousAllocations,以提升检测覆盖率。

3.2 运维态

       当应用发布后出现疑似地址越界问题,且默认 1/128 概率开启难以命中时,开发者可通过 hidebug 接口并结合 duration 参数,临时将应用切换为 100% 开启模式,用于线上问题复现和定位。

       示例:

import { hidebug } from '@kit.PerformanceAnalysisKit';
import { taskpool } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';

@Concurrent
function enableGwpAsanTask(): void {
  let options: hidebug.GwpAsanOptions = {
    alwaysEnabledtrue,
    sampleRate2500,
    maxSimutaneousAllocations1000,
    isRecovertrue// 可恢复模式
  };
  let durationnumber = 2// GWP-ASan 检测天数
  hidebug.enableGwpAsanGrayscale(options, duration);
}

taskpool.execute(enableGwpAsanTask).then(() => {
  console.info(`Succeeded in enabling GWP-ASan.`);
}).catch((error: BusinessError) => {
  const errBusinessError = error as BusinessError;
  console.error(`Failed to enable GWP-ASan. Code: ${err.code}, message: ${err.message}`);
})

       该场景下:在指定时长内,应用每次启动都会使能GWP-ASan检测,其中采样率为1/2500,槽位数为1000,检测到地址越界问题后不会崩溃,继续运行。同样地,如果问题难以复现,也可以适当的调整sampleRate和maxSimutaneousAllocations。

四、GWP-ASan日志

4.1 日志获取

4.1.1 开发态

       方式一:通过DevEco Studio获取日志

       DevEco Studio会收集设备/data/log/faultlog/faultlogger/路径下的进程崩溃故障日志到FaultLog下,根据进程名和故障和时间分类显示。获取日志的方法参见:DevEco Studio使用指南-FaultLog 

       方式二:通过hdc获取日志,需打开开发者选项

       日志默认都落盘至 /data/log/faultlog/faultlogger 下。在开发者选项打开的情况下,开发者可以通过hdc file recv /data/log/faultlog/faultlogger D:\命令导出故障日志到本地。故障日志文件名格式为gwpasan-com.example.sampleapplication-20020209-20260416234001285.log,其中 com.example.sampleapplication 表示应用包名,20020209 表示应用 UID,20260416234001285 表示故障发生时间。

4.1.2 运维态

       方式:通过HiAppEvent接口订阅

       HiAppEvent给开发者提供了故障订阅接口,详见HiAppEvent介绍 。参考订阅地址越界事件(ArkTS) 订阅地址越界事件(C/C++) 完成地址越界事件订阅,并通过事件的external_log 字段读取故障日志文件内容。

4.2 日志规格

       GWP-ASan的日志格式如下,会展示越界类型(Use After Free、Double Free、Overflow等)。以下示例为典型的Use-After-Free问题日志,包含内存块的分配、释放及违规访问的调用栈信息。

五、实战案例

5.1 原始日志

*** GWP-ASan detected a memory error ***
Use After Free at 0x5b41761010 (16 bytes into a 40-byte allocation at 0x5b41761000) by thread 18502 here:
 #0 0x5b13f1a6d0  (/system/lib64/libwm.z.so+0x1da6d0) (BuildId: xxx)
 #1 0x5b13f18d5c  (/system/lib64/libwm.z.so+0x1d8d5c) (BuildId: xxx)
 #2 0x5b13f1a24c  (/system/lib64/libwm.z.so+0x1da24c) (BuildId: xxx)
......
0x5b41761010 was deallocated by thread 18502 here:
 #0 0x5a859859d4  (/lib/ld-musl-aarch64.so.1+0x1579d4) (BuildId: xxx)
 #1 0x5a859853ac  (/lib/ld-musl-aarch64.so.1+0x1573ac) (BuildId: xxx)

 #2 0x5b13f1a6cc  (/system/lib64/libwm.z.so+0x1da6cc) (BuildId: xxx)
 #3 0x5b13f18d5c  (/system/lib64/libwm.z.so+0x1d8d5c) (BuildId: xxx)
......
0x5b41761010 was allocated by thread 18502 here:
 #0 0x5a859859d4  (/lib/ld-musl-aarch64.so.1+0x1579d4) (BuildId: xxx)
 #1 0x5a85985110  (/lib/ld-musl-aarch64.so.1+0x157110) (BuildId: xxx)
 #2 0x5a859a773c  (/lib/ld-musl-aarch64.so.1+0x17973c) (BuildId: xxx)
 #3 0x5a86f731a4  (/system/lib64/libc++.so+0xb31a4) (BuildId: xxx)
 #4 0x5b13f1a724  (/system/lib64/libwm.z.so+0x1da724) (BuildId: xxx)
 #5 0x5b13f18d5c  (/system/lib64/libwm.z.so+0x1d8d5c) (BuildId: xxx)
......

5.2 问题分析

       拿到一份 GWP-ASan 日志后,首先看日志中的故障类型和关键调用栈。从日志首行可以看出,该问题是 Use After Free,即释放后使用,异常访问地址为 0x5b41761010。继续看释放栈可以发现,异常访问和释放动作都发生在线程 18502 中,且报错栈和释放栈都指向 libwm.z.so 的相近位置:

   • 报错栈:#0 libwm.z.so+0x1da6d0

   • 释放栈:#2 libwm.z.so+0x1da6cc

       重点分析这两个偏移对应的代码逻辑。通过 addr2line -Cfpie libwm.z.so 0x1da6d0 0x1da6cc 定位源码行,关键代码如下:

       1、对报错栈 libwm.z.so+0x1da6d0 进行分析后,发现这里对应一处三元运算符逻辑。代码会根据迭代器 it 是否指向 ownPropList.end() 来决定 insertPair 的值。

       2、对释放栈 libwm.z.so+0x1da6cc 进行分析后,发现这里做了容器erase的操作。

       问题此时已经比较明确:erase(it) 会删除 it 指向的元素,删除后原来的 it 会失效,不能再继续使用。但当前代码在 erase(it) 之后,又继续使用 it 参与三元判断,并在特定分支下解引用 *it,从而触发 Use After Free。

5.3 修复建议

       需要避免在 erase(it) 后继续使用原来的迭代器 it。如果后续还需要使用该元素内容,应在 erase 前先保存;如果需要继续遍历,则使用 erase 返回的新迭代器。

六、聚类规则

       应用程序在不同版本,或同一版本的不同时间,可能因为同一根因产生多份 GWP-ASan 故障日志。但日志中的部分信息会随着版本、时间、地址随机化等因素发生变化,导致开发者难以快速判断这些日志是否属于同一类问题。同时,GWP-ASan 日志中通常同时包含系统侧和应用侧调用栈。如果不做过滤和归一化处理,容易受到系统库栈帧、BuildID 等信息干扰,不利于开发者快速聚焦应用侧问题。因此,为避免重复分析多份故障信息,提高应用故障问题的分析效率,可以根据故障类型和业务相关调用栈对日志进行聚类。判断多份日志是否属于同一问题,主要基于以下两个维度:

       1、地址越界的故障类型。

       2、业务相关调用栈,过滤基础库后的栈顶前两帧。

       通过上述信息,可以对问题进行初步定界。

6.1 步骤一:提取故障类型

       在GWP-ASAN日志中,故障类型根据原始日志中包含”at”的行提取。

6.2 步骤二:标准化栈信息

       提取故障类型后,需要对日志中的调用栈进行筛选、清洗和标准化,避免易变信息影响聚类结果。主要规则如下:

   1. 去除易变信息:行号、相同的BuildID和绝对地址。

   2. 过滤系统栈帧:系统库栈帧通常以“/system/lib”或“/system/lib64”为起始字符。

   3. 保留业务相关栈帧:保留业务so路径作为关键特征。

       例如:

原始栈帧内容 标准化后栈帧内容
#0 0x5b181acd48  (/lib/ld-musl-aarch64.so.1+0x169d48) (BuildId: 2e2fbc21511b76f80bdcc5ad5bcc0e79) 忽略(基础库)
#1 0x661be0e870 (/data/storage/el1/bundle/libs/arm64/libentry.so+0x4e870) (BuildId: 5bc0684c2c3fc90841c2498efe1af4fd4792e5a8) /data/storage/el1/bundle/libs/arm64/libentry.so+0x4e870

6.3 步骤三:提取聚类特征与聚类

       在完成标准化后,根据不同故障类型提取关键栈作为特征:

故障类型

关键栈帧

提取规则

Use After Free

释放栈

过滤基础库后,取栈顶的前两帧(so名+相对偏移)

 

 

 

 

Buffer Overflow

报错栈

Double Free

 

第一次释放栈

第二次释放栈

Invalid Free

报错栈

       最终聚类特征为:故障类型+关键栈帧

本文由 @华为开发者联盟 授权发布于人人都是产品经理。未经作者许可,禁止转载

题图来自Unsplash,基于CC0协议

该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务

更多精彩内容,请关注人人都是产品经理微信公众号或下载App
评论
评论请登录
  1. 目前还没评论,等你发挥!