使用 KASAN+KCOV 编译 Pixel 内核

Kernel Address Sanitizer (KASAN) 可以帮助内核开发者和测试人员找出与运行时内存相关的错误,例如出界读取或写入操作问题,以及“释放后使用”相关问题。虽然 KASAN 因其运行时性能低以及导致内存使用量增加而未在正式版中启用,但它仍然是用来测试调试版本的重要工具。

在与另一个名为 Kernel Coverage (KCOV) 的运行时工具搭配使用时,经过 KASAN 排错和 KCOV 检测的代码可以帮助开发者与测试人员检测运行时内存错误以及获取代码覆盖率信息。在内核模糊测试(例如通过 syzkaller)的情景中,KASAN 可以协助确定崩溃的根本原因,而 KCOV 则会向模糊引擎提供代码覆盖率信息,以在测试用例或语料库重复数据删除方面提供帮助。

本页不讨论 KASAN 的内部工作原理或机制,而是指导您编译和修改 Android 开放源代码项目 (AOSP) 和 Pixel 的内核源代码,以便在开启 KASAN 和 KCOV 的情况下启动。

设置编译环境

请遵循下载和编译部分的步骤来设置编译环境。

编译 AOSP

下载 Android 源代码。为了编译 KASAN 映像,请选择未处于积极开发阶段的稳定版本。通常,最新的发布版本/稳定分支是不错的选择。有关版本和分支的更多信息,请参阅源代码标记和细分版本

成功检出源代码后,请从 Nexus 和 Pixel 设备的驱动程序二进制文件下载与目前所用设备和分支对应的必要设备 Blob。从系统芯片 (SOC) 制造商处同时下载供应商映像和二进制文件集。然后,解压下载的压缩包,运行其中包含的脚本,并接受许可。

接下来,请按照编译准备工作中的步骤,清理并设置您的编译环境,然后选择您的编译目标。

要创建一个基准工作版本,请确保不要对第一个版本进行任何修改:

make -j48

将您的编译结果刷入测试设备(例如 marlin),并使其启动:

cd out/target/product/marlin
ANDROID_PRODUCT_OUT=`pwd` fastboot flashall -w

启动主屏幕后,您可能会看到一个显示以下信息的弹出式窗口:

There's an internal problem with your device. Contact your manufacturer for details. 该弹出式窗口的消息可能表示,您供应商的版本指纹与您系统分区的版本指纹不一致。由于此版本仅用于开发和测试,而非用于发布,因此您可以忽略此消息。

编译内核

要编译内核,您需要检出正确的源代码,对其进行交叉编译,然后在正确的 AOSP 目录中编译内核映像。

检出内核源代码

创建一个目录来存储内核源代码,并将 AOSP 内核 Git 代码库克隆到本地存储。

mkdir ~/src/marlin-kernel-src
cd ~/src/marlin-kernel-src
git clone https://android.googlesource.com/kernel/msm

完成后,您应该会看到一个名为 msm 的空目录。

进入 msm 目录并 git checkout 与您正在编译的源代码对应的分支。要查看可用分支和标记的列表,请参阅 Android msm 内核源代码树

cd msm
git checkout TAG_NAME

完成此步骤后,msm 目录中应该会有相关内容。

执行交叉编译

接下来,您需要编译 Android 内核。

设置交叉编译器

要编译内核,您需要设置交叉编译器。目前推荐的已经过测试的工具链是 Android 的 NDK 工具链的最新稳定版本。要下载 Android NDK,请访问官方 Android NDK 网站。为您的平台下载相应的 zip 文件,然后将其解压缩。这会产生类似于 android-ndk-NDK_VERSION 的目录。

下载 LZ4c 工具

Pixel 内核使用 LZ4 压缩算法,因此在编译内核时需要使用 lz4c 工具。如果您使用 Ubuntu,请使用以下命令安装 lz4c 工具:

sudo apt-get install liblz4-tool

编译内核

marlin-kernel-src/msm 目录中使用以下命令设置编译环境:

export ARCH=arm64
export CROSS_COMPILE=PATH_TO_NDK/android-ndk-NDK_VERSION/toolchains/aarch64-linux-android-TOOLCHAIN_VERSION/prebuilt/linux-x86_64/bin/aarch64-linux-android-

然后,编译一个未经修改的内核版本以创建基准工作版本:

make marlin_defconfig
make -j48

编译流程的结果可以在以下位置找到:arch/arm64/boot/Image.lz4-dtb

在 AOSP 中重新编译启动映像

编译内核映像之后,请使用以下命令将结果复制到 AOSP 的 device/google/marlin-kernel 目录下:

cp ${marlin-kernel-src}/msm/arch/arm64/boot/Image.lz4-dtb device/google/marlin-kernel
source build/envsetup.sh
lunch aosp_marlin-userdebug
make -j48

编译成功后,请使用以下命令刷入目标设备:

