使用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 = {
alwaysEnabled: true,
sampleRate: 2500,
maxSimutaneousAllocations: 5000,
isRecover: false, // 不可恢复模式
};
hidebug.enableGwpAsanGrayscale(options, duration);
}
taskpool.execute(enableGwpAsanTask).then(() => {
console.info(`Succeeded in enabling GWP-ASan.`);
}).catch((error: BusinessError) => {
const err: BusinessError = 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 = {
alwaysEnabled: true,
sampleRate: 2500,
maxSimutaneousAllocations: 1000,
isRecover: true, // 可恢复模式
};
let duration: number = 2; // GWP-ASan 检测天数
hidebug.enableGwpAsanGrayscale(options, duration);
}
taskpool.execute(enableGwpAsanTask).then(() => {
console.info(`Succeeded in enabling GWP-ASan.`);
}).catch((error: BusinessError) => {
const err: BusinessError = 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协议
该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务
- 目前还没评论,等你发挥!

起点课堂会员权益




