为了在我心爱的 Google Pixel 4 XL 上用上最新最热的 KernelSU,我一度下定决心要自己构建内核镜像来主动适配,然而前几次的尝试都以失败告终。不是遇到莫名奇妙的错误就是根本看不懂,终于在昨天进行了一下午的尝试后,我成功地给设备刷上了自己亲手编译的内核。

准备环境

由于是打算在自己的设备上编译,当然少不了 Linux 环境。在这之前需要确保至少有 20GB 以上的硬盘空间和 6GB 以上的内存。虽然我已经在用 ArchLinux 作为电脑系统了,但为了尽可能的排除环境问题,我还是决定再整一个 Ubuntu 的虚拟环境专门用来编译。不过用虚拟机就太大动干戈了,干脆弄一个 Docker 环境吧。

拉取镜像并运行:

1
2
docker pull ubuntu:22.04
docker run -v /mnt/disk9/Temp/DockerBuild:/workdir -it ubuntu:22.04 bash

我在这里挂载了本地硬盘上的一个文件夹到 Docker 容器的 /workdir 目录下,以便直接在系统文件管理器里面进行操作。

进入容器后更新软件源并安装必要的依赖,如果觉得慢的话可以换国内镜像源,我这里没有换。

1
2
apt-get update -y
apt-get install git repo kmod cpio ccache automake flex lzop bison gperf build-essential zip curl zlib1g-dev g++-multilib libxml2-utils bzip2 libbz2-dev libbz2-1.0 libghc-bzlib-dev squashfs-tools pngcrush schedtool dpkg-dev liblz4-tool make optipng maven libssl-dev pwgen libswitch-perl policycoreutils minicom libxml-sax-base-perl libxml-simple-perl bc libc6-dev-i386 lib32ncurses5-dev libx11-dev lib32z-dev libgl1-mesa-dev xsltproc unzip device-tree-compiler python3 python2

下载源码

由于这里我打算使用谷歌提供的构建系统,所以要用 repo 工具来直接下载它提供的一整套编译工具链和源码。首先需要下载目标仓库的 manifest 文件,它包含内核源码以及相关编译工具的所有仓库地址。首先,你需要去 manifest 的仓库里找到你设备型号所对应的分支名称,比如我的 Pixel 4 XL 的机型代号是 coral,所对应的分支名就是 android-msm-coral-4.14-android13。然后使用这个分支名称来下载源码和工具链:

1
2
3
4
5
6
7
8
9
mkdir /workdir/android-kernel && cd /workdir/android-kernel

# 使用 repo 下载个源码竟然还必须得先配置 git committer
git config --global user.email "you@example.com"
git config --global user.name "Your Name"

# 最后面的就是上面的分支名称
repo init -u https://android.googlesource.com/kernel/manifest -b android-msm-coral-4.14-android13
repo sync

在这一步由于一些原因,我碰到了一个 git 的报错,可能是网络问题或者设备导致的,如果你没碰到这个报错就可以接着下面的编译步骤。

1
2
3
stderr:
>> Fatal Error:early EOF
>> error:index-pack died

虽然我最终没能直接解决这个报错,但我找到了一种手动进行 repo init 的方法。

首先通过 USTC 的镜像站手动下载 repo 的源码:

1
git clone --depth=1 https://gerrit-googlesource.lug.ustc.edu.cn/git-repo

然后把 git-repo 下的主程序文件 repo 中的第一行:#!/usr/bin/env python 最后面改成 python3,接着保存。把 repo 文件复制到 /usr/bin 下面,最后再把整个文件夹重命名为 repo 并复制到构建目录的 .repo 目录下面。

1
2
3
cp git-repo/repo /usr/bin/
mv git-repo repo
cp -r repo /workdir/android-kernel/.repo/

然后重新运行上面的两个 repo init ...repo sync 命令即可拉取源码。

下载源码和工具链的过程非常慢,我大概 7M/s 的网速用了 20 分钟左右,垃圾校园网只能这样了。

