找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2328

积分

1

好友

321

主题
发表于 2025-12-25 10:41:07 | 查看: 30| 回复: 0

在开发过程中,我们有时会遇到一个现象:某些在Linux平台上运行正常的程序,移植到Android平台后却意外崩溃。分析这些崩溃日志后发现,问题往往出在程序自身,例如内存访问越界。这并非Android系统“吹毛求疵”,恰恰相反,是Linux内核在一定程度上“纵容”了这些潜在的Bug。

下面通过一个简单的C++示例来直观感受这个问题。程序中存在一个明显的堆内存越界写入操作:

#include <stdio.h>
#include <stdlib.h>

int main() {
    char *p = new char[16];
    printf(“p:%p\n”, p);
    // p合法的下标范围是[0,15],p[16]访问越界了
    p[16] = ‘a’;
    return 0;
}

注:本文示例基于常见的Arm64架构移动平台。

在标准的Linux系统上运行上述程序,通常不会有任何异常提示,程序“安静”地执行完毕:

p:0x558dae2830

然而,在Android 14系统上运行相同的程序,则会立即崩溃,并输出详细的诊断信息。日志明确指出程序尝试向地址 0x00458e3d2790(即 p[16])进行非法的单字节写入,精准定位了问题根源:

p:0x650000458e3d2780
==26586==ERROR: HWAddressSanitizer: tag-mismatch on address 0x00458e3d2790 at pc 0x00648e3f0120
WRITE of size 1 at 0x00458e3d2790 tags: 65/16 (ptr/mem) in thread T0
// ... 省略其他堆栈dump信息

Android是如何实现这种精准检测的呢?注意崩溃信息中的两个关键术语:HWAddressSanitizertag-mismatch。正是HWASan检测到了这次非法的内存访问并主动终止了进程。

1. HWASan是什么?

HWASan(Hardware-assisted AddressSanitizer)是一种基于硬件辅助的内存错误检测工具。它利用了ARM架构的内存标记技术,相比传统的软件模拟实现的AddressSanitizer,能显著降低运行时内存开销。

HWASan能够检测多种常见的内存错误,包括:

  • 堆/栈缓冲区溢出或下溢
  • 释放后使用
  • 作用域外栈内存访问
  • 重复释放或野指针释放

在性能开销方面,HWASan相比传统ASan有所改善:

  • CPU开销:与传统ASan类似,约为正常程序的2倍。
  • 二进制大小:与传统ASan类似,比正常程序增加40% – 50%。
  • 内存消耗显著降低,相比传统ASan减少了10% – 35%。

不过,HWASan对运行环境有特定要求:

  • 硬件要求:需要AArch64(arm64)架构支持。该架构提供了Top-byte Ignore技术,允许在内存寻址时忽略指针的最高字节。HWASan正是利用这个空闲字节来存储内存块的元数据标签。
  • 软件要求
    • 内核需支持TBI,以在寻址时忽略指针最高字节。
    • 需要编译器支持插桩(目前主要由Clang实现),在内存分配时打标签,在内存访问时检查标签匹配性。

Android 11开始,开发者可以启用HWASan进行应用测试,具体开启方法可参考官方文档。

2. HWASan工作原理

HWASan的核心技术基础是内存标记。其基本流程如下:

  1. 标记内存:系统以16字节为粒度,为每一块内存分配一个随机的1字节标签。
  2. 标记指针:当程序申请内存时,内存管理模块会分配内存和对应的标签,并将该标签值存入返回指针的最高字节。
  3. 检查匹配:每次通过指针访问内存时,硬件或插桩代码会检查指针中携带的标签与目标内存块的实际标签是否一致。若不匹配,则判定为非法访问并立即终止程序。

下面结合两个具体场景深入分析。

2.1 检测堆缓冲区溢出

下图展示了一个堆内存越界访问的例子:
Android HWASan内存错误检测原理与实战:提升应用稳定性的利器 - 图片 - 1

  • 程序申请了20字节,HWASan按16字节对齐实际分配了32字节(两个内存块)。
  • 返回的指针 p 值为 0x650000458e3d2780,其最高字节 0x65 即为标签。
  • 当执行 p[32] = ‘a’ 时,程序试图访问第二个内存块(标签为 0x23)。
  • 此时,指针标签 (0x65) 与目标内存标签 (0x23) 不匹配,HWASan触发崩溃。
  • 思考:如果访问的是 p[25],会导致崩溃吗?答案是不会,因为p[25]仍然位于第一个标签为0x65的16字节块内。

2.2 检测释放后使用

下图展示了HWASan如何检测“释放后使用”错误:
Android HWASan内存错误检测原理与实战:提升应用稳定性的利器 - 图片 - 2

  • 内存被释放后,其标签会被立即更新为一个新的随机值。
  • 如果后续程序仍通过持有旧标签的指针来访问这块已释放的内存,标签比对就会失败,从而被HWASan捕获。

