安卓上 KVM 虚拟机运行 Linux/WoA

 

上一篇博客创作的过程中, 我意外发现我目前的主力机 Redmi Note 10 Pro 5G (chopin) 居然有 KVM 支持.

于是决定不再纸上谈兵, 开始了实战之旅, 尝试在我的手机上跑跑 KVM 虚拟机(我的手机又遭殃了).

本篇博客可能较长, 应该会包含以下内容:

  1. 目前安卓手机上 KVM 的现况
  2. ARM SoC 启动流程以及特权等级
  3. 我是如何为我的 chopin 移植 OSS 内核并运行最新类原生安卓的
  4. chopin 上实际使用 Qemu 启动 Linux/WoA

移动设备上的 KVM

Pixel 6 及以上

Pixel 6 全系 (包括 Pixel 6, Pixel 6a, Pixel 6Pro) 及以上的 Pixel 有谷歌官方提供的 KVM 支持, 有篇文章给出了较为详细的介绍: https://lwn.net/Articles/836693/

开始我看到这个消息还是挺激动的, 毕竟也算是第一个官方支持的 KVM, 还是亲儿子系列, 对其他厂商多少有些促进作用, 难不成将来谷歌还想推出 ASL (指 Android Subsystem for Linux)(x) 的玩法?

后来仔细一看, 发现 Pixel 默认支持的是 “protected” KVM, 与常见的 nVHE (native Virtualization Host Extension) 不同, “protected” 虚拟机意味着虚拟机内部的状态受到保护, 连 Host 都无法修改. 虽说有个内核参数可以关闭 protected KVM 切换到 nVHE, 但这显然需要 OEM 解锁(同时破坏 hardware attestation).

嗅觉敏锐的你肯定已经发现了: Pixel 系列上的 pKVM 支持, 不过是谷歌作为世界上最大的广告公司, 打着”保护设备安全”的幌子, 保护 DRM 的又一邪恶计划.

目前唯一的好消息大概是, 谷歌目前还没有加入 pKVM 支持后的进一步动作, 但 KVM 支持已经实际存在, 手头上有 Pixel 系列的可以尝试使用 KVM 运行 Linux / Windows on Arm, 并且网上已有一些成功的案例供参考(如 Limbo Tensor).

Mediatek 天玑 1100/1200 等 SoC

非常幸运的, 我发现我的主力机 chopin 使用的 SoC 是天玑 1100, 天玑 1100 附近的一些 SoC (已知的包括天玑 700/720/800u/810/1100/1200) 是有 KVM 支持的.

要解释这些 SoC 为什么有 KVM 支持, 其实要从为什么大部分高通/联发科设备没有 KVM 支持说起…

在 ARM 的世界里, 与 x86 类似的, 有几个不同的”特权等级” (ARM 中叫 Exception Level, 简称 EL), 是 EL0 到 EL3.

但与 x86 中内核运行在最高的 ring0 不同, ARM 启动时, 厂商的固件(Firmware) 会启动并占据最高的 EL3.

根据 ARM 的文档, 特权最高的 EL3 为固件, EL2 为 Hypervisor, EL1 才是操作系统内核, EL0 为普通应用.

ARM 的 Exception LevelARM 的 Exception Level

所以 ARM 的设计上, 本身已经包含了虚拟化的硬件支持, 不过我们的 Linux 内核需要 运行在 EL2 或来自 EL2 的支持才能得到足够的权限, 启用 KVM 功能.

p.s. 以下的”固件”一词, 均指正常引导流程中, 在kernel得到控制权前, 运行的所有 OEM 代码.

在正常的启动流程中, 来自厂商的固件(Firmware)主要完成硬件的初始化, 读取设备树和内核放置在内存中, 最后将控制权转交给 kernel. (具体流程见 kernel 文档)

在现在主流的移动设备上, 厂商的固件调用内核时, 已经主动将特权等级降到了 EL1, 相当于占据了 EL2 这层, 但完全没有实现任何功能, 也没有提供回到 EL2 的接口, 直接导致在手机上我们无法使用 KVM 功能. (其实就类似于 BIOS 里主动禁用了虚拟化支持.)

而且现在的移动设备在出厂时也都已经启用了对固件的签名检查(例如高通的 Secure Boot), OEM 生产手机时, 会一次性不可逆地将自己的公钥写入 SoC, 此后该 SoC 就只能启动经过 OEM 签名的固件. 这是保证整体启动流程可信的一个重要步骤, 但间接导致了我们无法对 OEM 的固件进行任何修改, 在 OEM 固件没提供接口的情况下, 我们也无法手动启用 KVM.

