AddressSanitizer (Asan)

# AddressSanitizer

> 警告：在 Android 11 之后的 AOSP master 中，弃用了 arm64 上的平台开发 ASan，改为使用 HWASan。
>
>

AddressSanitizer (ASan) 是一种基于编译器的快速检测工具，用于检测原生代码中的内存错误。

ASan 可以检测以下问题：

* 堆栈和堆缓冲区上溢/下溢
* 释放之后的堆使用情况
* 超出范围的堆栈使用情况
* 重复释放/错误释放

ASan 可在 32 位和 64 位 ARM 以及 x86 和 x86-64 上运行。ASan 的 CPU 开销约为 2 倍，代码大小开销在一半到 2 倍之间，并且内存开销很大（具体取决于您的分配模式，但约为 2 倍）。

Android 10 和 AArch64 上的 AOSP master 分支支持硬件加速 ASan (HWASan)，这是一种 RAM 开销更小、检测到的错误范围更大的类似工具。除了 ASan 可以检测到的错误之外，HWASan 还可以检测返回之后的堆栈使用情况。

HWASan 具有类似的 CPU 和代码大小开销，但 RAM 开销要小得多 (15%)。HWASan 具有不确定性。只有 256 个可能的标记值，因此忽略任何错误的概率为 0.4%。ASan 对检测溢出规定了有限大小的红色区域，并对检测释放后使用情况规定了有限容量隔离区，而 HWAsan 没有这些规定，因此溢出大小或多久之前内存解除分配对 HWAsan 而言并不重要。这使得 HWASan 优于 ASan。您可以详细了解 HWAsan 的设计或 Android 上的 HWASan 的使用。

除了堆溢出外，ASan 还能检测堆栈/全局溢出，并能以最低的内存开销实现很高的速度。

本文档介绍如何使用 ASan 构建和运行 Android 的各个/所有组成部分。如果您准备使用 ASan 构建 SDK/NDK 应用，请改为参阅 Address Sanitizer。

## 使用 ASan 清理各个可执行文件

将 `LOCAL_SANITIZE:=address` 或 `sanitize: { address: true }` 添加到可执行文件的构建规则中。您可以搜索现有示例的代码或查找其他可用的排错程序。

检测到错误时，ASan 会向标准输出和 `logcat` 发送一份详细报告，然后让进程崩溃。

## 使用 ASan 清理共享库

根据 ASan 的运行原理，只有通过 ASan 构建的可执行文件可以使用通过 ASan 构建的库。
注意：在运行时，如果将 ASan 库加载到错误的进程中，系统将显示以 `_asan` 或 `_sanitizer` 开头的消息，提示您有无法解析的符号。

如需清理多个可执行文件中使用的共享库，其中部分可执行文件并非使用 ASan 构建的，您需要该库的两个副本。为此，建议您针对相关模块向 `Android.mk` 中添加以下命令：

```
LOCAL_SANITIZE:=address
LOCAL_MODULE_RELATIVE_PATH := asan

```

这样一来，系统会将库放到 `/system/lib/asan` 中而非 `/system/lib` 中。然后，使用以下方法运行您的可执行文件：

```
LD_LIBRARY_PATH=/system/lib/asan

```

对于系统守护程序，将以下命令添加到 `/init.rc` 或 `/init.$device$.rc` 的相应部分。

```
setenv LD_LIBRARY_PATH /system/lib/asan

```

警告：`LOCAL_MODULE_RELATIVE_PATH` 设置会将您的库移至 `/system/lib/asan`，这意味着，如果从头开始重写和重新构建，将会导致 `/system/lib` 中缺少该库，并且生成的映像可能无法启动。很遗憾，这是当前构建系统的一个限制。请不要执行 clobber 命令；而是使用 `make -j $N` 和 `adb sync`。

通过读取 `/proc/$PID/maps`，验证进程使用的库是否来自 `/system/lib/asan`（如果此库存在）。如果不是，您可能需要停用 SELinux：

```
adb root
adb shell setenforce 0
# restart the process with adb shell kill $PID

## 更出色的堆栈轨迹

ASan 使用基于帧指针的快速拆卷器 (unwinder)，为程序中的每个内存分配和解除分配事件记录堆栈轨迹。Android 的大部分组件都未使用帧指针。因此，您通常只会获得一到两个有意义的帧。如需解决此问题，请使用 ASan（推荐）或以下选项重新构建库：

```
LOCAL_CFLAGS:=-fno-omit-frame-pointer
LOCAL_ARM_MODE:=arm

