健忘的 CPU:在 M4 上启动 Linux
这篇博客相当详细地记录了我第一次在 M4 Mac mini 上启动 Linux 的过程。遇到不认识的术语和概念请自行查阅,毕竟我没法在一篇文章里讲清所有背景 ;)
在开始之前,我要先感谢 Asahi Linux 团队全体成员在此前的工作中以及在这次探索过程中给予的帮助。如果你希望看到更多在 Apple Silicon 上运行主线 Linux 的成果,请考虑向 Asahi Open Collective 捐款!
起点
2024 年 11 月,我买了一台 M4 Mac mini,赌的是它和 M1-M3 这些 Apple Silicon 机器差不多,能很快获得 Asahi Linux 的支持。M4 在我桌上放了几个月之后,关于这颗 SoC 的更多细节逐渐浮出水面。
实际情况确实更棘手,因为 M4 是第一代强制启用 SPTM(Secure Page Table Monitor)的 Apple Silicon 机器——这个机制用于加固 macOS 的 XNU 内核、防范相关漏洞。在此前的机型上,Linux 的适配工作很大程度上依赖通过 m1n1 hypervisor 抓取的 MMIO 跟踪记录,据此分析 macOS 原生驱动与硬件之间的交互。而有了 SPTM,要让 macOS 在 hypervisor 下运行,需要对 m1n1 做大规模改动,这显然超出了我这个新手的能力范围。
不过这事也并非全然无望。在 hypervisor 相关工作之外,我开始尝试直接在 M4 上启动 Linux。具体做法是:关闭严格的启动安全机制,通过 macOS 恢复模式把 m1n1 安装为自定义启动对象1,再接上串口控制台2以便查看日志。
被锁住的寄存器
m1n1 最初只能以 BRINGUP 模式启动,否则尝试初始化 GXF3 时会立即崩溃。 后来发现,在 M4 及更高版本 SoC 的原始启动模式下,GXF 功能是被禁用或锁定的, 因此让该初始化变为条件执行,并在这些机器上跳过它,是正确的做法。 除了被禁用的 GXF 功能外,还有 RVBAR(重置向量基址寄存器): 这是内存中的一个位置(每个 CPU 核心一个),决定核心上电时从哪里开始执行。 m1n1 代码在启动内核或链式加载另一个 m1n1 时, 会将入口点地址写入每个核心的 RVBAR。 但在 M4 上,写入该寄存器同样会导致崩溃。 简而言之,因为寄存器中已经包含了正确的值,所以这个写入操作也需要被跳过。
2024-12-01 17:37 <yuka> can confirm this state gives a working usb proxy, as in a "Generic m1n1 uartproxy v1.4.17-61-ga24ff77" turns up in lsusb, and I can open a shell :)
debug_putc
此后,我很久都没有再碰那台 Mac mini,直到 2025 年底的 Chaos Communication Congress 大会上,我才重新拾起这份兴趣。我终于摸索出一个极简的设备树,只包含 CPU 核心和 AIC 中断控制器,并加载了带有 `earlycon` 参数的 Linux 内核(使用 m1n1 的 `linux.py`)。然而,输出始终停留在“Vectoring to next stage”之后就没有下文了。既然内核给不出有用的信息,我除了瞎猜还能做什么呢?于是我决定用最直接的方法:经典的 `println` 调试。我从 m1n1 那里拿来了 `debug_putc` 汇编例程,并调整它使其能打印单个字符‘a’4。我把这段代码插入到 Linux 内核启动的极早期,果然,在“Vectoring to next stage”之后我看到了那个‘a’!本质上,我对 Linux 启动代码进行了一次二分查找,最终定位到 MMU 初始化代码处(位置非常靠前,仍在 `arch/arm64/kernel/head.S` 的汇编代码中)。难道 MMU 初始化会导致 CPU 崩溃吗?情况并非如此:UART 是通过内存映射 I/O 访问的。一旦 MMU 启用,所有内存访问都会指向虚拟地址,这些虚拟地址通过页表映射到对应的物理地址。尽管 m1n1 会建立映射,将 MMIO 地址空间暴露在相同的虚拟地址上,但 Linux 并未这样做,这意味着 MMU 启用后,我们实际访问的是未映射的空间,而非 UART。我修改了初始页表,为 MMIO 空间添加了这个 1:1 的映射,结果 `debug_putc` 的输出能够深入到启动流程的更后方。再次进行二分查找,现在打印功能可以持续到中断控制器初始化后的某个位置!我将问题缩小到对一个实现特定的 CPU 寄存器 `SYS_IMP_APL_VM_TMR_FIQ_ENA_EL2` 的写入操作,正是这个操作引发了新的崩溃。注释掉这个写入操作后,内核成功启动到了 shell。太棒了!`SYS_IMP_APL_VM_TMR_FIQ_ENA_EL2` 与虚拟化相关,在更新的 iBoot 版本中已被解除限制,因此现在已无需再注释掉该写入操作。
既然确认 Linux 代码确实运行了,我再次回头排查,尽管使用了 `earlycon` 启动参数,为何之前在串口控制台上没有看到任何 `printk` 输出。
2026-01-23 12:35 <yuka> added earlycon=s5l,0x3ad200000 to my bootargs
2026-01-23 12:36 <yuka> and now I'm actually getting useful output before the crash is happening!
果然,问题只是设备树里缺了
stdout-path = "serial0",加上之后,Linux 在早期崩溃时就能输出完整的寄存器转储和堆栈跟踪了。
副核与 WFI
目前 m1n1 还没有启动副核,因为缺少
smp_start_offset(这是 m1n1 里硬编码的一个偏移量;没有它,smp_init 就会被跳过)。我尝试了 M1 到 M3 所用的偏移量,成功启动了副核。但当我再次尝试加载 Linux 时,又遇到了一个神秘的崩溃。
此前的 Apple Silicon CPU 在 WFI 指令上就有已知的怪癖。根据一个"鸡位"(chicken bit,即寄存器中的一个位,厂商可以用它禁用某些 CPU 优化或特性,或者干脆"认怂")的状态,在 XNU 开源代码里叫
ARM64_REG_CYC_OVRD_ok2pwrdn_force_mask,WFI 指令会导致 CPU 寄存器 x0-x31 被清零。XNU 的做法是在 WFI 之前把这些寄存器压栈,之后再恢复。在 M1-M3 SoC 上,m1n1 会禁用这种行为,让 CPU 表现得和其他 arm64 处理器一样。而 Asahi 内核随后又会特意重新启用它,让 CPU 核心进入更深的睡眠状态以省电。这也是让同集群中某个核心在其他核心都处于这种 WFI 深度睡眠时能够提升到更高频率的前提。
看来这个鸡位在 M4 上要么被锁死了,要么干脆被删掉了,而默认行为不符合 ARM64 规范(规范明确写道:"如果系统配置允许 WFI 指令完成,那么 WFI 指令不得导致架构状态丢失。"5)。2026 年 4 月,我把内核里所有的 WFI 和 WFIT(带超时的等待中断)指令都替换成 NOP(空操作),成功在 M4 上以全部核心启动了 Linux。
由此,一场为 WFI 问题寻求上游解决方案的漫长征程正式开启。起初,我调研了 errata(硅片层级的行为偏差)在通常是如何被处理的。Linux 内部拥有一整套框架,使其能够在早期启动阶段通过内存打补丁的方式进行自我修正。不过,这种方案最终被否决,因为要精准识别出应当将 WFI 指令当作空操作(NOP)执行的情形非常困难。具体来说,在 macOS hypervisor 下运行的虚拟机同样会触发针对该 erratum 的拦截逻辑,但此时 WFI 会被 macOS hypervisor 捕获,并用于更高效地调度不同的虚拟机。检测虚拟化(尤其是嵌套虚拟化处于启用状态时)本身也异常复杂。因此,Will Deacon 提出了一个替代方案:内核应新增对禁用 WFI 空闲模式的支持,并引入一个新的 bootarg6;随后,m1n1 便可针对那些已知存在 WFI 缺陷的裸机设备,在启动时条件性地添加相应的 bootarg。在此基础之上,我们将进一步引入一种机制,允许 Linux 将核心置于睡眠状态。眼下,下游的 cpuidle-apple 驱动程序可以实现此功能,但 Sven 正在推进的基于 PSCI EFI conduit 的工作更具潜力,有望成为未来上游方案的核心基础。
很高兴地报告:用于防止 Linux 因 WFI 和 WFIT 指令而崩溃的机制,如今已正式并入主线 Linux7 8 以及 m1n19。这意味着,这些项目的最新发行版已能在搭载 M4 芯片的 Mac 上原生引导,并让次级核心(secondary cores)正常投入使用。
后续计划
目前为止,这项工作已经发掘并修复了 Linux 运行于 M4 及更晚的 Apple Silicon 芯片时所面临的底层障碍——Linux 现可引导至 shell,且所有核心均处于可用状态(同样的 WFI 规避方案在 M4 Pro、M4 Max 乃至 M5 芯片上均已验证有效!)。对周边硬件的逆向工程——此处不展开细述——正以缓慢但稳步的节奏推进。Sven 不知疲倦地为 m1n1 hypervisor 添砖加瓦,使其能够在这类设备上引导并追踪 macOS,这些进展将为攻克内置摄像头、显示控制器及 GPU 初始化等更为复杂的组件提供极大助力。大部分情况下,我都是直接向各相关的上游项目提交自己的成果,以便任何人都能从中受益。有时,我倒真希望那些获得大量资金资助的项目,能对它们如何受益于上游项目的进展表现得更为透明一些。
如果你想支持我在 M4 上运行 Linux 的工作,可以通过 LiberaPay 或 GitHub Sponsors 捐赠。也请考虑向 Asahi Open Collective 捐赠,以推动 Linux 在 Apple Silicon 上的主线化进程。
一如既往,希望这篇文章能对你有所帮助 :)
HACK: DEBUG: t8132 早期启动时的 debug_putc (8c86bd8c) · 提交记录 · Yureka Lilian / linux · GitLab↩︎
arch: arm64: 添加 early_param idle=<wfi|yield|nop> - kernel/git/torvalds/linux.git - Linux 内核源代码树↩︎
arm64: 添加 WFxT 覆盖 - kernel/git/torvalds/linux.git - Linux 内核源代码树↩︎
kboot: 如果 WFI 导致状态丢失,则禁用 wfi/wfit,by yuyuyureka · 拉取请求 #672 · AsahiLinux/m1n1↩︎