cd out/target/product/marlin
fastboot flashall -w

刷入之后,您的设备应该会启动。在设备完成启动后,检查 Settings -> System -> About phone 下的 Kernel version,验证您刷入设备的映像是否是您编译的内核映像。

修改内核

启用 KASAN 和 KCOV 编译选项

KASAN 和 KCOV 代码受编译标记保护,不会针对普通版本启用。要启用这些代码,请将 KASAN 和 KCOV 选项添加到配置文件中,但是要记得删除 LZ4 配置。

为此,请创建默认配置文件的副本,例如 marlin_defconfig

cd arch/arm64/configs
cp marlin_defconfig marlin-kasan_defconfig

在新的配置文件中,移除 CONFIG_KERNEL_LZ4=y 这一标记并添加以下标记:

CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y
CONFIG_KCOV=y
CONFIG_SLUB=y
CONFIG_SLUB_DEBUG=y

使用新配置重新编译内核

修改完配置文件的副本后,请重新编译该内核。

重新配置内核

设置您的编译环境。编译您修改的 defconfig,并检查生成的 .config 文件中是否存在新添加的标记。

make marlin-kasan_defconfig
grep KASAN .config
CONFIG_HAVE_ARCH_KASAN=y
CONFIG_KASAN=y
# CONFIG_KASAN_OUTLINE is not set
CONFIG_KASAN_INLINE=y

您应该会看到 KASAN 标记。编译您的内核:

make -j48

查看修改后的内核映像

编译成功后,转到 arch/arm64/boot 目录查看编译结果。一般而言,Image.gz-dtb 大约为 23MB,比标准版本大。

cd arch/arm64/boot
ls -lh Image.gz-dtb
-rw-r--r-- 1 username groupname 23M Aug 11 13:59 Image.gz-dtb

要了解 KCOV 是否已经过正确编译,请针对生成的 vmlinux(位于内核源代码树的根目录)执行进一步的分析。如果您在 vmlinux 上运行 objdump,应该会看到对 __sanitizer_cov_trace_pc() 的大量调用。

sh -c '${CROSS_COMPILE}objdump -d vmlinux' | grep sanitizer
ffffffc000082030:    94040658    bl    ffffffc000183990 <__sanitizer_cov_trace_pc>
ffffffc000082050:    94040650    bl    ffffffc000183990 <__sanitizer_cov_trace_pc>
ffffffc000082078:    94040646    bl    ffffffc000183990 <__sanitizer_cov_trace_pc>
ffffffc000082080:    94040644    bl    ffffffc000183990 <__sanitizer_cov_trace_pc>
ffffffc0000820ac:    94040639    bl    ffffffc000183990 <__sanitizer_cov_trace_pc>

修改 AOSP 代码

您需要先调整 AOSP 源代码中用于控制设备启动方式的特定参数,然后再插入新的启动映像。这样做主要是为了确保新(已扩容)映像正常启动。

调整板参数

调整设备的 BoardConfig.mk 文件中定义的启动参数。该文件位于 device/google/marlin/marlin(AOSP 源代码根目录的相对路径)下。

cd device/google/marlin/marlin
vim BoardConfig.mk

如果您不想修改 BoardConfig.mk 文件,则可以改为创建一个包含名称 marlin_kasan 的新启动目标。要详细了解此过程,请参阅添加新设备

调整本地 Makefile 中的内核目标

新内核使用 LZ4 压缩算法来提升速度,但 KASAN 要求使用 gzip 来实现更好的压缩比。为了解决这个问题,您可以在 device/google/marlin/device-common.mk 中修改 LOCAL_KERNEL 变量指向的位置,从而指示编译系统要将哪个内核与最终目标绑定。

重新编译启动映像

要重新编译启动映像,请将新的内核映像复制到 AOSP 树中的设备专用文件夹(例如 device/google/marlin-kernel)。请确保这是编译系统预期的内核目标映像位置(根据您之前的修改)。

接下来,请重新编译可刷入的映像,具体方式类似于您之前编译 AOSP 的方式。成功编译后,请照常刷入所有编译映像。

使用经过修改的内核映像启动设备

您现在应该有一个可启动并能进入主屏幕的版本。在该版本中,您可以在早期启动阶段检查设备的 dmesg 输出中是否存在“KernelAddressSanitizer initialized”消息。该消息表示 KASAN 已在启动期间初始化。此外,您还可以确认设备上是否存在 /sys/kernel/debug/kcov(要执行此操作,您需要 root 权限)。

问题排查

您可以使用不同的内核版本进行实验,先将标准版本用作基准工作版本,然后再启用 KASAN+KCOV 编译选项。如果流程中断,请先检查您设备上的引导加载程序和基带版本是否与新版本要求的一致。最后,如果您使用的内核版本过高,那么您可能需要使用 Android 树上的较新的分支。