📚 《从源码长一台 Pixel:自编译纯 AOSP 15 日用机》系列
- 打通中国移动 VoLTE 电话
- 焊上内核级 Root(SukiSU-Ultra GKI 内建) ←(本篇)
本系列记录把一台 Pixel 9 Pro XL(
komodo)刷成”从源码自编译、除必要闭源组件外尽量纯净”的 AOSP 15 日用机的全过程。
前言
接着上一篇打通中国移动 VoLTE 的 Pixel 9 Pro XL(代号 komodo,自编译纯 AOSP 15)继续折腾。这台机器我想要的是一个全功能日用机:电话、蓝牙、WiFi、GApps、双击熄屏,外加一个内核级 Root。
为什么非要内核级(KernelSU/SukiSU 这一类),而不是 Magisk 那种基于 init 的方案?因为既然内核都是我自己从源码编译的,root 就应该焊死在内核里——不依赖 ramdisk patch、不怕 OTA、不怕全量擦除,开机即生效。
听起来很美。实际上,从”内核里编进了 KSU”到”管理器真的拿到 root”,中间隔着五道坎,每一道都足够让人卡一整天。这篇就是完整记录。剧透结局:最后在 SELinux enforcing 下,SukiSU-Ultra 管理器显示绿色的 Working,dmesg 里那行 Crowning manager 稳稳出现,而且整套被烤进了 ROM 镜像,全量擦除重刷也不丢。
注:本文所有设备序列号、签名密钥指纹、内网地址、主机名等均已脱敏。涉及的 GKI 版本号、KMI 代号、内核配置项均为公开技术信息,予以保留。文中”编译服务器”指一台远程 Linux 构建机。
零、先把”内核级 root 到底怎么生效”想清楚
走弯路之前,得先理解 KernelSU 系(SukiSU-Ultra 是其分支)的工作模型。内核里编进了 KSU 代码 ≠ 你有 root。真正的链条是:
- 内核里的 KSU 通过 hook
execve,监视进程启动; - 检测到 zygote 启动(
/system/bin/app_process第一次拉起),触发on_post_fs_data(); on_post_fs_data启动一个 throne_tracker,去扫/data/app里安装的 APK;- 对每个 APK 调
is_manager_apk→check_v2_signature,把它的 V2 签名证书 跟内核里硬编码的apk_sign_keys[]比对; - 命中 → 内核打印
Crowning manager(加冕管理器),给这个 UID 装上 su 能力; - 此后管理器 App 发的 IOCTL 才被内核认,root 才真正可用。
任何一环断了,管理器都会显示”未安装/无 root”。下面五道坎,本质上就是这条链上五个不同的断点。
第一道坎:KPM 把内核拖进硬死锁
SukiSU-Ultra 支持 KPM(Kernel Patch Module),一个很诱人的特性。我一开始 CONFIG_KPM=y 编进去了。
结果一开机,进系统几十秒后必崩,dmesg/last_kmsg 里是:
1 | kernel_panic - hard_hang ... cpu X by ehld |
ehld 是 Exynos 的硬锁检测看门狗。每次都在 keystore / adb 授权 相关动作时触发——KPM 的运行时 patch 跟 SoC 的某些路径冲突,直接把 CPU 拖进 hard lockup。
排查方向一度跑偏(以为是 SELinux、以为是 HAL),最后用控制变量法:关掉 KPM 单独编一版,硬死锁立刻消失。
教训:
CONFIG_KPM在这颗 SoC(zumapro/Exynos 系)上不稳,直接去掉。内核级 root 本身不需要 KPM。
第二道坎:蓝牙和 WiFi 全废——元凶是一个 rfkill.ko
去掉 KPM,能进系统了。但蓝牙打不开,WiFi 也连不上。
dmesg 里一堆模块加载失败。顺藤摸瓜发现核心是 rfkill.ko:
- WiFi 驱动
bcmdhd、蓝牙的电源/共存驱动nitrous都依赖rfkill; - 而 GKI 把
rfkill.ko列为受保护模块(gki_aarch64_protected_modules),用的是 Google 预编译的那一份; - 我自己编的内核 vermagic 跟预编译
rfkill.ko对不上,内核拒绝加载它 → 连锁导致nitrous/bcmdhd全部起不来。
修法是把 rfkill 从模块改成内建,绕开 vermagic 校验:
- 内核配置
CONFIG_RFKILL=y(直接编进内核,不再是.ko); - 从
modules.bzl和gki_aarch64_protected_modules里删掉net/rfkill/rfkill.ko那一行; - ROM 侧
system_dlkm的modules.load/modules.dep里把 rfkill 的引用清掉。
重编、刷入,蓝牙能开、WiFi 能连了。模块加载数从个位数恢复到几百个,BT/WiFi 驱动正常 attach。
教训:自编译 GKI 内核 + 厂商预编译模块的组合里,任何”受保护模块”只要你动了内核就会 vermagic 失配。要么把它编成内建(
=y),要么保证它跟你的内核一起重编。
第三道坎:内核根本不认我的管理器签名
蓝牙 WiFi 好了,root 还是没有。管理器装上去显示”未安装/工作不正常”,dmesg 里完全没有 Crowning manager。
回到第零节那条链:内核 apk_sign_keys[] 里硬编码的是 SukiSU 官方发布版管理器的签名证书:
1 | // kernel/manager/apk_sign.c |
而我的管理器是用本机的 Android debug keystore(androiddebugkey)签的(后面会解释为什么必须自签)。证书对不上,check_v2_signature 直接判否,自然不加冕。
check_v2_signature 的逻辑是:读 APK 的 V2 签名块里”证书长度”字段,先比长度,再比证书的 SHA-256。所以我要做两件事:
算出自己 debug 证书的长度和 SHA-256:
1
2
3
4keytool -exportcert -keystore ~/.android/debug.keystore \
-alias androiddebugkey -storepass android | wc -c
# → 744 = 0x2e8(证书长度)
# 再对同样的输出取 sha256 即证书指纹把内核
apk_sign_keys[]第一项换成我自己的:1
{ 0x2e8, "<本机 debug keystore 证书 SHA-256,已脱敏>" },
重编内核。这下 dmesg 里开始出现签名比对的日志:
1 | KernelSU: sha256: <…>, expected: <…> |
两个 hash 一致了。但是——还是没有 Crowning manager。说明链条断在更前面:on_post_fs_data 压根没被触发过(dmesg 里搜不到 on_post_fs_data!)。这就引出了最难的第四坎。
第四道坎(最难):钩子模式不对,zygote 永远检测不到
这一坎卡了最久。现象:内核日志里有 KernelSU: LSM hooks initialized(说明 KSU 起来了),但永远不出现 on_post_fs_data!,因此管理器永远不被扫描、不被加冕。
逐行读源码后定位到根因,分两层:
第一层:kprobe 模式拿不到 argv。
on_post_fs_data() 只在一个地方被调用——runtime/ksud.c 的 ksu_handle_execveat_ksud() 里,检测到 zygote 启动时:
1 | if (first_zygote && !strcmp(path, "/system/bin/app_process") && argv_has(...)) { |
注意它要读 argv。而我当时用的是 kprobe hook 模式——kprobe 挂在 execve 上时,拿到的 argv 指针往往是 NULL/不可靠。条件判断永远不成立 → zygote 检测不到 → on_post_fs_data 永不触发。
第二层:手动 hook 的 patch 又没调对函数。
于是改用 manual hook 模式(CONFIG_KSU_MANUAL_HOOK=y),它需要你在内核 execve 路径上手动插入 KSU 的钩子调用。但我参考的那份 scope-minimized patch 只插了 ksu_handle_execve_sucompat()(处理 su 兼容),从来不调 ksu_handle_execveat_ksud()——也就是说,触发 on_post_fs_data 的那个函数根本没被挂上。
定位到这里,修法就清晰了。在内核的 fs/exec.c、do_execveat_common() 的入口处,亲手把那个关键调用加回去:
1 | // fs/exec.c, do_execveat_common() 开头 |
(extern 声明用 void* 形参,避免跟 struct user_arg_ptr 的内部定义冲突。)
重编、刷入。dmesg 终于走通了完整链条:
1 | KernelSU: exec zygote, /data prepared |
管理器 App 打开,绿色 Working、Manual Hook、Enforcing。这一刻 root 真正活了。
教训:kprobe 模式在这套内核上拿不到可靠
argv,必须用 manual hook;而且要确认你用的 hook patch 真的调用了ksu_handle_execveat_ksud,而不仅仅是 su_compat 那一支。这是整件事最隐蔽的断点。
第五道坎:boot.img 一刷就进 recovery
root 通了,但在把内核打包成 boot.img 实刷时又踩一个坑:用 mkbootimg 从厂商那个约 51MB 的预编译 boot 重新打包出来的镜像,fastboot boot 或刷入后会进 recovery(ro.boot.mode=recovery),起不到正常系统。
换了一种打包方式就好了:用 magiskboot 从 AOSP 自己 out/ 目录里那个 64MB 的 boot.img 做 unpack → 换内核 → repack。这样出来的 boot 正常进系统。
两者的差别在于 ramdisk / 镜像结构:厂商 51MB 预编译 boot 的 header 配置不适合直接拿来塞我的内核,而 AOSP out/ 的 64MB boot 结构正确。
教训:自换内核打 boot 时,基准镜像选错会静默地把你导进 recovery。用
magiskboot基于本机 AOSP 产出的 boot 来 repack,比 mkbootimg 从厂商预编译 boot 重打更稳。
六、烤进 ROM:让全量擦除也不丢
到这里,root 已经能用、重启也在(boot 分区刷进去了)。但还差临门一脚:全量擦除(fastboot -w)重刷出厂 img.zip,会回退到没有 manual-hook 的旧内核。要真正”焊死”,必须把这个内核烤进 ROM 镜像本体。
这台设备的 ROM 走的是 BOARD_PREBUILT_BOOTIMAGE(TARGET_NO_KERNEL=true):ROM 不从 Image 现场组装 boot,而是直接打包内核目录里那个预编译 boot.img。所以思路是:把那个预编译 boot 换成”已验证可用”的版本,再重新出包。
最稳的”已验证可用”基准是什么?——就是设备上此刻正在跑的那个 boot 分区本身。因为是 userdebug,adb 直接是 root,干脆 dd 把它整个拉回来当 ground truth:
1 | adb shell 'dd if=/dev/block/by-name/boot_a of=/data/local/tmp/live_boot.img' |
这份 live_boot.img 是 100% 跑通过的,连 AVB footer 都是现成正确的。把它覆盖到内核预编译目录的 boot.img,然后 m dist 重新出包。
构建产物是一个约 2GB 的 *-img-*.zip。验证它内部的 boot.img 确实是 manual-hook 内核(64MB、版本串对、AVBf 在)后,做一次非破坏性整刷(不带 -w,保留数据和已装管理器):
1 | fastboot update --force <baked-img>.zip # 注意:不带 -w |
重启,dmesg 自动出 Crowning manager,蓝牙 / SIM(中国移动)/ 内核模块全在。
这意味着 root 现在是 img.zip 的一部分——以后无论怎么刷(包括 -w 全量擦除),开机内核都会自己把管理器加冕。root 焊死了。
七、收尾:版本号红条
最后一个小尾巴:管理器顶部一直挂着一条红色警告——
Manager version (40781) and KernelSU driver version (40798) mismatch!
虽然不影响使用(已经 Working),但看着难受。根因挺有意思:管理器和内核驱动来自两个不同的 git 仓库,各自按自己的提交数算版本号,公式是:
1 | versionCode = 4 * 10000 + commitCount - 2815 |
- 管理器仓(浅克隆、git 传输被墙)里
commitCount被硬编码成 3596 →40781 - 内核驱动所在的 builtin 仓有 3613 个提交 →
40798
把管理器构建脚本里那个硬编码的 commit 数从 3596 改成 3613,重编 assembleDebug(debug keystore 会自动用我那个已被内核认可的证书签名),装上去——红条消失,管理器和驱动都是 40798。
总结
从”内核里有 KSU”到”管理器真正 Working”,这五道坎复盘下来:
| # | 坎 | 现象 | 修法 |
|---|---|---|---|
| 1 | KPM | keystore 时 EHLD 硬死锁 | 去掉 CONFIG_KPM |
| 2 | rfkill 受保护模块 | 蓝牙/WiFi 全废 | CONFIG_RFKILL=y + 从 protected modules / modules.dep 移除 |
| 3 | 管理器签名 | 内核不认、无 Crowning |
apk_sign_keys[] 植入自签证书的长度+SHA-256 |
| 4 | 钩子模式(最难) | on_post_fs_data 永不触发 |
manual hook + 在 do_execveat_common 手动调 ksu_handle_execveat_ksud |
| 5 | boot 打包 | 一刷进 recovery | 用 magiskboot 基于 AOSP out/ 的 64MB boot 重打 |
外加把内核烤进 BOARD_PREBUILT_BOOTIMAGE、用设备 dd 出的 live boot 当基准,做到全量擦除不丢 root。
最大的收获其实是第零节那个心智模型:内核里编进 KSU 只是起点,真正决定 root 能不能用的是”加冕链”上的每一环——execve 钩子能不能拿到 argv、zygote 能不能被检测到、on_post_fs_data 有没有被触发、管理器签名认不认。把这条链在脑子里跑通,每一个”管理器显示没 root”的现象,都能精确定位到链上的某个断点。
至此,这台自编译纯 AOSP 15 的 Pixel 9 Pro XL 集齐了:中国移动 VoLTE、蓝牙、WiFi、GApps、双击熄屏、以及焊进 ROM 的内核级 Root,全部在 SELinux enforcing 下永久生效。一台真正意义上”自己从源码长出来”的日用机。