安卓真机内存检测迎来变革, 即HWASan实战以及Sanitizer生态深度剖析, 于移动开发范畴里, 内存安全问题仿若幽灵, 老是在最料想不到之时出现, 致使应用崩溃、性能卡顿甚至产生安全漏洞。对中高级安卓开发者来讲, 在模拟器上运行得顺风顺水的代码,一旦部署到真机上, 特别是那些内存配置不大相同的千元机之上, 就兴许暴露本来面目。说起来传统的那个AddressSanitizer也就是ASan, 它确实是有一定优点的, 然而要是说起它那动不动就超过200%以上的内存开销, 哎, 在真机进行调试的时候, 这可常常是会让不少人一想到它便心生退意——毕竟谁都不会愿意仅仅是因为开启了这个检测工具, 就径直触发OOM也就是内存溢出的情况。而这恰恰就是那个HWASan也就是Hardware-assisted AddressSanitizer闪亮登场时所依托的背景来由。它可不是全新冒出来的某种设想, 只不过是ASan在ARM64架构之上的那种有着“硬件加速”特性的进化版本罢了, 它的核心目标确切来说就是要使极具高性能的内存检测能够顺利进入真机环境中去。对于这篇文章, 我们并不打算去复述那些基础的编译参数手册。我会从一个开发者视角, 该开发者经历过计数无法估算的heap - buffer - overflow以及use - after - free的折磨, 带领你深入到HWASan的实战腹地。我们要拆解它跟ASan在本质上的区别, 一步又一步动手操作完成从NDK配置直到Android Studio集成的全部流程, 并且解决那些你在真机上肯定会碰到的“坑”。进一步加强重要性凸显维度而言, 我们会朝着围绕全部Sanitizer工具生态的方向, 把HWASan往其中放置, 去弄明白在什么时候适宜应用它, 而在另外一些时候又应当挑选它那些等同于“兄弟姐妹们”的存在——UBSan、TSan、MSan方可达成合适配置 毕竟, 挑选契合适配的工具, 相较于一味盲目使用高级工具来说, 具备更为关键重要意味及突出指向意义## 1. 围绕充分理解 HWASan需求: 为何它会成为真机调试范畴之内的“游戏规则变换制造者”要达成真正意义上的妥善运用HWASan, 首先需要摒弃掉那种“其仅仅是一个具备更快速度表现意味的ASan”这样一种相对简单化的认知理念方可达成关联适配。软件逻辑与硬件能力协同的思维转变, 源自它根植于现代ARM处理器硬件特性的设计哲学, 这是一场思维转变。### 1.1核心原理: 从“影子内存”到“内存标签”传统ASan的工作原理是“影子内存(Shadow Memory)”, 它会为应用程序的每一字节内存, 在另一个隐藏区域, 维护一个“影子状态”, 任何一次内存访问, ASan都会先检查对应的影子状态, 判断这次访问是不是合法, 比如说, 是不是访问了已释放区域或者缓冲区之外。此机制极为强大, 然而代价高昂, 那便是它需额外占用大约两倍的内存用以存储影子数据, 并且每一次内存访问皆伴随着一次额外的检查, 进而致使性能下降两至三倍。c// 传统ASan的近似逻辑(概念性)void *ptr = malloc(16);// ASan内部: 在影子区域标记这16字节为“可访问”shadow_memory。
address_to_shadow(ptr)
等于已分配的空间, 在访问内存的时候, 字节值等于指针加上二十计算得出的值, 存在潜在的越界访问情况, 在ASan内部, 计算指针加上二十之后的影子地址并且进行检查, 如果影子内存。