HWASan同样可以检测栈上的非法访问(如栈溢出、访问已失效的局部变量),其原理与堆内存检测一致,都是基于内存标签的匹配机制。

2.3 技术局限性与漏检

由于标签只有1字节(256种可能值),当一个进程存活的内存块数量超过256时,必然会出现标签重复的情况。如果非法访问恰好跳转到了另一个标签相同的内存块,HWASan将无法检测出来,这种漏检的概率约为0.4%(1/256)。

在实际工程中,大多数非法内存访问都发生在相邻区域。因此,HWASan的标签随机化算法会确保相邻内存块的标签不同。但如果你“技艺高超”,编写出能“翻山越岭”精确踩中远处同标签内存的Bug,则有可能逃过检测。以下示例程序演示了这种极端情况:

#include <stdio.h>
#define TAG_MASK ((long)0xff << 56)
#define GET_TAG(pointer) (((long)pointer & TAG_MASK) >> 56)
#define CLEAR_TAG(pointer) ((long)pointer & ~TAG_MASK)
#define MALLOC_SIZE (32)

int main() {
    char* p1 = new char[MALLOC_SIZE];
    char p1Tag = GET_TAG(p1);
    printf(“p1:%p p1Tag:0x%x\n”, p1, p1Tag);
    for (;;) {
        char* p2 = new char[MALLOC_SIZE];
        char p2Tag = GET_TAG(p2);
        if (p2Tag != p1Tag) {
            // 如果新分配内存p2的tag和p1的不相等,就故意泄露,消耗TAG
            continue;
        }
        long distance = CLEAR_TAG(p2) - CLEAR_TAG(p1);
        if (distance > 0) {
            printf(“p2:%p p2Tag:0x%x\n”, p2, p2Tag);
            printf(“distance between p1 and p2 is %ld\n”, distance);
            printf(“write one byte to %p, which is between p2‘s range [%p, %p)\n”, p1 + distance,
                   p2, p2 + MALLOC_SIZE);
            p1[distance] = ‘a’; // 通过p1越界写入p2的内存,但标签相同,HWASan漏检
            break;
        }
    }
    return 0;
}

运行结果如下,p1p2 标签相同,p1 成功地“跨区域”修改了 p2 的内存,而HWASan未能拦截:

p1:0xb3000041e5032780 p1Tag:0xb3
p2:0xb3000041e50335a0 p2Tag:0xb3
distance between p1 and p2 is 3616
write one byte to 0xb3000041e50335a0, which is between p2‘s range [0xb3000041e50335a0, 0xb3000041e50335c0)

上述攻击的示意图如下:
Android HWASan内存错误检测原理与实战:提升应用稳定性的利器 - 图片 - 3

3. 延伸探讨

3.1 HWASan能捕获“第一现场”

在传统调试中,我们常遇到一种困境:即使拿到了程序崩溃的coredump文件,也可能难以定位问题的根本原因,因为崩溃点可能并非Bug的“第一现场”。

请看以下示例,指针 p 申请了8字节,却将其前后共1024字节的内存都写为0:

#include <stdio.h>
int main() {
    char* p = new char[8];
    for (int i = 0; i < 1024; i++) {
        *(p - 512 + i) = 0x0; // 真正的非法写入发生在这里
    }
    delete[] p; // 但程序可能在这里才崩溃
    return 0;
}

在Linux下使用GDB运行,崩溃很可能发生在 delete[] p 这一行,而真正的罪魁祸首 *(p - 512 + i) = 0x0 却被掩盖。在简单的Demo中我们一目了然,但在复杂的跨模块、跨线程项目中,这类问题排查起来极其困难。

下图对比了传统coredump与HWASan的定位能力:
Android HWASan内存错误检测原理与实战:提升应用稳定性的利器 - 图片 - 4
HWASan的优势在于,它能在内存发生错误访问的瞬间就捕获并报告,提供精确的错误地址和调用堆栈,极大提升了内存问题排查的效率。

提示:在Android平台编译上述测试程序时,建议添加 -O0 编译选项以防止Clang将无效操作优化掉。

3.2 硬件进化:Arm内存标记扩展

Arm 内存标记扩展(MTE)在功能与原理上与HWASan一脉相承,可以理解为将部分标签管理逻辑直接硬件化。因此,MTE的性能开销更低(资料显示CPU开销仅百分之几,内存开销约3%~5%),使得在生产环境中开启内存安全检测成为可能。

Android 13开始支持MTE,但目前兼容的硬件设备还相对较少。随着硬件生态的发展,MTE有望成为未来移动设备上标配的内存安全卫士。

参考资料

  1. Android官方HWASan文档
  2. Memory Tagging技术论文
  3. Android NDK Arm MTE指南
  4. MTE性能分析报告



上一篇:SmolVLM视觉语言模型深度解析:轻量级设计与消费级GPU部署实战
下一篇:Rust技术动态:2025下半年项目目标、GCC后端进展与WGPU/TOML更新
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 18:36 , Processed in 0.193323 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表