目前的状况下, 只要厂商密钥没泄露, SoC没被发现可以用于绕过 secboot 的漏洞, 我们只能通过硬改手机, 更换一个仍未烧录公钥的 SoC 来启动自定义的固件, 从而让固件让出 EL2, 使得 kernel 能运行在 EL2 上. (其实换未熔丝的 SoC 也是很多硬改解 BL 的原理.)


不过凡事总有例外… 我手头的这台 chopin 使用的 MTK 1100 就是一个例外.

在经过小米官方签名的固件中, 不知道出于什么原因, 在固件启动完成跳转到内核后, SoC 仍处于 EL2 等级. 考虑到后续的 SoC 中这个”特性”被移除, 之前的 SoC 也没有相关特性, 这可能是 Mediatek 官方给各个 OEM 提供的固件代码中遗漏了降到 EL1 这一步骤, 导致使用上述相关 SoC 的机型都天生默认”开启”了 KVM.

既然这么幸运地得到了一台天生支持 KVM 的安卓机, 当然要来尝试下在上面运行下虚拟机~

移植内核

显然, MIUI 系统的预构建内核中不会包含 KVM 模块支持. 我需要自己构建一个有 KVM 支持的内核.

较新版本的安卓(出厂安卓12及以上, 内核版本>=5.10)已经(被谷歌要求)采用了 GKI 技术(通用内核映像), 与 GSI 共同致力于解决安卓内核和系统领域的碎片化问题. 支持 GKI 的手机厂商驱动以内核模块的形式存在, 内核本体不包含厂商的内容, 只需要拉取安卓通用内核的源码即可自行编译替换原有内核.

然而… chopin 的内核版本是 4.14, 没法享受到 GKI 的优势, 搜索了一下也没找到较新的内核, 唯一一个 GitHub 上的开源内核只支持安卓 11.

(偷懒)尝试直接开 GSI

GitHub 上的现成的安卓 11 内核可以构建后加入 KVM 支持, 但是 MIUI 12.5 真的不太好用, 且 MIUI 12.5 的底包 HAL 实现有诸多 bug, 我尝试了很多个 GSI 都处于不可用的状态.

试了一大堆类原生全都不行试了一大堆类原生全都不行

(被迫)移植内核

显然我不能接受安卓 11 如此古老的安卓版本, 也很不想使用 MIUI.

所以我开始了自己移植新版安卓内核的旅程. 通过搜索, 发现小米出过的 Redmi K40 Gaming (代号 ares) 和 chopin 的配置比较类似, 且有现成的安卓 12 开源内核. 于是开始着手将 ares 的安卓内核移植到 chopin.

移植内核基本就是一个见招拆招的过程, 直接把旧的 defconfig 搬过来之后开始编译并解决各类问题:

  • 配环境
    编译内核的第一步是万恶的配环境. 不同的内核可能需要不同的编译器, 并且可能不是官方版本而是奇怪的 fork, 较新的安卓内核已经都开始使用 Clang 构建, 较旧的还在使用 gcc. 在缺乏文档的情况下, 这一步可能需要一些猜测和尝试.

  • 编译错误

    • 缺少文件: 搜索相关驱动, 复制过来或者直接编辑配置禁用相关功能
    • 缺少定义: 编辑配置启用缺少的功能, 或直接(合理地)修改代码删除不存在的变量或定义
    • 警告报错: 部分不重要的编译警告可以忽略, 可以调整编译选项忽略
  • 链接错误

    • 符号冲突: 相关符号改 static 或者手动改名
    • 符号缺失: 搜索相关符号有关模块, 修改配置启动缺失的模块或手动修改代码
  • 运行时错误
    第一次成功编译出的内核很可能不能完全正常运行. 我第一次编译出的内核虽然能开机, 但是电池, USB, 震动三个功能不工作.
    安卓内核的 dmesg 信息普遍非常混乱, 存在一堆 Red herring 干扰判断.(例如满天飞的 Error 但是完全不影响功能, 且使用官方 kernel 其实也有同样的报错)
    这里我发现的小技巧是: 对比自己构建的内核官方内核日志的差别.
    不用关注内核里满天飞的 Error, 主要看相关的日志有没有什么不同之处, 再通过反向搜索可以找到内核中相关的模块, 对比差异调整配置项或代码, 一般就可能解决问题. 如果有驱动不同的话, 也可以尝试手动移植其他版本的驱动.

最后我构建的 内核源码Actions 构建环境 都开源了, 不过除非恰好你和我使用同一款手机, 否则这里的构建产物可能对你的帮助不是很大. 整体构建过程不是很难, 但比较繁琐, 需要反复在手机实机上测试…