address_to_shadow(ptr+20)
!= ALLOCATED) {报告错误("堆缓冲区溢出");}然而HWASan运用了ARMv8.5 - A架构所引入的**最高字节忽略(TBI)**特性。在64位系统里, 一个指针的高8位(最高有效字节)平常并未被用于寻址。HWASan的聪慧之处在于, 它**把这闲置的8位当中的4位用作"内存标签(Memory Tag)"**。它的工作流程是这样的: 首先, 在分配的时候进行打标签, 也就是当借助malloc或者new来分配一块内存之际, HWASan分配器会生成一个随机的标签, 比如说0xA, 并且把这个标签存储在指针的高位部分, 与此同时, 还会将这个标与该内存区域的元数据建立关联。其次, 在访问的时候检验标签, 即当通过指针去访问这块内存时, CPU硬件会自动把指针里的标签提取出来, 然后跟内存区域所关联的标签开展比对。3. 存在这样一种情况, 即当匹配不当的时候那就会出现报错现象: 要是标签之间不构成匹配关系(举例来说,指针标签呈现为0xA, 然而内存区域标签却已经经过重新分配而变为0xB), 硬件层面则会瞬间引发一个异常状况, 硬件一旦引发这个异常, 由HWASan运行时捕捉住这个异常, 并且上报错误信息。c语言中, HWASan的近似逻辑是概念性的, 借助硬件, void类型指针ptr通过malloc函数分配16字节内存空间, HWASan会生成随机标签0xA, 存入ptr的高位, 并且在元数据中记录ptr到ptr加15区域的标签为0xA, ptr的值 的高位是标签0xA, 看起来可能像这样0x0Axxxxxxxxxxxxxx, 访问内存时硬件会自动参与!有一个字节的值, 它等于那个指针加上二十之后所作递增指向处的值, 这会引发越界访问, 在CPU硬件方面, 会发现那个指针加上二十之后所指向的地址, 其关联的元数据标签有可能是未定义的, 或者属于其他区域范畴, 硬件会触发标签不匹配这种异常, 接下来HWASan运行时会捕获, 并报告此机制, 其精妙之处存在于, 标签检查是呈现由硬件进行并行执行状态的,几乎不会增加额外的时钟周期, 然而标签自身仅仅占用指针当中本来闲置的位, 内存开销是极小的。理解原理之后, 对于 HWASan 与 ASan, 我们通过一个表格直观感受两者关键性能的差异, 其决定适用场景。对比维度方面, 有 HWASan、ASan 以及对开发者的意义之分。内存开销一项中, 前者是约 16% , 后者是超 200% , 就此而言, HWASan 让在真机特别像 4GB/6GB 内存设备上去进行内存检测变为可能, 极大程度降低了 OOM 风险。它、呢 , 、是 , 此 , 这 有 , 其 用 , 适为 , , 场 , 景 , 定!关于性能开销, 存在这样的情况, 在HWASan检测下应用运行更流畅, 更接近真实用户体验, 其性能开销处于1.1x - 1.7x区间, 还有一种情况性能开销在2x - 3x区间,此情况适合进行集成测试, 甚至适合部分场景的压力测试, 所以HWASan检测下有着类似多种相应特性, 会让各项运行流畅度等变化且多样, 还与适合的测试具体情形不同密切相关啊。**检测范围**: 堆溢出, 栈溢出, 全局变量溢出, Use - after - free, 同左, **额外增加内存泄漏检测**, HWASan**不是直接对内存泄漏展开检测**, 需要与LeakSanitizer(LSan)或者其他工具相结合。ASan则默认把LSan集成在内。|| **硬件依赖** | **仅仅局限于ARM64 (v8.5+)**, 需要TBI给予支持, 拥有x86_64、ARM64、MIPS等主流架构 | HWASan是ARM64那种生态环境独自具有的优化, 在x86模拟器或者旧的ARM设备上面是没办法去使用的。 || **部署环境** | **确实是真机也就是(Android 11+)** | 主要还是针对模拟器/仿真环境 | HWASan的核心价值是体现在真机能进行调试, 能够把问题复现的环境跟用户设备达成一致。|| **栈溢出检测粒度** | 只是针对于8/有16/32/64字节对齐的访问 | 能够检测任意字节大小的溢出 | HWASan对于栈上微小溢出(就像char buf写入第有6字节)有可能漏报, 这是其精度方面的一次权衡。 |> **提示**: HWASan有的内存开销主要源自为毎16字节内存块维护的标签存储与额外的保护页(Guard Pages), 并非像ASan那样庞大的影子内存。这正是そり开销大幅降低的关键。可以从表格看出, HWASan凭借对栈溢出检测的部分精度, 以及对x86架构的兼容性, 换得了在ARM64真机上革命性的低开销。对于以真机当作最终运行环境的Android开发来讲, 这个交换无疑是值得的。## 2. 搭建HWASan实战环境: 从NDK到APK理论听起来光鲜漂亮, 但使HWASan在你自己的项目里能够运行起来, 这才是真正的起始。在此我们规避教科书式的步骤清单, 以解决实际问题作为导向。在着手敲代码以前, 就得确认自身的环境契合HWASan的“上岗”需求。好多开发者在这一步受阻, 皆是由于忽视了某个前置条件。其中, “NDK版本”是首个阻碍因素。你务必要采用“NDK r21或更高版本”。我极力推荐选用当下稳定的LTS版本(像r25+), 以此获取最优的兼容性以及工具链支持。你能在堪称Android Studio的SDK Manager里去开展下载以及管理NDK版本这一行为, 目标设备与系统方面, 设备CPU得是ARM64 (AArch64)架构才行, 为此你能够借助adb shell getprop ro.product.cpu.abi命令去进行确认, 而Android系统版本, 那也必须是Android 11 (API 30) 或者更高方可。这是鉴于HWASan需要内核里的特定支持 , 也就是CONFIG_ARM64_SW_TTBR0_PAN。 内核配置方面 , 设备内核得启用CONFIG_ARM64_SW_TTBR0_PAN以及CONFIG_ARM64_TAGGED_ADDR_ABI。 对于Google Pixel系列或者主流厂商Android 11+的设备 , 一般已经开启。 要是不确定 , 有个简单的测试办法是尝试编译并运行一个HWASan示例。**编译工具链**: 一定得运用**Clang/LLVM**。GCC对HWASan不提供支持。NDK r21+默认情况已启用了Clang , 因而一般来讲无需增加额外配置。### 2.2 在CMake项目里融入HWASan假定你采用CMake当作原生库的构建系统(这同样是当下Android Studio所推荐的方式) , 融入HWASan简便得出人意料。 你没必要改动每一处编译标志, CMake给出了不错的抽象。**核心操作处于你的CMakeLists.txt文件里, 针对目标(一般是你的本地库)去设置编译以及链接标志: **cmake# 于你的CMakeLists.txt当中target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=hwaddress -fno-omit-frame-pointer)target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=hwaddress)* -fsanitize=hwaddress: 此为启用HWASan的神奇开关。添加 -fno-omit-frame-pointer, 强烈被建议这么做。它将会保留帧指针, 促使当HWASan报告错误时, 堆栈回溯的信息变得更完整且更可读出来。虽说会造成微小的性能有损失, 可是对于调试的体验提升规模巨大。要是你的项目结构繁杂, 存在多个静态库或者有着动态库, 请搞确保, 所有相互之间链接起来的库都运用相同的sanitizer标志去编译, 不然的话, 或许在链接或者运行的时候会出现未定义行为。2.3之中, 于Android Studio(Gradle)里一键配置, 针对大多数Android应用开发者而言, 我们更为习惯在Gradle的方面去开展配置, Android Gradle插件借助externalNativeBuild使得这所有的一切变得简易。于你模块级的build.gradle.kts(或者build.gradle)文件里, 找寻android.defaultConfig.externalNativeBuild.cmake部分, kotlinandroid, defaultConfig, 在此部分中, externalNativeBuild, cmake, 在此范围内, 有其他参数, arguments.add("-DANDROID_ARM_NEON=TRUE"), arguments.add("-DANDROID_STL=c++_shared") , 这里可以是c++_static, 关键的一行是启用HWASan, arguments.add("-DANDROID_SANITIZE=hwaddress") , 没错, 仅仅只需添加-DANDROID_SANITIZE=hwaddress这一个参数。Gradle以及CMake会帮你去处理后续的全部复杂的编译器与链接器标志传递事宜, 要构建你的项目, 要是配置正确的话, 你会在构建输出当中看到相关标志被应用, 需要注意的是, 要是你同时针对多个ABI进行构建, 像是arm64 - v8a以及x86_64, 那么HWASan仅仅会对arm64 - v8a产生效果, 在为x86_64进行构建的时候, 这个标志会被忽略或者引发编译警告, 这属于正常现象。### 3. 等到真机运行、问题捕获以及日志分析配置完毕之后, 把带有HWASan检测那个APK安装到符合条件的真机之上。在这种时候, 你的应用已然处于HWASan的监测之下了。### 3.1 致使触发而且捕获错误, HWASan的错误报告是即刻的。一旦出现内存违规情况, 应用会立马崩溃, 并且在出现记载中的猫中输出详尽的诊断消息。这跟ASan的行为是一样的。请明确一下问题, 比如对这段内容进行润色、提取关键信息、根据其进行拓展等等, 不然不太清楚具体按什么要求改写。如果是按照要求改写这段操作流程的表述, 以下是改写后的: 操作流程如下, 首先, 要进行安装 APK 这一步, 具体操作是通过 adb install app-debug-hwasan.apk 命令来实现。接着到启动并监控日志环节, 在一个终端里面, 运用过滤的命令去开启对 HWASan 相关日志的监控, 具体的命令是 bash, 但这里的 adb logcat 需要加上 -s"hwaddress" 这个参数, 当然你也可以选择更宽泛的方式, 也就是使用 grep 并且带上 HWAddressSanitizer 来操作。最后一部分是运行应用并触发错误, 你的动作得是操作自家的应用, 从而重现疑似出现内存问题的那种场景。4. 解析崩溃日志, 于崩溃出现之时, logcat会给出一份格式显得特别清晰的对应报告。### 3.2详细解读HWASan崩溃报告, 一份典型的HWASan对应报告涵盖了去定位问题所需要提及的全部信息。我们来瞅一瞅一个实例, ==12345==错误提示, 硬件地址清理器显示, 在地址0x0(此处有误, 应为0x0042a00000000010, 按要求保留错误原文)处 tag -不匹配, 该地址在程序计数器0x00400abc处, 线程T0(主线)在那里对应有一个大小为4的数据读取操作;#0 0x400abc在某个函数(SomeFunction)里对应有操作, 该函数在文件/path/to/your/file.cpp路径里的第123行;#1 0x401234在主程序(main)里有对应情况 对应文件是/path/to/your/main.cpp路径下的第56行;0x0042a00000000010这个地址位于一个具有16字节存储区域右侧零字节 的对应的地方。
将0x0042a00000000000, 0x0042a00000000010所对应的内存区域, 由线程T0在此处分配: #0 位于/path/to/llvm/projects/compiler - rt/lib/hwasan/hwasan_allocation.cpp:123中的0x400a0d处的malloc函数#1 位于/path/to/your/file.cpp:120中的SomeFunction()函数标签: a1/00 (指针/内存) 让我们一行一行剖析: * **ERROR: HWAddressSanitizer: tag - mismatch**: 错误种类, 此处为“标签不匹配”, 一般而言意味着“use - after - free”或者“访问了未分配/已释放的内存”。关于在地址0x0042a00000000010处尝试弄出的那个读操作, 实际上是4字节的读, 这里发生在了主线程也就是thread T0 (main), 之后堆栈回溯了, #0、#1展现出致使错误出现的函数调用链条, 准确到了文件和行号是file.cpp:123, 这可是最宝贵的信息。“内存区域描述”为, is located 0 bytes to the right of 16 - byte region, 明确指出这属于一个“堆缓冲区溢出(heap - buffer - overflow)”, 访问点紧挨着一个16字节分配区域的右边界, 也就是越界了0字节,而实际上是访问了边界外的第一个字节。分配堆栈, 显示这块内存分配位置的“allocated by thread T0 here”, 这有助于理解该内存生命周期。标签信息, 显示指针携带标签(a1)与内存区域当前标签(00)的“Tags: a1/00”, 不匹配(a1!= 00)直接触发错误。3.3解决常见真机环境报错, 在真机上你可能会碰到一些于模拟器时没有遭遇到的状况, 其中一种显示为error: unknown sanitizer 'hwaddress', 此状况产生的缘由是NDK版本偏低, 也就是要低于r21 , 而解决该问题的办法是, 将你的NDK更新至r21或高于r21的相应版本。将应用安装之后立马启动就会崩溃, 而且丝毫没有详细的HWASan日志, 原因之一是, 设备内核并不支持HWASan, 比如说那些Android 10或者更早发布版本的设备, 解决办法是, 要确认设备系统是Android 11以及以上版本, 还可以试着在adb shell当中执行cat /proc/config.gz | gunzip | grep CONFIG_ARM64_SW_TTBR0_PAN, 瞧瞧是否在返回结果里出现= y或者=m。在原因方面, 存在这样一种情况, 即依赖的某个原生库, 它没有使用HWASan进行编译, 进而致使出现不兼容现象。在解决办法上, 要做到确保, 确保所有你连接到项目中的第三方原生库, 这里的库则包含预编译生成的.so文件, 这些全部要么同样使用HWASan重新编译, 要么是那种纯净的、不依赖sanitizer运行时的版本。而预编译库通常是最大的导致兼容性出现挑战的因素。报告当中呈现出堆栈回溯的情况, 然而函数名却是搞成了乱码要么是偏移地址, 原因在于发布版本把调试符号给剥离掉了, 或者是没有去添加 -fno - omit - frame - pointer, 解决办法是在调试版本里要确保编译的时候添加了 -g也就是生成调试信息以及 -fno - omit - frame - pointer标志。越过 HWASan: 该工具链的生态位与选型指南里, HWASan可不是单独的一个工具, 它仅仅是 LLVM Sanitizer 家族当中, 被应用在针对特别场景情况的时候, 所被使用的一个利器, 一个已经发展到头变得很厉害较完善的人, 要在熟悉过程这种跟开发者调试器一样的东西的时候熟知整个工具家族里的每一个成员, 以及他们各自的特长, 还要在熟知的过程中注意这些工具是在针对特定场景情况的时候才选用的。### 4.1 工具名为Sanitizer家族核心成员图谱中的某一部分呈现出这样一批信息与内容, 其中工具一项所显示名字为AddressSanitizer (ASan), 核心目标是检测内存出现的错误情况, 包括溢出、被错误地进行释放操作即用空闲之后还要再使用等情况, 以及有可能会出现内存被泄露的状况, 该工具的典型开销体现在对内存占有达到该内存本身两倍以上, 速度降低到原本速度的二分之一到三分之一, 此工具的最佳适用场景是在模拟器或者持续集成环境当中进行深度的内存检测, 它对于Android真机是否合适也在图谱中有相应情况的显示。具有最全功能。发生开销巨大, 真机容易引发OOM相关情况, 不进行推荐。**HWAddressSanitizer (HWASan)** 具备针对内存错误(不包含泄漏情况)的低开销检测功能, 在内存方面占用约16%, 速度相较于原来为1.1x - 1.7x增长范围, 用于 **ARM64真机** 上展开内存错误检测。在性能与检测能力方面达成平衡状态。属于 **核心推荐** 范畴, 适用于真机调试以及测试场景。并列双竖线后面跟着的 **未定义行为净化嚣(圣婴行为清洁者)**, 执行那种检测未定义活动之举(像是整数过度溢出、空的指的针去解除引用之类等情况), 其速度方面所产生的耗费通常。