Android ASan 内存错误检测
一、功能背景
Native开发时,无法保证内存的安全性,开发时的不规范写法或无心之过可能导致内存相关的崩溃问题,但崩溃堆栈往往又不是第一案发现场,线上采集的minidump信息也十分有限,这使得Native代码中的内存错误很难排查。 为了解决这些影响程序稳定性的内存问题,Bugly提供线上ASan内存错误检测能力,支持检测在堆内存中分配、释放和使用时导致的基本错误。
Bugly提供的内存错误检测工具原理基于GWP-ASan,是一种基于采样内存分配的轻量级内存错误检测工具,性能开销小且不需要指令插桩和重新编译,适用于线上场景。
Google官方提供GWP-ASan原生内存分配器功能,用于调试和减少内存错误,支持在tombstone中提供ASan内存错误崩溃信息,仅支持Android 11(API 级别30)以上的系统,且不支持采样及参数控制。 详情可参考 Google Android GWP-ASan。
在使用前,需要先简单了解Bugly ASan内存错误检测工具的原理。
通过初始化接口和配置开启功能后,Bugly ASan会随机拦截堆内存分配操作,并将从指定的ASan Memory Pool中返回可用的内存地址给分配操作。ASan Memory Pool中的内存通常以页为单位连续排列,主要由两种类型的页交替间隔组成。红色 部分的内存页Guard Page为不可读写的页,绿色 部分的内存页Slot Page为可读写的页,用于内存的分配,如下图所示。
在内存分配事件采样命中时,会从指定的ASan Memory Pool中寻找可用的Slot Page进行内存分配,实际分配空间通常都小于一页,因此分配的内存会随机进行左右对齐。当分配内存左对齐时,可用于检测 Underflow 内存问题,访问Underflow地址时是不可访问区域,会触发SIGSEGV错误。同理,当分配内存右对齐时,可用于检测 Overflow 内存问题。
当对ASan分配内存进行释放后,会将所在Slot Page区域一段时间内标记为不可读写(标记时间取决于空余可读写Slot Page数量,一定时间后空余Slot Page数量不多则会重新将不可读写区域标记回可读写),如果再对相同的区域进行内存释放操作时,会触发 Double free 问题。如果对已经释放内存的区域进行写操作时,会触发 Use after free 问题。
由于内存的分配和对齐方式都是随机采样的,因此需要足够多的测试机器来检测对应的问题。
二、功能开启
Bugly ASan能力需要使用 4.4.3.x
及以上版本SDK(版本灰度中,详询Bugly小助手),接入方式与Bugly其他性能模块无差异,可以采用SDK端和portal端共同控制的方式。在接口初始化后,并且配置了 sample_ratio
采样率命中了当前设备,Bugly ASan能力才会开启,仅初始化接口或仅修改采样配置,不会开启功能。
1. 初始化接口
开启Bugly ASan能力需在接口中开启对应功能项。
// 开启Bugly ASan内存错误检测能力
RMonitor.startMonitors(MonitorName.ASAN);
通过接口初始化后,需参考配置指南,修改功能的采样率 sample_ratio
来配置功能开启。
2. 开启成功日志
当前设备被采样命中后,可以看到以下初始化日志。
06-11 21:03:29.182 12351 12397 I Bugly_Asan_Monitor: prepare to start addressSanitizer, local sample success!
06-11 21:03:30.162 12351 12397 D Bugly_Asan_Monitor: start addressSanitizer success!
由于ASan功能会对系统内存分配操作进行hook,与 FD触顶 监控和 Native内存触顶 监控不建议同时开启,因此当同时开启这几项功能时,会在本地进行二次的开启采样,如未采样到ASan功能时,上面的日志会打印开启失败。如需保证ASan的开启率,建议关闭或降低 FD触顶 监控和 Native内存触顶 监控的采样率。
三、配置指南
在portal设置 -> SDK配置中,开启ASan内存检测能力。其中对应的配置项说明如下。
监控项 | 配置项 | 配置参数 | 取值说明 |
---|---|---|---|
ASan内存错误检测 | "asan" | "sample_ratio" | 设备采样率,取值[0,1],1表示全部打开,0表示全部关闭,其他表示按指定概率抽样打开 |
ASan内存错误检测 | "asan" | "slot_size" | 可分配内存Slot Page大小,单位KB,越大所需内存开销越大,建议使用默认值 |
ASan内存错误检测 | "asan" | "slot_count" | 可分配内存Slot Page数量,数量越多会有更多的内存用于内存错误检测,内存开销越大,建议使用默认值 |
ASan内存错误检测 | "asan" | "max_sample_gap_count" | 循环采样随机数的最大值,值越大内存分配时采样命中的概率越低 |
ASan内存错误检测 | "asan" | "left_side_align_percentage" | 在ASan Memory Pool中内存分配时左对齐的概率,取值[0,100],值越小越易于检测Overflow问题,反之越容易检测Underflow问题 |
ASan内存错误检测 | "asan" | "right_side_perfect_align" | 在ASan Memory Pool中内存分配右对齐时是否不按页对齐,若不按页对齐更容易检测出Overflow问题,但也可能带来未知的crash风险 |
ASan内存错误检测 | "asan" | "ignore_overlapped_reading" | 是否忽略重读类型的ASan错误 |
ASan内存错误检测 | "asan" | "target_so_patterns" | 需要检测的SO文件,若为空默认检测全部SO,支持正则表达式设置,当内存问题能定位具体SO时,建议设置该值,可以减少稳定性问题 |
ASan内存错误检测 | "asan" | "ignore_so_patterns" | 需要忽略的SO文件,若不为空默认不忽略任何SO,支持正则表达式设置 |
四、功能使用
1. ASan 错误异常触发
在开启Bugly ASan内存错误检测能力后,当堆内存错误发生且被检测到时,会 触发SIGSEGV异常,直接Crash退出程序,因此内存错误的表现就是一个Native Crash异常。
如堆内存错误错误发生,但未开启Bugly ASan内存错误检测,或未被检测到时,可能并不会触发SIGSEGV异常,但可能后续会导致其他不可预估的异常,且难以定位解析。
ASan错误类型支持在portal上展示及筛选,可以通过筛选 ASan类型 快速定位出ASan模块触发的异常问题。
在触发的Native Crash异常中,Crash Error Message信息和个例详情中会指明异常来自ASan内存错误检测,并标识出错误的类型,展示效果如下。
Crash异常的堆栈即为发生内存错误的第一现场。对于不同类型的问题,栈顶操作分别表示以下含义:
- Overflow: 栈顶表示首次访问内存上溢地址时执行的操作。
- Underflow: 栈顶表示首次访问内存下溢地址时执行的操作。
- Use after free: 栈顶表示首次访问被释放内存地址时执行的操作。
- Double free: 栈顶表示首次重复释放已释放内存地址时执行的操作。
2. ASan 附件信息
在ASan内存错误触发的SIGSEGV异常中,会携带错误内存的分配及释放信息,可以在个例详情中 附件
-> asan_info.zip
中找到对应的附件,如下图所示。
- ASan附件信息在进程二次启动时进行上报,因此当没有看到对应附件时,可能需要重启应用触发上报。
- 附件上报存在5min左右的处理延迟,重启后,需等待5min左右再才能看到
asan_info.zip
附件。
ASan附件主要包含以下信息,错误类型、触发的地址、触发的进程及线程、错误内存的释放线程及堆栈(如未释放则没有释放堆栈)、错误内存的分配线程及堆栈等。
释放及分配堆栈支持附件直接翻译,点击附件后即可查看翻译后的堆栈(需上传对应SO的符号表),翻译后堆栈展示如下。
五、注意事项
开启Bugly ASan内存检测功能需关注以下事项。
- 开启ASan内存错误检测后,当触发错误被检测到会直接触发Crash异常,不管是否对当前App运行已经造成影响。
- 如在标记为ASan内存错误的异常问题中出现bugly相关堆栈,如
TriggerIssue
等关键字时,并不是bugly导致异常,而是由bugly主动进行异常触发上报内存错误问题。 - ASan内存错误检测功能会对内存分配操作进行hook拦截,如命中采样会直接从ASan内存池中返回可用内存地址,并不会回调原有内存分配操作。因此,如业务有自行对内存分配操作执行hook操作时,需评估hook冲突问题,或通过配置避免在有业务hook内存分配的SO中开启ASan内存错误检测。
- 开启ASan内存错误检测能力,会通过PLT hook方式修改对应SO中的内存分配实现,不同业务之间环境复杂,建议优先测试环境使用或线上小规模灰度使用。同时,也建议在配置中指定
target_so_patterns
,减少对SO的hook操作,提高稳定性。
附、内存错误简单示例
Overflow:
ptr = (char*) malloc(16);
ptr[20] = '\0';
Underflow:
ptr = (char*) malloc(16);
ptr[-5] = '\0';
Use after free:
ptr = (char*) malloc(16);
snprintf(ptr, 16, "test");
free(ptr);
snprintf(ptr, 16, "test");
Double free:
ptr = (char*) malloc(16);
snprintf(ptr, 16, "test");
free(ptr);
free(ptr);