ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ARKM KERNEL:基于CAF的安卓内核深度定制与DWS调度优化

ARKM KERNEL:基于CAF的安卓内核深度定制与DWS调度优化 最近几天不少朋友在问 ARKM KERNEL 的动态其实项目还没有正式 release但既然代码已经跑到了 90% 稳定度我可以先把这一路的思路、选型、踩坑记录摊开聊聊。内核这层东西最怕的就是看起来跑通了——真机重启一次、某个 IO 调度器换了策略、某个驱动在特定固件下丢中断问题就原形毕露。ARKM KERNEL 想做的是围绕移动平台的内核深度定制在高通 CAF 主线基础上重构调度策略、强化安全加固、收敛功耗曲线最后以可刷写内核包的形式提供给开发者。注意ARKM 不是一个官方厂商项目而是一个社区驱动的内核实验计划。从标题也能看出来我们离发布不远了但approaching you soon这种表述也意味着还有几个已知问题没有完全闭环。这篇文章挑几个重点聊聊为什么选 CAF 分支、DWS 调度器的引入逻辑、构建链路里那些反复踩坑的细节、以及安全加固方向我们到底改了哪些东西。不管你是想自己编译内核的开发者还是对内核调度感兴趣的玩家读完后都应该能少走不少弯路。1. ARKM KERNEL 到底想解决什么问题1.1 为什么是 CAF 分支而不是 AOSP Common很多第一次接触安卓内核的人会直接去拉 AOSP 的 common kernel也就是 android13-5.10、android14-5.15 这类分支。这个分支的好处是干净、通用、和上游 Linux 主线接近适合做学术研究或者给模拟器用。但放到高通平台上它就是跑不起来的典型——缺少 GPU 驱动、缺少 DSP 相关节点、modem 接口对不上、显示流水线完全没有初始化。原因很简单高通的硬件能力大量依赖 vendor 闭源模块这些模块通过内核的 vendor hooks、ioctl 透传和特定的设备树节点交互AOSP common kernel 没有这些上下文。所以我们选用 CAFCode Aurora Forum内核。CAF 是高通面向自家 SoC 维护的 kernel 分支本质上是在 Linux 主线基础上把硬件驱动、调度增强、功耗管理、安全补丁全部揉进去的分支。拉取路径现在是 CodeLinaro比如 msm-5.10 分支git clone https://git.codelinaro.org/clo/la/kernel/msm-5.10.git -b kernel.lnx.5.10.r1-rel这一步看似简单实际有个容易翻车的点CAF 有非常多的小分支release 分支和 debug 分支混在一起分支名里的r1-rel表示这是第一个 release 版本。如果你图省事直接拉默认分支会在编到一半的时候发现头文件冲突因为默认分支可能带了额外的 trace 或 test 代码。选分支不要选最新选对应目标平台维护最成熟的那个。另外注意CAF 的代码结构其实包含了多个子目录比如techpack/下是音频、显示、摄像头、GPU 等驱动模块。属于高通的 vendor 模块刷第三方内核时如果这些模块版本和用户态固件不匹配会出现没有声音、相机黑屏、GPU 掉驱动等问题。ARKM KERNEL 首版尽量不动 techpack 里的核心接口只做调度、内存、安全三个方向的修改这是控制风险的关键决定。1.2 项目形态与内核版本选型ARKM KERNEL 的定位不是换个壁纸式的内核也不是全功能重写式的野心项目。它的目标分三层第一层可复现的构建系统。任何一个拿到源码的人按照文档跑完编译命令能得到和发布包哈希一致的产物。第二层基于场景的调度优化。在 EAS 框架下引入 DWS 动态权重调度改善前台交互任务和后台任务的资源竞争。第三层安全加固子集的落地。把 PAN、KASLR、CFI 强制开启并对部分高危驱动做权限收敛和调用路径审计。版本选型上我们最终选了 msm-5.10。5.10 是 LTS 分支生命周期足够长CAF 对它的维护最完善主流高通平台如 SM8250、SM8350、SM8450 等都有对应内核。5.15 的新特性固然更吸引人但 CAF 对 5.15 的部分平台支持还不完整尤其是老旧 GPU 驱动模块对 5.15 的适配有明显断层。还有一点很多玩家会选择直接拉最新但内核开发最忌讳追新。LTS 版本意味着你有足够长的时间验证改动社区遇到过的坑基本都有解工具链兼容性也容易确定。ARKM KERNEL 不是军用武器不图纸面参数最新图的是真机能稳定跑一个月不出诡异问题。2. 调度器路线从 EAS 到 DWS 的实验2.1 WALT 与 EAS 的配合逻辑聊 DWS 之前得先把调度器的基础逻辑说清楚。现在安卓设备清一色 ARM 大小核架构所以 CAF 调度器的主流方案是 EASEnergy Aware Scheduling配合 WALTWindows-Assist Load Tracing。EAS 的核心思路很简单调度器在决定任务跑在哪个 CPU 上时不再只看 CPU 负载而是把功耗模型也纳入了考虑。大小核有不同的功耗曲线一个轻量级后台任务放到小核上可能只耗 50mW放到大核上却要 200mWEAS 会倾向把小核作为目标。听起来很美但 EAS 对负载预测的准确性依赖非常高。这就是 WALT 的用武之地。传统 Linux 的 PELTPer-Entity Load Tracking追踪的是过去一段时间的平均负载反映的是历史趋势WALT 则在 20ms 左右的窗口期内统计任务的忙碌时间和休眠时间能更快捕捉到任务的突然变化。手机上的交互任务有个特点突然唤醒、短暂爆发、然后又休眠PELT 对这种模式反应偏慢经常出现任务已经跑到大核上了负载统计还没跟上的情况。CAF 的 EASWALT 组合已经比上游 Linux 好很多但它在多任务场景下有明显偏向所有任务一视同仁地被放进负载追踪和功耗模型但实际使用中用户正在滑动列表时启动一个后台编译这两类任务的优先级天差地别EAS 却可能把后台编译任务误判成值得上大核的任务结果就是前台滑动掉帧后台编译也没快多少。2.2 DWS 动态权重如何改变任务分类DWSDynamic Weighted Scheduling动态权重调度是我们正在实验的一套调度扩展。它的核心不是新造一个调度类而是给 WALT 的任务负载追踪增加一组场景权重让调度器在计算任务优先级和 CPU 选择时额外考虑任务所属的场景类型。简单地说WALT 统计的是一个任务最近有多忙DWS 统计的是这个忙到底有多重要。比如一个渲染线程它忙碌是因为用户在滑动屏幕这个忙碌的权重要放大一个后台下载任务的忙碌权重则要相应缩小。权重不是静态的而是随系统状态动态调整当系统检测到当前用户处于高交互状态滑动、输入、动画播放交互类任务的权重上调当系统处于空闲或深度休眠等待时后台任务的权重恢复正常水平。这个机制在实现上依赖一组新的 trace 点和用户态注入的场景标签。CAF 的调度器树里已经有类似的雏形比如sched_assist、sched_boost等接口但它们的粒度太粗——boost 一下就是全局所有任务的优先级都提高没法做细粒度区分。DWS 做的事情是在负载追踪的update_task_ravg路径上引入权重系数让任务进入 CPU 选择逻辑之前它的负载值已经被修正过一轮。举个例子一个渲染线程在 WALT 窗口内的运行时间是 15ms窗口大小是 20ms负载是 0.75。如果不加权重它在大多数平台上已经可以触发调度器的大核迁移阈值。但加入 DWS 后如果这个任务被打上了后台音效渲染标签权重系数可能是 0.6负载变成 0.45调度器就会认为小核也能满足它如果被打上输入响应标签权重系数是 1.3负载变成 0.975调度器会果断把它放到大核并且给它更高的迁移稳定性避免在大小核之间来回震荡。这种方式的好处是保留了 EAS 的功耗模型计算没有推翻重建。坏处是用户态的标签注入如果做得不好反而会引入额外开销所以我们首版做得很保守只对六类任务注入标签输入分发、渲染、视频解码、音频播放、后台下载、后台编译其余任务权重一律为 1.0避免默认路径被污染。2.3 调参实践先从日志而不是数据开始DWS 最难的其实不是逻辑实现而是参数调优。内核调度器跟业务代码不一样它跑在非常靠底的位置任何参数的改变都像湖水里的涟漪会在奇怪的角落激起浪花。我们的调参流程是先在调度器里打开CONFIG_SCHED_DEBUG在/sys/kernel/debug/sched/下面挂起详细的状态输出。然后跑一组标准场景包括连续滑动微博/信息流 5 分钟、播放 4K 视频、后台循环编译内核模块、混合场景前台滑动 后台编译同时进行。每种场景跑完后抓取 WALT 窗口内的任务负载分布、CPU 迁移次数、大核驻留时间、以及掉帧率。刚开始调 DWS 权重时我把交互任务权重拉到 1.5结果掉帧率确实降低了但整机功耗上升了 18%。原因是权重太高导致调度器过度激进任务频繁上大核而交互任务本身的执行时间很短迁移和唤醒的开销反而抵消了性能收益。后来又试了 1.2掉帧率略有回升但功耗只上升了 6%是收益最平衡的点。这里有个经验调内核参数时不要只看性能测试 App 的分数一定要看调度的实际行为日志。量化分数往往会掩盖瞬时延迟问题而日志能告诉你真实发生了什么。比如有一次性能数据非常好看但日志里显示某个渲染线程在大核和小核之间来回迁移了几百次显然是一种虚假性能——跑分负载足够高掩盖了迁移抖动但用户实际手感就是掉帧。后来给任务加了迁移稳定窗口限制才真正解决问题。3. 构建链路的坑比想象中多得多3.1 工具链版本的隐形兼容层如果有人告诉你内核编译就是make defconfig make那他只适合编 x86 上游主线。到了 CAF 内核工具链版本的选择直接决定你能不能被坑死。CAF 官方推荐用 Clang 12 或 13 来编译 5.10 分支但我们实测下来Clang 15 在某些文件上会产生不同的二进制布局虽然能编译通过但个别驱动在运行时会出现不可解释的内存异常。所以我们干脆把工具链版本锁死在 Clang 12.x同时配套使用对应版本的ld.lld。LLVM 各子工具之间的版本耦合非常强只换编译器不换链接器或者只换了 ld.lld 而 binutils 版本落后都会导致链接阶段出现奇怪的unwind段错误或relocation truncated。构建命令我们固定在这样一套export ARCHarm64 export CCclang export CROSS_COMPILEaarch64-linux-gnu- export LLVM1 export LLVM_IAS1 make Oout vendor/lahaina_defconfig make Oout -j$(nproc) Image.gz dtbs这里面LLVM1的意思是整个构建工具链全部走 LLVM包括汇编器、链接器、strip、objcopy全都用 LLVM 系工具。如果你机器上没有装齐全套 LLVM 工具比如只有 clang 没有 llvm-strip构建过程会在最后一步打包时报错。这是新手最容易忽略的地方。另外如果你的开发环境是 WSL2还需要注意一点WSL2 的内核版本会影响挂载和文件 I/O 行为特别是inotify和 overlayfs 相关功能。建议先用wsl --update把 WSL2 内核升级到最新再开始构建否则可能遇到make时文件监视报错、或者输出目录权限异常的问题。这个和安卓内核本身无关纯属构建环境的隐性依赖。3.2 defconfig 裁剪的边界感CAF 默认的 defconfig 文件往往开了一大堆驱动和调试选项直接编译产物很大而且很多模块我们根本用不到。ARKM KERNEL 对 defconfig 做了三周的裁剪但裁完之后发现一个重要经验内核裁剪要克制不是为了把 .config 改小而是为了关闭不需要的攻击面和不必要的调度路径。以 CAF 的 vendor defconfig 为例路径通常在arch/arm64/configs/vendor/vendor.lahaina.config。里面默认开启了几百个硬件驱动其中大量设备在这台手机上根本不存在。我们做的是把与目标设备硬件列表无关的驱动全数关闭用adb shell ls /sys/class/和getprop ro.hardware对照硬件清单把不会出现的设备驱动# CONFIG_XXX is not set。这能让镜像体积缩小 20% 左右更重要的是减少驱动层攻击面降低后续安全审计的代码量。但决不要为了追求极致的zImage小而裁剪过头。我们踩过一个大坑把CONFIG_SERIAL_MSM_GENI关了因为觉得串口调试没什么用米青简一点。结果下一次真机死机时串口日志完全无法输出只能盲猜崩溃点。后来重新把这个配置打开还额外开了CONFIG_PSTORE_RAM才恢复了对崩溃现场的基本掌控。裁剪的内核必须保留两样东西串口日志输出能力和 pstore 崩溃记录能力。这两条是内核调优的底线绝对不能裁掉。3.3 从 Image.gz 到 AnyKernel3 刷入包编出来的内核只是arch/arm64/boot/Image.gz要刷进手机还得做一套完整的 boot 镜像。网上流传的各种一键刷内核脚本本质上都是先解包现有 boot.img替换里面的内核再重新打包。但这种方式有个隐患内核镜像需要和dtb对齐。很多脚本只会替换Image.gz不会管 dtb结果刷完开机卡在 logo 或者进不了系统其实就是 dtb 不匹配导致。我推荐直接用 AnyKernel3 来做刷入包它会在设备端执行dd写入 boot 分区并在 updater-script 里自动 patch 内核到现有 boot 镜像。它的最大好处是兼容性好能在不知道原始 boot 镜像确切格式的情况下完成内核替换因为它在设备端通过unpack_bootimg实时处理。ARKM KERNEL 的打包流程简化下来就是编出Image.gz和dtbs把 dtb 按平台规则拼接成dtb.img放进 AnyKernel3 的根目录再放入我们自写的anykernel.sh里面指定block/dev/block/by-name/boot然后zip打包。整个流程可以在 CI 里自动化跑确保每个测试 build 都有一致的刷入包结构。一个非常重要但容易被忽略的点刷内核前必须备份当前 boot 镜像。即使你用的是 AnyKernel3也不能保证设备端固件里没有额外的 boot header 校验逻辑。备份命令很简单adb shell dd if/dev/block/by-name/boot of/sdcard/boot_backup.img adb pull /sdcard/boot_backup.img我见过太多人跳过这步最后只能在变砖边缘挣扎。刷机有风险备份是唯一能让你全身而退的保险。4. 安全加固用攻击者的思路反推改造清单4.1 攻击面分析漏洞往往在边缘驱动里提到内核安全很多人第一反应是iPhone 越狱、内核漏洞利用但实际的安全工作远没有那么热血。真实的攻击面分布在那些不起眼的角落ioctl 没有校验用户传入的指针、驱动对传入的缓冲区长度没有做边界检查、某个 netlink 命令在特定版本下可以越权访问。热搜词里的 exploiting kernel 就是从攻击者视角找可利用入口ARKM KERNEL 的防御思路正好相反我们把攻击者最常走的入口封掉。以安卓设备为例最高危的攻击面其实不是 CPU 架构漏洞而是 vendor 驱动的 ioctl 接口。这些接口由高通和 ODM 各自维护代码质量参差不齐而且往往没有经过严格的安全审计。抢在攻击者利用之前需要对这些 ioctl 做白名单化处理。具体操作上我们给 camera 和 GPU 的关键 ioctl 增加了额外的权限校验确保只有具有特定 SELinux domain 的进程才能调用。这个不是靠内核自身的权限位那只是 uid/gid 层面的控制而是依靠 LSM hook 在security_file_ioctl层做二次检查。从整体安全设计来看就是即便进程拿到了文件句柄它的 SELinux 权限标签本身就不允许调用这类 ioctl。4.2 PAN/KASLR/CFI 的配置落地ARM64 平台有一些基础但必须开启的加固特性很多人以为安卓内核算默开启实际上部分 vendor defconfig 为了兼容老驱动会偷偷关掉。ARKM KERNEL 在 defconfig 里强制锁死几项CONFIG_ARM64_PANy CONFIG_ARM64_UAOy CONFIG_RANDOMIZE_BASEy CONFIG_RANDOMIZE_MODULE_REGION_FULLy CONFIG_CFI_CLANGyCONFIG_ARM64_PAN的作用是阻止内核在任意时刻直接访问用户态内存必须显式通过特定指令来访问这能挡住很大一类用户态指针注入利用手法。CONFIG_RANDOMIZE_BASE是内核地址空间布局随机化KASLR让攻击者无法预测关键函数地址。CONFIG_CFI_CLANG是 Clang 的间接调用完整性检查函数指针在调用前会先验证其类型签名能有效阻断很多 function pointer overwrite 类利用。但开启 CFI 不是没有代价。最明显的影响是性能回退大概在 2% 到 5% 之间尤其在热路径上syscall 密集、网络收发、文件系统操作。而且CFI 对驱动代码的要求很高如果有哪个驱动是手工汇编写的、或者用了非标准的函数指针跳转开启后就会直接触发 panic。ARKM KERNEL 在开启 CFI 后光是适配驱动代码就花了一周多时间很多厂商驱动从来没有人对齐过 CFI 的校验格式只能在__cfi_check报错日志里一个个定位。另外module加载也需要额外注意。KASLR 开启后内核模块的加载地址会被随机化部分闭源驱动会在加载时进行地址计算如果它们硬编码了某个固定地址就会加载失败。我们首版干脆做成了不加载任何第三方内核模块的策略所有功能编译进内核。这也是一种安全设计——少一个模块加载接口就少一堆潜在风险。4.3 SELinux 与 vendor hooks 的边界约束安卓的 SELinux 是系统安全的核心边界工具。内核本身只负责执行 LSM 勾子真正的策略定义在用户态。ARKM KERNEL 和普通内核不同的地方在于我们修改了一些平台默认的 sepolicy 规则让内核暴露给用户态的procfs和sysfs节点更难被滥用。最常见的问题是/proc/下的信息泄露。默认配置可能允许普通 App 读取/proc/interrupts这看起来无害但攻击者能通过中断频率推断其它 App 是否在运行从而建立用户行为侧信道。我们在 sepolicy 中把这类节点的读取权限收窄到系统级 domain。另外一个常见点是 vendor 的/sys/kernel/tracing/节点默认可能允许某些特权 domain 直接读写这块如果被利用攻击者能够直接修改内核 trace 配置甚至把 trace 事件注入到任意进程。ARKM 的策略是把 tracefs 的挂载权限限定为 shell 和 system_app普通 App 完全没有访问能力。高通的 vendor hooks 存在一份复杂的vendor_hooks.c文件里面定义了各种 traceable hooks 的函数指针。这些 hook 原本是给厂商做性能分析用的但它们同时也是一种间接调用风险。我们的做法不是删除这些 hook删掉会影响厂商用户态功能而是通过 CFI 把它们纳入保护范围并在关键 hooks 上加上特定 function trace 前缀方便审计时快速定位调用来源。5. 内核调试的野路子与正规军5.1 pstore 与串口日志崩溃现场的考古学内核 panic 之后最着急的事就是搞清楚到底死在哪。现代设备基本没有串口外露所以 pstore 是救命稻草。pstore是内核用来在死机时把日志写到 RAM 特定区域的机制重启后内核从该区域读回日志并暴露给用户态。打开 pstore 需要配置CONFIG_PSTOREy CONFIG_PSTORE_RAMy CONFIG_RAMOOPSy RAMOOPS_RECORD_SIZE0x40000还要在设备树里给 ramoops 预留内存区域这部分各平台不一样。调试 ARKM 的过程中pstore 帮我解决了至少三次诡异的启动崩溃。崩溃后不要急着拔线让设备重启完成然后adb shell ls /sys/fs/pstore/ adb shell cat /sys/fs/pstore/dmesg-ramoops-0 crash.log这个crash.log就是崩溃现场的考古报告里面会有 panic 时的寄存器、调用栈、以及最后输出的核心日志。如果手头有工程机的串口接口那体验会好得多。高通平台的串口日志通常通过 USB 转串口线接出使用 minicom 或 picocom 以 115200-8N1 方式连接可以在崩溃发生的第一秒就拿到完整现场。不过串口输出在高负载场景下会拖慢系统所以量产机上我们手动禁用了串口 console只保留 pstore。5.2 死机之后如何重建调用栈拿到 pstore 的日志后最常看到的是一堆十六进制地址直接看根本不知道是哪个函数崩了。需要借助vmlinux把地址翻译回函数名。编译时一定要保留vmlinux和 System.mapmake Oout Image.gz dtbs vmlinux崩溃日志中的函数名一般会自动显示如果内核开启了CONFIG_KALLSYMSy但如果被 KASLR 随机化干扰显示的地址和实际符号偏移会不一致。KASLR 导致的问题在早期很误导人你可能看到崩溃点在完全不相关的函数上。处理方式是先在内核日志里找Kernel Offset比如Kernel Offset: 0x39400000 from 0xffffffc000000000然后在使用llvm-addr2line或aarch64-linux-gnu-addr2line转换地址时提前减去这个偏移量才能对应到正确的源码行。这事我踩了两次坑浪费了两天时间现在把它写进团队的调试手册里任何人看到 panic 地址时先做 KASLR 偏移计算再开始分析。调试好了的调用栈通常会把问题定位到某个驱动文件。接下来就是用git bisect的方式去查哪一次改动引入了 bug。ARKM KERNEL 在 gki 和 CAF 的基础上加了大量补丁如果出现回归我会先二分法测试最近的 10~20 个 commit在第三次 panic 时就能锁定问题 commit。5.3 常见构建错误的快速判断表构建内核的时候有一批高频报错我整理成了一张速查表遇到问题可以先对照再搜报错现象可能原因解决办法ld.lld: error: undefined symbol: ktime_get_raw_ns依赖的某个驱动没有选择检查.config里对应的 configmake ARCHarm64 menuconfig搜索该符号所属选项fatal error: linux/module.h: No such file or directory错误设置了交叉编译头文件路径重新 exportARCH和CROSS_COMPILEmake mrproper后重来error: expected ) before COMPAT_SYSCALL_DEFINE编译环境 glibc 过旧升级构建容器到 Ubuntu 22.04 以上或使用linux-libc-dev的 current 版本no rule to make target arch/arm64/boot/dts/vendor/xxx.dtsi拉取仓库不完整检查是否用了git lfs或漏掉了 dtbo 子模块CONFIG_CFI_CLANGy后编译虽过但启动即 panic某个驱动函数指针类型不对齐用 pstore 抓 panic 点找到__cfi_check失败的函数修改该驱动的函数签名定义这份表格是踩了无数坑换来的每一条后面几乎都有一晚上的折腾。排查构建错误时我的建议永远是先定位到哪一步开始失败的再去找对应的报错避免被最后一步的大段错误带偏。6. 关于首版发布我目前的计划和预期6.1 首版覆盖的设备与功能边界ARKM KERNEL 的首版不会铺开支持所有设备这既不现实也不负责任。目前计划优先支持 SM8250 平台骁龙 865/870 系列和 SM8350 平台骁龙 888 系列这两代设备存量够大社区活跃度高反馈迭代快。每个平台只锁定一款参考设备作为主测机型其它型号即使能刷也只给出可尝试但不保证稳定的说明。首版的功能边界如下支持 DWS 调度权重默认开启但系统级参数可以通过内核 cmdline 关闭方便比较。支持 KASLR、PAN、CFI 全开不接受以性能为由关闭安全特性的请求。支持restorecon和动态 sepolicy 加载但不支持用户自己松绑安全策略——这违背 ARKM 的出发点。不支持任何第三方 kernel module 加载需要扩展功能请提交源码一起编译。6.2 参与协作的方式如果你对 ARKM KERNEL 感兴趣现在最需要的是三类贡献源码级别的补丁、真实设备上的回归测试、以及安全审计报告。项目代码会放在公开仓库构建脚本和刷入包生成流程会在首个 release 时一并提供。内核开发是一件非常需要耐心和严谨的事情ARKM KERNEL 之所以敢说 approaching you soon是因为团队内部已经经过了接近三个月的开发、测试、推倒重来。稳定是发布的第一要求宁可晚一周也不交一个会导致用户数据损坏的版本。我个人在这几次内核调试中最大的体会是内核开发没有银弹每一个优化都需要在日志、性能数据和用户反馈之间反复权衡。一个参数的修改、一个 config 的开关、一个驱动代码的调整背后都是几小时的验证。ARKM KERNEL 能推进到今天靠的不是什么高超的技巧而是把每一步做完、做扎实的笨功夫。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进