Android 中 调试和减少内存错误
ASan
概述
官网连接: https://developer.android.com/ndk/guides/asan?hl=zh-cn
- ASan API 27开始
- HWASan(替换AScan)
- 从 NDK r21 和 Android 10(API 级别 29)开始
- 适用于 64 位 Arm 设备
- 性能比AScan更好
主要功能
- 堆栈和堆缓冲区上溢或下溢
- 释放之后的堆使用情况
- 超出范围的堆栈使用情况
- 重复释放或错误释放
示例应用
示例应用展示了如何为 hwasan 配置 build 变体。
模拟出几个内存错误的问题
1. 越界访问 (Buffer Overflow/Underflow)
#### 问题描述: 当程序试图访问数组或缓冲区以外的内存时,就会发生越界访问错误。这类问题往往导致数据损坏,崩溃或安全漏洞。 #### 具体表征:
- 程序崩溃:可能导致应用无预警地关闭。
- 数据损坏:错误地重写了其他变量的值。
- 安全漏洞:可能被利用执行恶意代码。
#### 示例代码:
int array[5] = {0};
int value = array[10]; // 越界读取
array[-1] = 3; // 越界写入
2. 使用后释放 (Use After Free)
#### 问题描述: 当程序释放了一块内存后仍然尝试使用它时,就会发生使用后释放错误。这是一种非常危险的安全漏洞。 #### 具体表征:
- 不可预知的行为:可能导致程序执行一段随机的内存操作。
- 程序崩溃:访问被释放内存可能导致程序无法继续执行。
- 内存损坏:可能不经意间修改了由其他部分的程序占用的内存。
#### 示例代码:
char *ptr = new char[20];
delete [] ptr; // 释放内存
strcpy(ptr, "这是错误的使用!"); // 在释放后使用内存
3. 内存泄露 (Memory Leak)
#### 问题描述: 未能释放不再使用的内存导致内存泄露。虽然它不会立即导致程序崩溃,但会随着时间推移而逐渐消耗系统资源。 #### 具体表征:
- 内存消耗增加:随着程序的运行,预期的内存使用量会不断增加。
- 性能降低:可用内存减少可能导致系统运行变慢,甚至出现延迟。
- 可能的程序崩溃:如果内存泄露严重,系统最终可能耗尽内存,导致应用或系统崩溃。
#### 示例代码:
void func()
{char *ptr = new char[10]; // 分配内存// 这里应该有一些处理// 忘记释放ptr分配的内存
}
这些内存错误问题都是开发过程中应当注意避免的。常规的调试方法和一些专用工具比如Valgrind、AddressSanitizer(ASan)和上文提到的HWAddressSanitizer(HWASan)能够帮助开发者检测并解决这些内存错误。
HWASan输出日志分析
模拟HWAddressSanitizer的日志输出实际会涉及到针对具体错误调用堆栈和内存映射的信息。由于HWASan的具体日志输出会根据实际运行时的环境和错误情境有所不同,下面我将模拟几个内存错误的HWASan日志输出及关键点说明:
1. 越界访问错误
=================================================================
==12345==ERROR: HWAddressSanitizer: tag-mismatch on address 0x00b100004008 at pc 0x000000552abc
WRITE of size 4 at 0x00b100004008 tags: 1c/15 (ptr/mem) in thread T0#0 0x552abc (/path/to/binary+0x552abc)#1 0x55678f (/path/to/binary+0x55678f)#2 0x7f7c85dd9c (/system/lib/hwaddress-sanitizer.so+0x5dd9c)
0x00b100004008 is located 0 bytes to the right of 8-byte region [0x00b100004000,0x00b100004008)
allocated by thread T0 here:#0 0x7f7c85ca58 (/system/lib/hwaddress-sanitizer.so+0x5ca58)#1 0x5556f4 (/path/to/binary+0x5556f4)#2 0x555979 (/path/to/binary+0x555979)
SUMMARY: HWAddressSanitizer: tag-mismatch /path/to/binary (0x552abc) WRITE 0x00b100004008
=================================================================
#### 关键点说明:
tag-mismatch
表示检测到内存标签不匹配,这通常提示内存访问错误。WRITE of size 4
说明试图写入4个字节的数据。address 0x00b100004008 at pc 0x000000552abc
显示了发生错误的内存地址和程序计数器的地址。- 调用堆栈(call stack)提供了错误发生时的函数调用序列。
0x00b100004008 is located 0 bytes to the right of 8-byte region
说明写操作是在8字节区域的右侧进行的,这是越界访问。
2使用后释放错误
================================================================= ==12345==ERROR: HWAddressSanitizer: use-after-free on address 0x00b100004010 at pc 0x000000552efc READ of size 8 at 0x00b100004010 tags: 1c/00 (ptr/mem) in thread T0 #0 0x552efc (/path/to/binary+0x552efc) #1 0x556abc (/path/to/binary+0x556abc) #2 0x7f7c85dd9c (/system/lib/hwaddress-sanitizer.so+0x5dd9c) 0x00b100004010 is located 0 bytes inside of 10-byte region [0x00b100004010,0x00b10000401a) freed by thread T0 here: #0 0x7f7c85caff0 (/system/lib/hwaddress-sanitizer.so+0xcaff0) #1 0x555999 (/path/to/binary+0x555999) #2 0x5.#2 0x7f7c85dd9c (/system/lib/hwaddress-sanitizer.so+0x5dd9c)
0x00b100004010 is located 0 bytes inside of 10-byte region [0x00b100004010,0x00b10000401a)
freed by thread T0 here:#0 0x7f7c85caff0 (/system/lib/hwaddress-sanitizer.so+0xcaff0)#1 0x555999 (/path/to/binary+0x555999)#2 0x556df9 (/path/to/binary+0x556df9)
previously allocated by thread T0 here:#0 0x7f7c85ca58 (/system/lib/hwaddress-sanitizer.so+0x5ca58)#1 0x5557e8 (/path/to/binary+0x5557e8)
SUMMARY: HWAddressSanitizer: use-after-free (/path/to/binary+0x552efc) READ 0x00b100004010
=================================================================
#### 关键点说明:
use-after-free
表示在释放后再次使用了内存,这是一种严重的错误。READ of size 8
说明尝试读取8个字节的数据。address 0x00b100004010 at pc 0x000000552efc
显示了被错误读取的内存地址和相关的程序计数器的地址。- 调用堆栈(call stack)提供了释放内存和随后错误使用该内存的函数调用序列。
freed by thread T0 here:
和previously allocated by thread T0 here:
显示了内存分配及后续释放的位置。
在分析这样的日志时,开发者需要关注堆栈跟踪来确定错误发生的上下文,并修复代码中相应的问题。日志中的内存地址、线程信息以及函数调用序列都是确定问题所在和解决问题的关键信息。在实际的开发工作中,可通过这些详尽的日志来定位问题,优化代码,并进一步强化软件的稳健性和安全性。
Arm 内存标记扩展 (MTE)
- 支持ARMv8.5-A及以上架构的处理器
- 从 Android 13 开始,部分设备支持 MTE
- adb shell grep mte /proc/cpuinfo 出现
Features : [...] mte
则表示设备在运行时启用了 MTE
MTE vs ASan
MTE是 ARM 架构提供的硬件特性,它通过在物理内存中添加一些标记信息来辅助检测内存安全错误,它的优势在于提供硬件层面的检测,开销相较软件层面的检测方式较小,特别是在ASYNC模式下。HWASan 是一种基于软件的解决方案,通过修改编译器和运行时环境来检测内存安全问题,特别是地址错误和内存泄漏等。它是谷歌为ARM64架构设计的,针对Android操作系统进行了优化。HWASan在测试和分析阶段特别有用,因为它能提供详尽的错误报告,包括堆栈追踪和内存访问历史等。
如在支持MTE的ARMv8.5及以上架构的设备上,可以优先选用MTE
SYNC 和ASYNC异同
维度 | SYNC(同步模式) | ASYNC(异步模式) |
---|---|---|
优化目标 | 针对可调试性优化,适用于精确的 bug 检测工具 | 针对 bug 报告的性能优化,偏向于低开销的内存安全检测 |
处理器反应 | 在接收到违规的加载或存储指令时,会立即终止进程 | 处理器会在到达最近的内核入口(如系统调用或计时器中断)时终止进程 |
错误报告 | 返回SIGSEGV,提供内存访问和故障地址的详细信息 | 返回SIGSEGV,但是不记录错误地址或内存访问 |
配合Android分配器 | 分配器会记录每次分配和取消分配的堆栈轨迹以提供更好的错误报告 | 不进行此操作 |
应用场景 | 用作测试阶段的HWASan的更快替代方案,或生产环境中的应用出现漏洞时的安全缓解措施。 | 对经过严格测试的代码库(已知其内存安全 bug 的密度较低)降低内存安全漏洞的生产环境 |
两种模式的相同点包括:
在进行硬件辅助的内存安全错误检测时,SYNC和ASYNC都可以监测到内存安全错误。一旦发现标记不匹配,两种模式均会触发处理器的响应,终止进程并返回SIGSEGV。在这两种模式下,都可以在测试阶段运用来找出内存安全bug。
给自己分配的内存打TAG
scudo 实现代码
setRandomTag
函数:该函数是用来为给定的指针Ptr
设置一个随机的内存标签。untagPointer
函数:这个函数会移除指针Ptr
的内存标签loadTag
函数:此函数似乎用于返回指针Ptr
上的内存标签- addFixedTag 函数:这个函数用于给一个指针 Ptr 添加一个固定的标签 Tag
allocatorSupportsMemoryTagging
模板函数:此函数用于确定是否支持内存标记。
使用场景
- 内存错误检测:在分配和释放内存时为其打上Tag,可以帮助检测内存安全错误,如缓冲区溢出、使用后释放等。当一个内存区域被释放后,您可以为其分配一个新的Tag,如果之后系统尝试使用相同的旧Tag访问这块内存,MTE机制将检测到错误,并产生一个异常。
- 运行时监控和诊断:打标签的内存使得开发者可以追踪内存访问模式,识别内存是如何被程序的不同部分访问的。这在调试过程中尤为有用,因为它可以帮助发现那些隐蔽的内存错误。
- 保护关键数据:通过给敏感数据打个特定的Tag,你可以确保只有被授权的代码能够访问这些数据。如果其他不相关的代码尝试访问,标签不匹配会触发异常,这为敏感数据提供了一层额外的保护。
- 内存使用分析:Tag可以作为分析工具,以了解某类内存分配的分布和生命周期。例如,通过给特定类型的对象或资源分配统一的Tag,可以在运行时分析其分布情况。
- 内存泄露排查:如果一个内存块长时间没有释放,并且带有与之关联的Tag,那么这可能是一个内存泄露的迹象。分析这些留存的Tag可以帮助追踪潜在的内存泄露点。
进阶
Arm 撰写的 Android OS MTE 用户指南
GWP-ASan
GWP-ASan(GWP-ASan Will Provide Allocation SANity) 是一种原生内存分配器功能,可帮助查找释放后使用和堆缓冲区溢出 bug。
- GWP-ASan 不需要源代码或重新编译
- 适用于以 Android 11(API 级别 30)以上
概述
- GWP-ASan启用情况:在进程启动以及zygote派生时,系统会随机选择一些应用和平台可执行文件启用GWP-ASan。
- 作用:GWP-ASan旨在帮助开发者发现与内存相关的错误,并帮助应用准备好对ARM内存标记扩展(MTE)的支持。
- 内存分配拦截:一旦启用,GWP-ASan会随机拦截堆分配的子集,并将它们移入特殊区域,以发现通常难以检测的堆内存损坏错误。
- 低采样率的效果:即使是低采样率,只要用户基数足够大,也能够发现常规测试中未能发现的堆内存安全错误。
- 错误检测示例:GWP-ASan已在Chrome中发现大量错误。
- 信息收集:GWP-ASan为它拦截的所有分配收集额外信息,这些信息有助于内存安全违规的调试,且会被自动加入到原生代码的崩溃报告中。
- 性能影响:启用GWP-ASan后,会产生较小的CPU开销。
- 资源消耗:GWP-ASan会带来一定的固定RAM开销,目前累计每个受影响进程大约70KiB。
获取检测到“释放后堆使用”或“堆缓冲区溢出” bug
ActivityManager#getHistoricalProcessExitReasons
json格式
[{"processName": "com.example.app","pid": 12345,"reason": "CRASH","timestamp": 1617998745000,"description": "NullPointer exception in MainActivity","trace": "java.lang.NullPointerException: Attempt to invoke virtual method on a null object reference\n\tat com.example.MainActivity.onCreate(MainActivity.java:85)\n\t...","importance": "FOREGROUND_SERVICE","status": "SIGNAL 9"},{"processName": "com.example.serviceapp","pid": 12346,"reason": "USER_REQUESTED","timestamp": 1617998746000,"description": "User requested force stop","trace": "","importance": "VISIBLE","status": "SIGNAL 9"},