开始编译

在准备好源码和工具链之后,就可以开始进行编译了。首先我决定先试一下谷歌官方的内核源码,看看有没有问题,之后再尝试编译其他的第三方内核。

转到 kernel-build 目录下,找到机型所对应的可执行脚本文件。比如这里我需要执行的文件应该是 build_floral.sh,其中的 floral 是上面提到的机型代号。不过实际上 Pixle 4 XL 的机型代号叫做 coral,只是在内核代号这里叫 floral,不过其他的大部分设备应该都不会有这个区别,这里不用太关心这个问题。

执行 ./build_floal.sh 就可以开始编译。整个编译过程大概只有十来分钟,如果配置更高的电脑应该会更快一些。编译完成后的产物都在 out/android-msm-pixel-4.14/dist 文件夹下,其中的 Image.lz4 就是我们所需要的内核镜像文件。有的机型可能也会叫 Image.lz4-dtb,这些只是压缩格式上的区别,同样不需要担心。

至于如何使用这个文件会在最下面还会提到,只需要把它复制到 AnyKernel3 的目录下并打包刷入即可。

编译 PixelExperience 内核

由于我的设备的谷歌原生系统已经停止维护,所以为了能用上最新的安全补丁,我刷了 PixelExperience 类原生系统。对于内核来说,如果仍然使用刚刚编译的谷歌官方内核,就相当于缺少最新的内核补丁了。所以我决定试着编译第三方的 PixelExperience 内核,并把它刷入到设备里。

PE 类原生的内核仓库都在 PixelExperience-Devices 这个组织帐号下面,只需要搜索对应的机型代号就可以找到内核源码仓库,例如我的 Pixel 4 XL (coral) 的内核源码仓库就是 kernel_google_msm-4.14 。下面我们需要把这个仓库拉取下来,并用它替换掉上面步骤中下载的谷歌官方源码。

对于谷歌的内核构建系统,内核源码文件都在 private 目录下。其中的 msm-google 包含了内核的大部分源码文件,msm-google-modules 目录则包含的是一些外置的驱动文件。需要注意的是,大部分第三方内核都是已经把这些外置驱动集成到 msm-google 的源码中去了,所以没有 msm-google-modules 这个文件夹。

首先备份一下谷歌官方源码:

1
2
3
cd private
mv msm-google msm-google.bak
mv msm-google-modules msm-google-modules.bak

然后下载 PE 的第三方内核源码,放到 msm-google 目录下。

1
git clone --depth=1 https://github.com/PixelExperience-Devices/kernel_google_msm-4.14.git msm-google

然后接下来就要开始编译这个第三方内核了。这里遇到的坑非常多,在研究了一两个小时后勉强算是找到了解决方法。下面我来说一下大概率会遇到的几个坑吧。

首先是开始编译的时候很有可能出现的一个报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Verifying that savedefconfig matches private/msm-google/arch/arm64/configs/floral_defconfig
++ RES=0
++ diff -u private/msm-google/arch/arm64/configs/floral_defconfig /workdir/android-kernel/out/android-msm-pixel-4.14/private/msm-google/defconfig
--- private/msm-google/arch/arm64/configs/floral_defconfig 2023-06-25 09:42:16.617639700 +0000
+++ /workdir/android-kernel/out/android-msm-pixel-4.14/private/msm-google/defconfig 2023-06-25 09:42:29.594563400 +0000
@@ -371,7 +371,6 @@
CONFIG_TOUCHSCREEN_SEC_TS=y
CONFIG_TOUCHSCREEN_TBN=y
CONFIG_TOUCHSCREEN_OFFLOAD=y
-CONFIG_TOUCHSCREEN_HEATMAP=m
CONFIG_TOUCHSCREEN_FTS=y
CONFIG_INPUT_MISC=y
CONFIG_INPUT_QPNP_POWER_ON=y
++ RES=1
++ '[' 1 -ne 0 ']'
++ echo ERROR: savedefconfig does not match private/msm-google/arch/arm64/configs/floral_defconfig
ERROR: savedefconfig does not match private/msm-google/arch/arm64/configs/floral_defconfig
++ return 1