```

或者，在进程环境中设置 `ASAN_OPTIONS=fast_unwind_on_malloc=0`。后者可能对 CPU 要求极高，具体取决于负载情况。

## 符号化

最初，ASan 报告中包含对二进制文件和共享库中的偏移量的引用。您可以通过以下两种方法获取源文件和行信息：

* 确保 `/system/bin` 中有 `llvm-symbolizer` 二进制文件。`llvm-symbolizer` 根据 `third_party/llvm/tools/llvm-symbolizer` 中的源代码构建而成。
* 通过 `external/compiler-rt/lib/asan/scripts/symbolize.py` 脚本过滤报告。

由于可以使用主机上的符号化库，因此第二种方法可以提供更多数据（即 `file:line` 位置）。

## 在应用中使用 ASan

ASan 无法检查 Java 代码，但可以检测 JNI 库中的错误。为此，您需要使用 ASan 构建可执行文件（在此情况下是 `/system/bin/app_process(*32|64*)`）。这将在设备上的所有应用中同时启用 ASan，因而负载非常大，但 2 GB RAM 的设备应该能够从容应对。

将 `LOCAL_SANITIZE:=address` 添加到 `frameworks/base/cmds/app_process` 中的 `app_process` 构建规则中。暂时忽略同一个文件中的 `app_process__asan` 目标（如果在您阅读本文时这个目标仍在其中）。

修改相应 `system/core/rootdir/init.zygote(*32|64*).rc` 文件的 `service zygote` 部分，将以下代码行添加到包含 `class main` 的缩进行代码块中，且缩进量相同：

```
    setenv LD_LIBRARY_PATH /system/lib/asan:/system/lib
    setenv ASAN_OPTIONS allow_user_segv_handler=true

```

构建，然后依次执行以下命令：adb sync、fastboot flash boot、reboot。

## 使用 wrap 属性

上一部分中的方法将 ASan 放到系统的每个应用中（实际上是放到 Zygote 进程的每个子项中）。您可以只通过 ASan 运行一个或少数几个应用，从而节省一些内存开销，但是应用启动速度会变慢。

为实现这一目标，您可以通过 `wrap.` 属性启动应用。下面是在 ASan 下运行 Gmail 应用的示例：

```
adb root
adb shell setenforce 0  # disable SELinux
adb shell setprop wrap.com.google.android.gm "asanwrapper"

```

在此情况下，`asanwrapper` 会将 `/system/bin/app_process` 重写至使用 ASan 构建的 `/system/bin/asan/app_process`。此外，它还会在动态库搜索路径的开头添加 `/system/lib/asan`。这样一来，通过 `asanwrapper` 运行应用时，系统会优先使用 `/system/lib/asan` 中进行 ASan 插桩的库，而非 `/system/lib` 中的常规库。

如果发现错误，应用会崩溃，且系统会将报告输出到日志中。

## SANITIZE\_TARGET

Android 7.0 及更高版本支持使用 ASan 一次性构建整个 Android 平台。（如果您要构建的版本高于 Android 9，那么 HWASan 是更好的选择。）

请在同一构建树中运行以下命令。

```
make -j42
SANITIZE_TARGET=address make -j42

```

在此模式下，`userdata.img` 中包含其他库，必须也刷写到设备上。请使用以下命令行：

```
fastboot flash userdata && fastboot flashall

```

这样将构建两组共享库：`/system/lib` 中的常规库（第一次 make 调用）和 `/data/asan/lib` 中进行 ASan 插桩的库（第二次 make 调用）。第二次构建的可执行文件会覆盖第一次构建的可执行文件。通过使用 `PT_INTERP` 中的 `/system/bin/linker_asan`，进行 ASan 插桩的可执行文件会获得一个不同的库搜索路径，该路径中的 `/system/lib` 前面添加了 `/data/asan/lib`。

`$SANITIZE_TARGET` 的值变更时，构建系统会重写中间对象目录。这样一来，系统便会强制重新构建所有目标，同时保留 `/system/lib` 下已安装的二进制文件。

有些目标无法使用 ASan 构建：

* 静态关联的可执行文件
* `LOCAL_CLANG:=false` 目标
* 不会针对 `SANITIZE_TARGET=address` 进行 ASan 操作的 `LOCAL_SANITIZE:=false` 目标

在 `SANITIZE_TARGET` build 中，系统会跳过此类可执行文件，且会将第一次 make 调用中构建的版本留在 `/system/bin` 中。

此类库未使用 ASan 进行构建，但它们可以包含来自其依赖的静态库的 ASan 代码。

## 支持文档

* Address Sanitizer
* AddressSanitizer 和 Chromium
* 其他 Google 排错程序