在一些波折之后, 我还是成功构建出了一个没有 BUG 的内核, 并且 MIUI13 (安卓 12) 底包中 HAL 的 Bug 明显减少了, GSI 安装后大部分功能可以正常使用. 既然已经删除了原系统, 就顺水推舟, 把安卓版本升到了最新的 14.

安卓GSI14+新鲜出炉的内核安卓GSI14+新鲜出炉的内核

能自己编译出内核后, 加入一些配置项就可以启用 KVM. 这里还有个小曲折: MIUI 13 刷机包的固件已经会降级到 EL1 了. 所以我先刷入了完整的 MIUI 13 后, 还需要手动刷入旧版 MIUI 12 的 preloader.bin 文件(谢天谢地, 它们俩是兼容的), 才能最终运行在 EL2, 在系统中获得 KVM.

几经波折, 终于成功了, 此时现实时间已经过去一个多礼拜了...几经波折, 终于成功了, 此时现实时间已经过去一个多礼拜了...

QEMU, 启动!

(十分顺利的) Linux

在 KVM 加速下运行 Linux 十分的顺利, 下载 Ubuntu 官方的 ARM 安装镜像, 直接在 Limbo Tensor 中配置 ISO 和磁盘即可直接启动, 安装过程也十分顺利, 一次成功.

Ubuntu 24.04 运行流畅, 占用还算挺低的Ubuntu 24.04 运行流畅, 占用还算挺低的

唯一的一个小问题是, 在高内存占用的情况下, KVM 可能会报错 Bad Address 退出. 这应该是由于安卓的 zRAM 机制和 KVM 不兼容导致的, 暂时使用 swapoff /dev/block/zram0 关闭 zRAM 即可.

(曲折的) Windows

当我尝试使用同样的方法运行 Windows 11 on Arm 时, 启动流程直接卡住了… 尝试按照 Limbo Tensor 的说明启用 Full UEFI 后, QEMU 干脆直接报错 Function not implemented 退出了, 此时 dmesg 中报错 load/store instruction decoding not implemented.

报错大概类似下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@localhost:~/roms# ./bin/qemu-system-aarch64 -display vnc=:0 -device qemu-xhci,id=usb-bus -device usb-tablet,bus=usb-bus.0 -device usb-kbd,bus=usb-bus.0 -L . -smp 4 -M virt -cpu host -m 3072 -drive if=pflash,format=raw,unit=0,file=./edk2/edk2_qemu_aarch64.fd,readonly=on -drive if=pflash,format=raw,unit=1,file=./edk2/edk2_vars.fd -device nvme,drive=drive0,serial=drive0 -drive if=none,media=disk,id=drive0,file=/mnt/android/tiny11arm64.qcow2 -net none -device virtio-ramfb -accel kvm -machine kernel_irqchip=off -s -S
error: kvm run failed Function not implemented
PC=0000000000004c28 X00=0000000000000001 X01=0000000040080000
X02=0000000000000004 X03=0000000030d0198d X04=00ff000000008000
X05=0000000080000001 X06=0000000000000018 X07=0000000000000016
X08=000000004007c000 X09=5aa55aa55aa55aa5 X10=0000000000000000
X11=0000000000000000 X12=0000000000000000 X13=0000000000000000
X14=0000000000000000 X15=0000000000000000 X16=0000000000000000
X17=0000000000000000 X18=0000000000000000 X19=0000000000000000
X20=0000000000000000 X21=0000000000000000 X22=0000000000000000
X23=0000000000000000 X24=0000000000000000 X25=0000000000000000
X26=0000000000000000 X27=0000000000000000 X28=0000000000000000
X29=0000000000000000 X30=0000000000004ba8 SP=0000000040080000
PSTATE=600003c5 -ZC- EL1h

搜索后发现似乎是当前版本的 kernel KVM 实现的不是十分完整… 在 BIOS 使用 MMIO 写入 nvram 会直接导致 KVM 出错.

于是继续尝试在不启用 Full UEFI 的情况下启动 Windows, 转而着手来解决 Windows 启动卡死的问题.

使用 GDB 挂到 QEMU 上, 运行到卡死后 ^C 发现停在了一个死循环上, 前后的指令却都看起来不太对劲… 初步推测, 可能是在等待中断, 却因为某些原因一直没有收到需要的中断, 所以启动过程就卡住了. 此时 QEMU 和 dmesg 中均没有发现任何错误信息.

