1. 不是点灯的Arduino先把Uno Q的定位说清楚最近整个人泡在一个有点“反常识”的项目里给手头这块Arduino Uno Q打补丁修改Qualcomm QHEE在启动阶段的行为最终目的是让KVM在这块开发板上真正跑起来。说实话如果是三年前我不会相信Arduino这个名字会和KVM出现在同一句话里但Arduino Uno Q不是传统意义上那类AVR单片机它内部插的是一颗带多个Cortex-A核心的高通SoC整体更像一台袖珍Linux电脑Arduino只是包了一张耳熟能详的脸。这个项目的起因很简单我想在Uno Q上同时跑两个相互隔离的系统一个负责实时采集传感器数据另一个负责跑网络服务。在嵌入式Linux领域最省事的隔离方案就是KVM加虚拟机。但一上手就发现板子自带的引导固件在安全世界固件初始化时把非安全世界的虚拟化扩展锁住了Linux内核进不了EL2KVM自然无法初始化。于是正常的“编译一个开启KVM的内核”这条路线直接走死剩下必须研究的就是标题里写的那件事Patching Qualcomm QHEE。这篇内容适合谁看如果你是玩树莓派、DragonBoard这类Linux开发板已经觉得没什么挑战想进一步接触高通平台启动链、TrustZone、EL2虚拟化的人这篇文章能帮你少走很多弯路。我尽量把每一步说清楚包括固件在哪里、为什么改这个地方、改了以后怎么刷回去不会变砖。2. 底层机制QHEE、ARM异常级别与KVM的三角关系2.1 QHEE到底是什么为什么它如此关键QHEE的全称是Qualcomm Hypervisor Execution Environment是高通平台安全世界里最早启动的组件之一。刷过其他高通设备的同学应该知道PBL、XBL、ABL这套启动链QHEE的定位比它们更底下它工作在安全侧负责初始化TrustZone相关资源验证后续镜像的签名并且管理安全内存、密钥、指纹等敏感外设的访问。换个生活化的说法QHEE相当于这个系统里的“保安队长兼影音室管理员”。它不管你的用户程序怎么跑但它决定谁能进EL3、谁能碰安全定时器、谁有资格给其他固件签名。更关键的是在高通某些平台上QHEE初始化时要决定非安全世界能不能使用ARM虚拟化相关寄存器。如果它在启动配置里把某个开关设成了0那么即使你的Linux内核编译选项全部正确运行到KVM初始化时也会直接报错退出。在Uno Q这款板子上QHEE存放在专门的qhee分区里与XBL、ABL分区并列。厂商出厂固件为了稳定和安全默认把非安全世界的EL2访问权限关掉了。整块板子不是不能用跑普通Linux没问题但你想用KVM就必须让QHEE在启动时“松手”。2.2 KVM运行需要满足的三个硬条件很多人以为KVM只要内核模块编译进去就能用这在高通平台上是错的。ARM64架构下KVM要跑起来至少需要三个条件同时成立。第一CPU必须暴露虚拟化扩展也就是ID_AA64PFR0_EL1寄存器里显示支持EL2并且当前引导状态确实允许接下来运行的代码进入EL2。第二Linux内核要么直接从EL2启动要么通过一个Hypervisor跳板进入EL2因为KVM本身需要在EL2安装Hyp异常向量表。第三内存管理单元和中断控制器要正确配置不能出现安全世界把关键Interrupt Controller寄存器锁死的情况。这三条里前两条和QHEE直接相关。如果QHEE在早期启动时把CPU停在EL1后续Bootloader也没想办法抬高特权级那Linux就永远只能在EL1里当普通Guest OSKVM模块即使加载也只会打印一行“KVM/arm64: Failed to initialize hypervisor”然后退出。2.3 启动链视角QHEE手里握着哪些开关结合Uno Q的实际启动流程来看整个链条大致是固化在芯片内部的PBL先运行加载XBLXBL完成DDR初始化后把签名验证通过的QHEE带进安全内存QHEE跑起来后再引导ABL由ABL把Linux内核加载进内存。整个过程中QHEE有权限决定ABL运行在什么异常级别也有权限决定非安全侧的虚拟化扩展是否可用。所以“给QHEE打补丁”本质上是修改一个启动早期的安全固件让它不再对非安全世界的EL2访问做限制。这比单纯改内核配置要麻烦因为QHEE是签名的而且直接修改会破坏校验。但好在工程样片/开发者版本的Uno Q支持调试签名我们可以用高通EDL模式读回分区、修改后再刷入只要签名的Key匹配校验就能通过。3. 准备阶段固件提取、工具链和风险控制3.1 接线、串口日志、进入EDL 9008开工之前最要紧的是把调试环境搭好否则中途变砖连救的机会都没有。我用的连接方式是USB-C数据线加USB转TTL串口。Uno Q把调试串口引在了板子边缘的排针上还有一个隐藏的下载按钮短按后插上USB线在电脑设备管理器里会出现“Qualcomm HS-USB QDLoader 9008”设备这就是高通EDL模式。在EDL模式下可以通过QFIL、QDL或一些开源Python工具直接读写分区表。注意EDL模式不是给你随便玩的正常启动的时候只有高通签名工具才能和它通信但开发者版机器有调试授权允许使用特定的Firehose配置。读取分区之前一定先把分区表完整备份下来我习惯把每个分区都dd一份特别是qhee、xbl、abl这三分后续恢复镜像全靠它们。串口日志也很重要。我接好TTL线后用minicom以115200波特率监视启动输出。如果QHEE校验失败串口上会明确打印类似“Authentication failed”或者“rollback failure”的字段不同版本号的固件提示还不完全一样。建议整个项目期间保持串口一直开着刷完机不等重启提示直接看日志很多问题日志比猜测更快暴露。3.2 三种拿到qhee分区的方式我先梳理一下能拿到qhee分区镜像的路径按可靠程度排序。第一种从官方开发者固件包里解包。Arduino四川的固件发布包通常包含了所有分区镜像qhee.img或者qhee.mbn就在里面。这种最省事镜像干净不会有人为污染。第二种在系统内通过root权限读取分区节点例如/dev/block/bootdevice/by-name/qhee用dd命令直接导出。但这个前提是Linux已经能跑起来而且你有root如果QHEE卡在早期启动连内核都进不去这条路就走不通。第三种进入EDL模式后用Firehose工具读取这也是变砖后唯一的选择。我推荐大家优先用第一种和第三种配合先解包拿原厂镜像做分析刷坏了再用EDL模式还原。纯靠EDL抓镜像会比较痛苦因为Firehose的XML配置要自己核对分区大小一旦搞错就把别的地方覆写了。3.3 静态分析工具推荐QHEE固件的格式在不同平台上差别不小但这几年主流是高通MBN转ELF也就是在MBN头部里内嵌一个ELF文件。解包工具方面我推荐binwalk做初步扫描再用Ghidra做反汇编。IDA当然也可以但Ghidra免费对ARM64的支持这些年已经很成熟。另外需要准备一个十六进制编辑器比如Hex Workshop或010 Editor。因为有些修改只改两个字节用Ghidra改完导出的二进制有时候会损坏原有布局不如先在Ghidra里找到偏移再在010 Editor里做精确写入。补丁管理我建议用git虽然git本身不是为二进制设计的但你可以把修改前后的镜像、补丁脚本和分析记录都扔进同一个仓库每次改动用git diff查看十六进制文件变化。项目后期需要回滚某个修改时这个习惯会救你一命。4. 打补丁的整体流程从Ghidra到Fastboot4.1 解析QHEE镜像格式拿到qhee.mbn之后第一步是用binwalk确认格式。常见结果是头部有“EBD0”魔数的高通MB N格式里面包着一个AArch64 ELF。我习惯用自定义脚本把MBN头剥掉直接导出内部ELF然后用Ghidra的“Import ELF”加载。看ELF段信息时特别留意.text和.rodata的位置启动逻辑一般在.text字符串和配置标志在.rodata。如果镜像包含符号表那工作会轻松非常多即使被strip掉QHEE里通常也会保留一部分与高通BootChain相关的字符串可以通过引用找函数边界。加载完以后我用“auto analysis”跑一遍然后从入口点entry point开始看。QHEE入口通常是一段初始化CPU、建立异常向量表的代码不需要完全弄懂只需要找到后面关键分支。4.2 定位关键判定逻辑补丁点怎么找是我这次项目花时间最多的地方。通过反复看启动日志和反汇编我发现QHEE在初始化非安全世界之前会调用一个内部函数读取平台上关于“virt_enable”的配置值然后根据结果决定是否向非安全侧开放EL2能力。函数内能看到类似“ldr w8, [x0, #0x28]”或者“tbz w8, #5, loc_xxx”的模式。在默认固件里这个分支会跳到关闭虚拟化的路径把某个MMIO寄存器清零并把系统MMU配置成非安全世界只能使用EL1。我们要做的就是把这个跳转条件反过来让“开启”路径执行跳过“关闭”路径。具体修改要看反汇编结果。我这次遇到的指令模式比较简单ldr w8, [x0, #0x28] and w8, w8, #0x20 cbz w8, disable_virt意思是当配置位的bit5为0时跳转到关闭虚拟化逻辑。那最简单的补丁就是把cbz改成cbnz或者直接把and那一条指令改成“mov w8, #0x20”让条件永远为真。前者只改一个条件码后者需要修改两条指令但更直观。我先试了改条件码的方式只修改2个字节。在Ghidra里记下指令的文件偏移用010 Editor打开原MBN按MB N头的偏移换算到内部ELF的虚拟地址最终算出物理偏移。改完以后用readelf重新加载确认那个位置的指令变成目标指令。4.3 写入补丁与重新打包修改只是开始重打包和绕过校验才是真正容易踩坑的地方。开发者版Uno Q使用的是高通“engineering secure boot”模式支持测试签名。那我只需要把修改后的QHEE重新计算分区大小填充到新的MB N镜像里再用签名工具对镜像做哈希。不同平台重打包脚本差异很大我这里是参照Arduino官方提供的FlashAll脚本改的里面有调用pil-splitter.py和QComSignTool的示例。流程大致是准备一个工作目录放原始qhee.mbn。运行pil-splitter.py把MB N拆成ELF和元数据。对ELF做所需字节修改。运行签名工具指定设备对应的debug证书和私钥生成新的MB N。把新MB N文件按原命名放回固件包。这里有个经验QComSignTool输出里会显示“hash: xxxx”记录下来刷机后如果启动日志里的hash和这个对不上说明刷入时分区错了或者Fastboot没有真正写入。4.4 刷机与验证刷QHEE有两种路径一种是在Linux系统里用fastboot主动刷入另一种是在EDL模式下用Firehose刷入。如果系统还能启动fastboot更安全因为fastboot会检查你刷的分区名是否存在不会误刷成别的分区。执行前先备份fastboot getvar current-slot fastboot fetch qhee qhee_backup.img fastboot flash qhee qhee_patched.mbn fastboot reboot如果是EDL模式则需要Firehose配置文件我习惯先把配置里的program命令人工读一遍确认每次写入的LBA地址和大小都对应qhee分区再执行。刷完以后观察串口如果打印正常进入ABL并拉起内核说明签名校验通过了。如果看到“Image authentication failed”赶紧检查签名证书是否匹配如果看到“XBL is already done”则可能是分区名没对应上多半是刷到xbl或者abl分区了。5. KVM启用实操内核配置、引导参数与第一台虚拟机5.1 编译内核四板斧QHEE补丁生效只是第一步Linux内核本身也必须开启虚拟化支持。我以6.1内核为例在Uno Q的BSP内核代码上重新编译。重点配置项有四个方向KVM模块本身、ARM虚拟化支持、IOMMU直通、以及virtio驱动。CONFIG_KVMy CONFIG_KVM_ARM_HOSTy CONFIG_KVM_ARM_PMUy CONFIG_VIRTUALIZATIONy CONFIG_ARM_GIC_V3y CONFIG_VIRTIOy CONFIG_VIRTIO_PCIy CONFIG_VIRTIO_MMIOy交叉编译用aarch64-linux-gnu工具链注意Uno Q的SoC支持ARMv8.1虚拟化扩展所以不需要额外下兼容补丁。编译环境我建议直接用在官方BSP里已经配置好的build脚本别自己改make参数频率和内存控制器分频配置如果不对内核起不来。编译完把Image和dtb复制到启动分区修改extlinux.conf或者grub.cfg添加consolettyMSM0,115200n8 root/dev/mmcblk0p22 rw没有额外加虚拟化参数因为KVM的初始化完全靠QHEE和内核自动识别。5.2 在EL2启动U-Boot与CPU_RELEASE_ADDR这里要说明一个关键点QHEE放行并不代表CPU自动回到EL2Bootloader也要配合。Uno Q官方Bootloader默认把内核启动到EL1如果直接这样跑KVM还是起不来。我这边选择了用U-Boot替换ABL作为最终引导因为U-Boot可以显式配置CPU进入EL2。在U-Boot环境变量里设置setenv loadaddr 0x80080000 setenv kernel_addr_r 0x80080000 setenv fdt_addr_r 0x83000000 setenv bootm_size 0x4000000 setenv bootargs consolettyMSM0,115200n8 root/dev/mmcblk0p22 rw bootm $kernel_addr_r - $fdt_addr_r但前提是U-Boot本身也运行在EL2否则它没有能力把内核放到EL2。你用“dump_el”命令查看当前异常级别如果显示EL1就要在U-Boot编译阶段打开ARMv8的PSCI或SPL支持让U-Boot自身先切到EL2。我用的U-Boot版本里关键配置是CONFIG_ARMV8_SWITCH_TO_EL1n CONFIG_ARM64_EL2y CONFIG_ARM_GIC_V3_ITSy编译U-Boot时还会生成一个bl31.bin这个是ARM可信固件也需要一并烧到xbl分区配套一起用。有时候“QHEE补丁生效了但KVM仍起不来”的原因就在这U-Boot跑在EL1没有能力抬高级别。5.3 验证/dev/kvm并启动第一个Guest内核启动后在串口终端输入dmesg | grep -i kvm如果看到kvm: Virtualization mode is not available说明EL2还是没进去。如果看到KVM: interrupting vcpu at guest EL2 KVM: using HYP MMIO KVM: using vgic-v2那基本就成了。接着确认设备节点ls -l /dev/kvm有输出后我用kvmtool快速启动一个最小aarch64 guest因为它不像QEMU全虚拟化需要额外固件lkvm run --kernel Image_guest \ --disk guest-rootfs.img \ --cpus 2 \ --mem 256 \ --console virtio如果看到guest里的登录提示符说明KVM已经完全打通。也可以换成QEMUqemu-system-aarch64 \ -machine virt \ -cpu host \ -enable-kvm \ -m 256 \ -kernel Image_guest \ -drive fileguest-rootfs.img,formatraw,ifvirtio \ -nographic这里-cpu host最关键它让Guest直接暴露宿主机CPU特性性能损耗非常小。5.4 性能与隔离调优KVM跑起来只是开始实际要长时间跑两个服务还得做两件事CPU绑核和中断隔离。我在宿主机内核参数里加了isolcpus2,3把两个核心隔离给Guest使用Host自己的服务和中断尽量留在CPU0和CPU1上。同时把Guest的vCPU绑到CPU2和CPU3。taskset -c 2,3 /usr/bin/lkvm run ...内存方面QHEE和TrustZone默认保留了一部分内存给安全世界Host端能用的内存比标称略少。KVM需要给Guest预留连续内存但Uno Q的IOMMU可以处理分散内存所以不需要预留大块内存直接交给内核动态分配即可。网络方面我试试virtio-net直接共享Host的网络接口性能比User-mode网络高很多延迟也低。如果要做实时任务建议Guest里开PREEMPT_RT补丁Host端保持普通内核也能获得不错的效果。6. 问题排查记录与经验速查表6.1 常见失败现象与处理这次打补丁过程里我至少遇到四类问题列在这里方便以后排查。第一类是刷完QHEE后串口完全无输出。这个多半是分区写错了或者签名不匹配。不要慌进EDL模式把备份的qhee重新刷回去只要能进EDL板子就有救。第二类是能启动Linux但/dev/kvm不存在。这种情况先看内核启动日志确认是否编译了KVM模块再看Bootloader当前异常级别。如果U-Boot跑在EL1及时调整编译配置重新刷。第三类是dev/kvm存在但启动QEMU时报“kvm run failed: Function not implemented”。这通常是QEMU版本太老不支持host CPU模型升级QEMU到7.0以上就解决了。第四类是虚拟机启动进系统后非常卡整体只有几十毫秒响应。这是中断没有分配到隔离核检查ulimit、irqaffinity和isolcpus是否真正生效必要时用htop看中断号再改写/proc/irq/xxx/smp_affinity。6.2 一张问题速查表现象可能原因处理方式刷完QHEE后无日志分区写错/签名错误EDL模式恢复qhee备份串口提示authentication failed证书不匹配检查debug证书和私钥重新签名内核打印Virtualization failedU-Boot未在EL2重新编译U-Boot开启ARMV8_EL2/dev/kvm不存在内核未编KVM检查CONFIG_KVM与CONFIG_VIRTUALIZATIONKVM module加载成功但exitcode非0QEMU版本过旧升级QEMU使用-cpu hostGuest系统网络不通virtio-net配置异常在Host端用taskset绑定vhost线程Guest时钟漂移严重没有配置虚拟化定时器内核开CONFIG_ARM_ARCH_TIMER关闭KVM下clock_gettime模拟6.3 最后再分享一点经验如果让我重新做这个项目我会把时间分配调整得更极端用六成时间做静态分析和启动日志对照三成时间做U-Boot引导最后一成才是修改QHEE字节。因为一旦把QHEE和U-Boot的异常级别搞顺了KVM反而顺理成章。另外整个修改过程必须保持在git里可追溯。我在工作目录里建了patches文件夹每个阶段保存一个patch文件类似于git format-patch -1 git apply --stat 0001-qhee-enable-virt.patch虽然没有用git做内核源码管理但把二进制镜像的hash、反汇编分析截图、刷机记录都提交版本控制后期定位问题会特别方便。想偷懒的话至少记下每次刷机的hash一旦出问题能立刻对比到底是固件还是启动参数变了。高通平台上的这类玩法本质上就是一个“谁控制异常级别谁就能控制整个系统”的游戏。QHEE作为最早启动的安全组件只要遇到开发者版本给了一个信任链的入口剩下的就是耐心读汇编。这个项目做完以后你会对ARMv8异常级别、TrustZone和高通BootChain有很直观的理解这些知识以后调试任何高端嵌入式平台都用得上。