报错的主要部分是 savedefconfig does not match... 这句话,上面的具体内容不见得会一模一样。大致的意思就是生成的配置文件跟内核中的配置文件有一些项目不匹配,这个需要手动改掉内核源码配置来解决。在上面的报错中,我打开了 private/msm-google/arch/arm64/configs/floral_defconfig 这个配置文件,删掉 CONFIG_TOUCHSCREEN_HEATMAP=m 这一行就可以把问题解决。虽然这个 ``TOUCHSCREEN_HEATMAP配置项没了,但是实际编译的时候经过观察.config` 文件发现它的值是 y,也就是说仍然会编译这个。猜测可能是跟官方源码的默认值冲突了吧。

接着的一个坑是在编译快结束的时候,在显示 “Building external modules and installing them into staging directory” 之后编译程序会突然退出,不输出任何报错信息,但实际上编译没有成功。这个问题的排查虽然费了我一些时间,但是最后发现的原因是很简单的。

还记得上面提到的 msm-google-modules 文件夹吗?它其实就是这里的 “external modules”。然而我现在在编译的是第三方的内核,这些 “external modules” 已经被集成了,根本就没有这个文件夹,所以编译到这里就会出错。

那怎么让它不编译这个 “external modules” 呢?让我们来看看 build.sh 里面是怎么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if [[ -z "${SKIP_EXT_MODULES}" ]] && [[ -n "${EXT_MODULES}" ]]; then
echo "========================================================"
echo " Building external modules and installing them into staging directory"

for EXT_MOD in ${EXT_MODULES}; do
# The path that we pass in via the variable M needs to be a relative path
# relative to the kernel source directory. The source files will then be
# looked for in ${KERNEL_DIR}/${EXT_MOD_REL} and the object files (i.e. .o
# and .ko) files will be stored in ${OUT_DIR}/${EXT_MOD_REL}. If we
# instead set M to an absolute path, then object (i.e. .o and .ko) files
# are stored in the module source directory which is not what we want.
EXT_MOD_REL=$(rel_path ${ROOT_DIR}/${EXT_MOD} ${KERNEL_DIR})
# The output directory must exist before we invoke make. Otherwise, the
# build system behaves horribly wrong.
mkdir -p ${OUT_DIR}/${EXT_MOD_REL}
set -x
make -C ${EXT_MOD} M=${EXT_MOD_REL} KERNEL_SRC=${ROOT_DIR}/${KERNEL_DIR} \
O=${OUT_DIR} "${TOOL_ARGS[@]}" ${MAKE_ARGS}
make -C ${EXT_MOD} M=${EXT_MOD_REL} KERNEL_SRC=${ROOT_DIR}/${KERNEL_DIR} \
O=${OUT_DIR} "${TOOL_ARGS[@]}" ${MODULE_STRIP_FLAG} \
INSTALL_MOD_PATH=${MODULES_STAGING_DIR} \
${MAKE_ARGS} modules_install
set +x
done

fi

根据第一行的判断来看,只需要设置了 SKIP_EXT_MODULES 这个环境变量就可以让它不编译 External Modules 了。那么 EXT_MODULES 这个变量又是在哪定义的呢?经过我的一番研究,发现它是在 private/msm-google/build.config.common 这个文件里面定义的。所以指定 SKIP_EXT_MODULES这个环境变量或者是删掉这个文件里的 EXT_MODULES 变量都可以让它跳过 External Modules 的编译。

在这里需要插入讲一个知识点。谷歌的这套编译脚本每次执行的时候都会把之前的编译产物全部删掉再重新编译。但这里我们并没有改内核源码,按理说是不需要清除编译产物的。为了节省时间,有没有办法在重新执行的时候让它接着先前失败的地方继续编译呢?在 build.sh 中有着这样一段:

1
2
3
4
5
6
7
echo "========================================================"
echo " Setting up for build"
if [ -z "${SKIP_MRPROPER}" ] ; then
set -x
(cd ${KERNEL_DIR} && make "${TOOL_ARGS[@]}" O=${OUT_DIR} ${MAKE_ARGS} mrproper)
set +x
fi

也就是说只要设置了 SKIP_MRPROPER 这个环境变量,就可以让它不删除先前的编译产物。

那么让我们指定一下这些变量重新编译:

1
SKIP_EXT_MODULES=1 SKIP_MRPROPER=1 ./build_floral.sh

接着在编译快完成的时候又碰到一个报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
========================================================
Creating initramfs
+++ rm -rf /workdir/android-kernel/out/android-msm-pixel-4.14/staging/initramfs_staging
+++ mkdir -p /workdir/android-kernel/out/android-msm-pixel-4.14/staging/initramfs_staging/lib/modules/0.0/kernel/
+++ cp -r /workdir/android-kernel/out/android-msm-pixel-4.14/staging/lib/modules/4.14.295-g0706bafc763f-dirty/kernel/drivers /workdir/android-kernel/out/android-msm-pixel-4.14/staging/lib/modules/4.14.295-g0706bafc763f-dirty/kernel/fs /workdir/android-kernel/out/android-msm-pixel-4.14/staging/lib/modules/4.14.295-g0706bafc763f-dirty/kernel/techpack /workdir/android-kernel/out/android-msm-pixel-4.14/staging/initramfs_staging/lib/modules/0.0/kernel/
+++ cp /workdir/android-kernel/out/android-msm-pixel-4.14/staging/lib/modules/4.14.295-g0706bafc763f-dirty/modules.order /workdir/android-kernel/out/android-msm-pixel-4.14/staging/initramfs_staging/lib/modules/0.0/modules.order
+++ cp /workdir/android-kernel/out/android-msm-pixel-4.14/staging/lib/modules/4.14.295-g0706bafc763f-dirty/modules.builtin /workdir/android-kernel/out/android-msm-pixel-4.14/staging/initramfs_staging/lib/modules/0.0/modules.builtin
+++ '[' -n '
private/msm-google-modules/wlan/qcacld-3.0
private/msm-google-modules/touch/fts/floral
' ']'
+++ mkdir -p /workdir/android-kernel/out/android-msm-pixel-4.14/staging/initramfs_staging/lib/modules/0.0/extra/
+++ cp -r '/workdir/android-kernel/out/android-msm-pixel-4.14/staging/lib/modules/*/extra/*' /workdir/android-kernel/out/android-msm-pixel-4.14/staging/initramfs_staging/lib/modules/0.0/extra/
cp: cannot stat '/workdir/android-kernel/out/android-msm-pixel-4.14/staging/lib/modules/*/extra/*': No such file or directory

仔细找了一下这个步骤的具体位置,还是在 build.sh 里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if [ -n "${BUILD_INITRAMFS}" ]; then
echo "========================================================"
echo " Creating initramfs"
set -x
rm -rf ${INITRAMFS_STAGING_DIR}
# Depmod requires a version number; use 0.0 instead of determining the
# actual kernel version since it is not necessary and will be removed for
# the final initramfs image.
mkdir -p ${INITRAMFS_STAGING_DIR}/lib/modules/0.0/kernel/
cp -r ${MODULES_STAGING_DIR}/lib/modules/*/kernel/* ${INITRAMFS_STAGING_DIR}/lib/modules/0.0/kernel/
cp ${MODULES_STAGING_DIR}/lib/modules/*/modules.order ${INITRAMFS_STAGING_DIR}/lib/modules/0.0/modules.order
cp ${MODULES_STAGING_DIR}/lib/modules/*/modules.builtin ${INITRAMFS_STAGING_DIR}/lib/modules/0.0/modules.builtin

if [ -n "${EXT_MODULES}" ]; then
mkdir -p ${INITRAMFS_STAGING_DIR}/lib/modules/0.0/extra/
cp -r ${MODULES_STAGING_DIR}/lib/modules/*/extra/* ${INITRAMFS_STAGING_DIR}/lib/modules/0.0/extra/ # 这一行
(cd ${INITRAMFS_STAGING_DIR}/lib/modules/0.0/ && \
find extra -type f -name "*.ko" | sort >> modules.order)
fi

通过上面标注的一行可以推断出这个错误还是由于没有 External Modules 导致的。由于定义 EXT_MODULES 的地方有好几个,这里决定直接把 BUILD_INITRAMFS 这个变量删除掉,反正我们确实不需要 initramfs 镜像。

private/msm-google/build.config.common 文件中,删除掉 BUILD_INITRAMFS=1 这一行。重新编译。

下面的编译几乎就没有碰到报错了。如果又碰到了 make 的莫名其妙的报错,可以尝试把 out 文件夹彻底删除掉再重新编译。

集成 KernelSU

终于可以开始试着集成 KernelSU 到内核了。在经过上面的一番折腾之后,这一步反倒是显得很简单。

只需要进入内核源码目录,执行 KernelSU 已经为我们准备好的一键脚本:

1
2
cd private/msm-google
curl -LSs "https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh" | bash -

重新执行编译脚本。需要注意的是这里已经修改了内核源码,所以重新编译时就不要设置 SKIP_MRPROPER 这个环境变量了,让它自己把之前的编译产物删掉就行了。

1
2
cd /workdir/android-kernel
SKIP_EXT_MODULES=1 ./build_floral.sh

这里大概率还会碰到上面所说的第一个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Verifying that savedefconfig matches private/msm-google/arch/arm64/configs/floral_defconfig
++++ RES=0
++++ diff -u private/msm-google/arch/arm64/configs/floral_defconfig /workdir/android-kernel/out/android-msm-pixel-4.14/private/msm-google/defconfig
--- private/msm-google/arch/arm64/configs/floral_defconfig 2023-06-25 09:47:37.083873000 +0000
+++ /workdir/android-kernel/out/android-msm-pixel-4.14/private/msm-google/defconfig 2023-06-25 11:22:40.879957400 +0000
@@ -687,7 +687,6 @@
CONFIG_QUOTA_NETLINK_INTERFACE=y
CONFIG_QFMT_V2=y
CONFIG_FUSE_FS=y
-CONFIG_OVERLAY_FS=y
CONFIG_INCREMENTAL_FS=m
CONFIG_VFAT_FS=y
CONFIG_TMPFS_POSIX_ACL=y
++++ RES=1
++++ '[' 1 -ne 0 ']'
++++ echo ERROR: savedefconfig does not match private/msm-google/arch/arm64/configs/floral_defconfig
ERROR: savedefconfig does not match private/msm-google/arch/arm64/configs/floral_defconfig
++++ return 1

解决方法同上,打开 private/msm-google/arch/arm64/configs/floral_defconfig 这个文件,删掉 CONFIG_OVERLAY_FS=y 这一行即可。

重新编译,不出意外的话应该不会出什么问题。

使用 AnyKernel3 刷入内核

编译完成后,所有的编译产物都在 out 文件夹里面,我们所需要的内核镜像文件就是 out/android-msm-pixel-4.14/dist/Image.lz4 这个文件(也有可能是 Image.lz4-dtb)。现在我们需要把这个文件放到 AnyKernel3 里面。

1
2
3
4
cd /workdir
git clone --depth=1 https://github.com/osm0sis/AnyKernel3.git
cp android-kernel/out/android-msm-pixel-4.14/dist/Image.lz4 AnyKernel3/
cd AnyKernel3

然后需要稍微修改一下 AnyKernel3 的配置:

1
2
3
4
5
6
7
8
# 关闭设备型号检查
sed -i 's/do.devicecheck=1/do.devicecheck=0/g' anykernel.sh
# 把目标分区设为自动
sed -i 's!block=/dev/block/platform/omap/omap_hsmmc.0/by-name/boot;!block=auto;!g' anykernel.sh
# 把检测是否为 A/B 分区的选项设为自动
sed -i 's/is_slot_device=0;/is_slot_device=auto;/g' anykernel.sh
# 删除不必要的文件
rm -rf .git* README.md

然后把这个目录打包成压缩包:

1
zip -r kernel-flash.zip .

然后重启手机到 TWRP,以你喜欢的方式把 kernel-flash.zip 传到手机上并刷入即可。

需要注意的是,AnyKernel3 默认会检测是否已安装 Magisk,如果已经安装了 Magisk 的话则会尝试保留 Magisk 并刷入新内核。也就是说刷完之后 KernelSU 和 Magisk 是同时存在的。如果你想使用 KernelSU 的模块系统的话,就得先把 Magisk 卸载掉再刷入新内核。至于怎么卸载这里就不在赘述了,常见的方法有刷卸载包或者还原 boot 镜像等,在网上也有很多相关教程。

刷完内核后 WiFi 用不了?

经过我的一系列尝试,发现无论是刷入原厂内核还是 PE 内核或者是带 KSU 的 PE 内核,只要是自己编译出来的内核,刷完开机之后 WiFi 一定都用不了。一开始以为是内核中无线网卡的驱动没打上,但经过我又一下午的研究,发现这个问题跟具体的设备是有关系的。

首先来讲一下 Linux 系统的 WiFi 驱动到底是什么。实际上,Linux 的驱动基本都是内核模块(Kernel Modules)。这些驱动大部分都会在编译内核的时候直接集成进去,但是并不是所有的设备驱动都会集成到内核里,比如 WLAN 驱动就是在编译内核的时候单独把模块编译成 .ko 文件,然后集成到系统中,再让系统控制内核去从外部加载这个模块,WiFi 才能正常工作。对于 AOSP 13,这些单独的内核模块被放在了 /vendor/lib/modules 目录下。

但是,Linux 内核从外部加载模块时对版本号以及内核指纹等都有着严格的要求,而我们自己编译的内核指纹和 ROM 开发者构建时的内核指纹一定是不匹配的。所以当刷入了自己编译的内核后,由 ROM 提供的内核模块便都无法加载。不过有的设备的 WiFi 驱动可能并不是以外部内核模块的形式驱动的,所以可能不会遇到这个问题。

至于解决方法,我目前并没有找到特别完美的方案。不过,我们可以通过手动载入模块的方法来验证上面我的想法的正确性。还记得编译产物的那个文件夹吗?在 out/android-msm-pixel-4.14/dist/ 文件夹下,除了我们所需要的内核镜像外,还有一大堆编译出来的内核模块文件。其中有一个叫做 wlan.ko 的文件,就是我们所需要的无线网卡驱动了。把这个文件复制到手机上,然后手动载入:

1
2
3
4
adb push wlan.ko /sdcard/
adb shell
cd /sdcard/
insmod wlan.ko

如果在执行完 insmod 命令之后没有任何输出的话,就说明这个模块已经被成功载入了。如果不确定,可以用 lsmod 命令来查看当前内核中载入了哪些模块。载入成功之后,试着去打开 WiFi 开关,不出意外的话已经可以正常使用 WiFi 了。

另外我观察到一部分论坛魔改的第三方内核(比如 CleanState)在刷入我的设备后其实是可以正常使用 WiFi 的,这里个人猜测这种魔改的第三方内核可能已经修改了内核的模块加载机制,使其可以加载任何 ROM 所提供的内核模块,不过这个的研究难度对我可能就相当大了,等下次有空再研究吧。


不出意外的话,我决定还是老老实实用 Magisk,等换了支持 GKI 的手机之后再想着上 KernelSU 吧。