1
2
3
4
5
6
7
8
9
10
11
(gdb) x/10i $pc-20
0xfffff8007b69829c: ldr x9, [x8, #2400]
0xfffff8007b6982a0: bl 0xfffff8007b40ce60
0xfffff8007b6982a4: bl 0xfffff8007b6983b0
0xfffff8007b6982a8: brk #0xf000
0xfffff8007b6982ac: udf #0
=> 0xfffff8007b6982b0: b 0xfffff8007b6982b0
0xfffff8007b6982b4: nop
0xfffff8007b6982b8: nop
0xfffff8007b6982bc: udf #0
0xfffff8007b6982c0: pacibsp

我的初步设想是, 把一个相对较新版本 kernel 中的 KVM 模块 backport 回当前 4.14 的内核, 这样应该能修好目前 kernel 中 MMIO 和 IRQ 相关的问题.

backport 过程中遇到了不少问题, 主要是 KVM 中的新 feature 可能和内核其他模块也有较多关联, 就产生了一大堆宏/变量/函数未定义的问题, 要一点点动手修.

修到一半, 搜索 KVM 和 QEMU 相关资料的时候, 在 QEMU 网站上 发现 -machine kernel_irqchip=off 这么个选项, 似乎可以使用 KVM 加速的同时在用户态模拟 Interrupt Controller. 我尝试了这个选项, 惊喜地发现 Windows 11 直接启动成功了~ (所以 backport 计划暂时鸽了)

终于... MTK 上硬件加速的 Windows on ARM终于... MTK 上硬件加速的 Windows on ARM

上图是我第一个开的 Tiny11, 不过似乎在我的手机上运行有些 Bug, 一开机就占用了 3GB 内存, 而且有一堆 WerFault 进程占满了 CPU. 还发现之前忽略的一个问题: QEMU 参数给了 -smp 4 会分配四个 vCPU, 每个 1 核心, 然而普通的 Windows 不支持那么多 CPU, 所以实际上这个 Windows 在单核运行…

于是着手重新安装了一份 Windows 10 on ARM, 调整了 -smp cores=4 参数, 同时手动指定了 CPU 亲和性把 QEMU 绑定到大核运行, 最后安装了 virtio 网卡驱动, 启动了端口转发和 RDP 访问. 在我 8GB 的手机上, 基本只能分配 4~5GB 的 RAM, 分配更多 RAM 安卓容易因为 OOM 杀掉 QEMU 或直接卡死.

1
2
# 最后使用的启动指令, CPU4-7 在我的手机上是大核
taskset -c 4-7 ./bin/qemu-system-aarch64 -display vnc=:0 -device qemu-xhci,id=usb-bus -device usb-tablet,bus=usb-bus.0 -device usb-kbd,bus=usb-bus.0 -L . -smp cores=4 -M virt -cpu host -m 4096 -drive if=pflash,format=raw,unit=0,file=./edk2/edk2_qemu_aarch64_nonvram.fd,readonly=on -drive if=pflash,format=raw,unit=1,file=./edk2/edk2_vars.fd -device nvme,drive=drive0,serial=drive0,bootindex=0 -drive if=none,media=disk,id=drive0,file=/mnt/android/win10_21390.qcow2 -device virtio-ramfb -net user,hostfwd=tcp::3389-:3389 -net nic,model=virtio -accel kvm -machine kernel_irqchip=off

具体体验录了一段视频在此. 实际性能测试单核比骁龙 860 强, 和 Microsoft SQ2 接近, 因为只分配了 4 核心所以多核弱一些, 不过我觉得基本达到了”可用”水平.

跑分结果, 单核基本接近 Microsoft SQ2, 使用体验还不错跑分结果, 单核基本接近 Microsoft SQ2, 使用体验还不错

即使无法使用 GPU, 也能还算流畅地浏览网页即使无法使用 GPU, 也能还算流畅地浏览网页

小结

每次折腾安卓都是个费时费力的活, 这次零零总总搞了两个礼拜不止, 清理硬盘的时候发现下载下来的各类系统镜像堆起来都 >200GB 了… 把相关的文件删了大多数用不到的, 整理了一下传到了我的网盘.

虽然搞了半天, 得到的这些成果也并不”通用”, 只能用于 chopin 这一台设备, 其他一些少数 MTK 的设备需要重新编译自己的内核才能跑 KVM, 而所有的高通手机和大多数不能进入 EL2 的联发科手机则根本不可能复现本文的 KVM.

但这应该是第一个在联发科设备上, 以接近原生性能运行的 WoA, 这实在是太酷了.

本文采用 CC BY-NC-SA 4.0 许可协议发布.

作者: lyc8503, 文章链接: https://blog.lyc8503.net/post/android-kvm-on-mediatek/
如果本文给你带来了帮助或让你觉得有趣, 可以考虑赞助我¬_¬