返回 文章 build CMS 文章

一个月从零构建 M4 Mac Mini 的 Linux GPU 驱动

用 LLM 辅助逆向工程,一个月内从零构建 M4 Mac Mini 的 Linux GPU 驱动。

GPU驱动逆向工程LLMLinux
成长分 / 100 80 综合收获、行动、留存与影响

一个月从零构建 M4 Mac Mini 的 Linux GPU 驱动
为什么值得读了解如何用 LLM 辅助完成通常需要数年的 GPU 驱动逆向工程。

学习洁净室逆向工程方法和 hypervisor 在硬件追踪中的应用。

关键洞察
  1. 通过 hypervisor 实时探测和重放硬件状态,可以逆向工程 AGX 固件 ABI 和用户空间。
  2. LLM 在逆向工程中表现出色,但可能过于迂腐,陷入无关紧要的细节。
  3. 采用 Mesa 优先的方法比纯硬件逆向工程更高效。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:20608

我来了,我提示了,我走了(第二部分):一个月内从零构建 GPU 驱动

上一篇博客:https://codyho.dev/blog/hypervisor-macbook-neo/

我们做了什么

太长不看:Niklas 和我用大约一个月的时间,为 M4 Mac Mini 和 MacBook Neo 构建了一个完全符合 OpenGL ES 3.0 标准的 GPU 驱动,而这个过程通常需要数年。下面是 Chrome 和 Firefox 在 M4 Mac Mini 上运行 WebGL 的画面,合成显示工作正常:

Chrome 和 Firefox 在我们的驱动上运行 three.js WebGL 演示。

最重要的是,这个驱动足够快,可以以 200fps 运行 Minecraft:

Minecraft 在 M4 Mac Mini 上以 212fps 运行。

构建这个驱动需要对 AGX(苹果对其 GPU 的称呼)极其复杂的固件 ABI 和用户空间组件进行逆向工程。这一切都是以透明、可验证的洁净室方式,使用成熟的技术完成的。代码尚未准备好面向最终用户,但我们希望尽快把它交到用户手中。

我们是怎么做的

此前我构建了一个用来逆向 macOS 的 hypervisor。现在的目标是用它做点真正有用的事,还有什么目标比编写 GPU 驱动更合适呢?GPU 实际上是任何现代系统的必需品,否则一切都得靠 CPU 渲染,速度要慢几个数量级,能效也低得多。我们的目标是为 M4 Mac Mini 和 MacBook Neo 实现符合标准的 OpenGL(以及即将到来的 Vulkan)驱动。

通常,构建 GPU 驱动要花上数年;我们的目标是在几天内完成。事实证明“几天”过于乐观,但“几周”仍然是巨大的进步。在这几周里,我们完成了:

  • 仅通过实时探测就逆向了 M4、A18 Pro 和(大部分的)M5 用户空间,并发现了硬件支持、但苹果驱动不会发出的功能和指令
  • 构建了一个完全可用的用户空间驱动,包括全新的自研 IR/着色器编译器、命令流构建器以及更多组件
  • 利用我之前构建的 hypervisor 抓取的轨迹,从零逆向了完整的 AGX 固件 ABI
  • 为该固件 ABI 实现了完整的 Linux 内核驱动

在整个过程中,我们没有查看过任何苹果的二进制文件,只使用了(来自我们 hypervisor 的)硬件轨迹和我们自己编写的着色器。对于用户空间图形逆向,我们谨慎地把所有必需的苹果 blob 视为不透明对象。我们请一位朋友为这些 blob 撰写了文档 1,这样我们就能自行完成洁净室实现(其中大部分是靠盲目尝试直到跑通构建出来的)。我们已公开所有实验,任何人都可以验证我们工作的来源(见交付成果下的双 agx-re 仓库)。

本文分为用户空间和内核空间两部分,这与所有现代 GPU 驱动的划分一致:内核负责与固件对接、分配缓冲区和管理调度,而这些缓冲区的实际内容以及被调度的是什么,对内核来说是不透明的。用户空间则负责真正理解 GPU 如何工作,并往这些缓冲区里填入内容。

内核空间

在 Apple Silicon 上,内核驱动并不直接与硬件对接,而是与运行一个名为 RTKit 的自定义 RTOS 的 GPU 固件通信。这意味着编写内核驱动的第一步不是与硬件对话,而是搞清楚固件 ABI。

固件 ABI 是这个项目中迄今最令人头疼的部分。苹果没有做设计一个带有良好接口的合理 ABI 这种理智的事,而是基本上把一个普通的内核驱动切成两半,把其中一半放进 AGX 并称之为固件,另一半内核驱动则通过内存中的共享结构体与之通信。其中许多结构体把固件拥有的字段(我们绝不能修改,只能通过逆向去了解)与主机控制的字段交错在一起。要感受这个 ABI 有多复杂,看看 M1/M2 上的共享内存树就知道了:

M1/M2 固件 ABI

Asahi Lina 以在艰苦的 12 小时工作日中搞清楚这一切而闻名,并借此构建了 M1/M2 内核驱动,这是令人惊叹的技术成就。不幸的是,A18 Pro 的固件 ABI(我最初在 MacBook Neo 上开始逆向工作,后来转到 M4 Mac Mini)比本就非常复杂的 M1 固件 ABI 复杂得多:

A18 Pro 固件 ABI

搞什么鬼,Apple。请注意 A18 有:

  • 1.5 倍数量的结构体
  • 两倍数量的指针
  • 复杂得多的任务提交流程

还有许多其他问题给逆向过程增添了阻力 2。我确实有一些关于固件 ABI 的文档,但非常不完整,说实话也没什么用 3。

我的方法很简单,基于成功逆向 M1/M2 机器时所用的方法:观察 macOS 做了什么,重放它,然后尝试自己去做,而这一切都由 hypervisor 实现。

当我向 LLM 描述这个方法时,它把“重放”理解得极其字面:它做的第一件事是等待第一个对固件可见的事件(称为“kick”),然后保存了整个 GPU 内存状态的副本。重启后,它把保存的内存状态直接复制回主机内存,执行 kick,并观察输出页面的变化。接着它会尝试用代码重建这些对象,跟踪所有指针并理解其内容。在一次次实验中,Codex 不断减少复制的页面数量,直到不再有任何重放的状态,一切都从源码构建。4 令人惊讶的是,我注意到 Codex 很有品味,知道什么时候该继续戳硬件,什么时候该直接运行 hypervisor 自己抓取状态。

主要有三个问题,都是因为我们无法干净地捕获主机端的工作:

第一个问题是在 GPU 固件启动之后提交的渲染任务。我们可以在固件启动前预先布置任务,启动 GPU,这些任务会按预期完成;但一旦固件启动,之后提交的任何任务都只会被 ACK 并直接退役,而不会真正执行任何操作。固件启动后,捕获状态要困难得多,因为一切都变成动态的,固件成了一个带有难以重放的状态的有状态对象。

此时我不得不介入,检查 Codex 的做法。结果发现它试图在 AGX 生命周期很晚的时候重放一次捕获,那时已经发生过许多先前的事件。当我让它选择 AGX 生命周期早得多的捕获,也就是固件启动后的第一次捕获时,Codex 几乎立刻就发现了问题(它漏掉了一个字节的描述符)。这花了几天时间。

第二个问题,也是唯一一个真正造成阻塞的问题,是计算。AGX 大体上支持两类任务:计算和渲染。在常规 GUI 路径中,计算任务只有在大量渲染任务已经执行之后才会被调度。因此,要获得一份干净的计算负载捕获花了很长时间,而当 Codex 终于拿到时,它有 336 MB,根本无法重放(它试了很久)。它还尝试通过查看捕获自行构建这些对象,为此花了一个多星期,但最终没有成功。要筛查的无意义内容实在太多。我这边的问题也加剧了这一点:渲染跑通后,我以为提交计算任务会更简单(计算的固件 ABI 确实更简单,所以这点我判断对了),但我失去了谦逊,认为这会是小菜一碟,只要几个小时。因此我没有为 LLM 把任务搭好脚手架。

解决办法其实是在另一个 Codex 会话里给我的。本质上是:

  1. 通过启动到单用户模式来禁用 GUI,这意味着不会有任何渲染任务。
  2. 安装一个 LaunchDaemon,让它在尽可能早的时间点运行,即 Metal(苹果专有的图形框架)可用的那一刻。
  3. 运行我们提供的一个很小的 Metal 程序
  4. 捕获并重放这条很小的、纯计算的轨迹。

轨迹被成功捕获。几个小时内,Codex 就把它拆解完毕,几天内 Codex 就让计算跑通了。至于最初的计算代码库为什么不能用……Codex 也不知道。能用的和坏掉的看起来非常相似。

事后看来,这本该从一开始就是策略:捕获尽可能小,并在单用户模式下运行,以免干扰结果。针对最后一个问题,我吸取了教训:

部分渲染(partial render)最终成为最难搞清楚的事情之一。当分块顶点缓冲区(Tiled Vertex Buffer,TVB)不够大、无法存放当前几何数据时(也就是要画的三角形太多),就会发生这种情况。此时有两个选择,驱动需要同时支持:要么增大 TVB,要么执行部分渲染,即先渲染一部分几何,然后用其余的三角形重新加载缓冲区,再完成这次部分渲染。事实证明,部分渲染非常非常棘手,比其他工作更甚,因为它本质上意味着要给 GPU 驱动加上保存与恢复功能。

我之前发现的工作流在这里派上了大用场。Codex 能够重放一次部分渲染事务,然后修改我们的 Metal 着色器以执行多次部分渲染(做法很简单:用成千上万个三角形反复轰击单个图块,直到触发部分渲染),接着学会了如何重放这些渲染。一旦 Codex 成功重放,它学会自行构建只是时间问题。

构建内核驱动

从 Python 原型驱动迁移到功能完整的 Linux 驱动花了三天,其中有一天几乎完全浪费了,因为 Codex 出于某个我至今不理解的原因,选择先攻克部分渲染(迄今最难的任务),而不是先做计算(最简单的任务)。我让它先做计算之后,一切都顺利了。

总体而言,整个过程基本上是:

  1. 按照完全相同的模式用 Rust 重写现有的 drm-shim,这样我们就得到一个同步的 Rust 驱动。
  2. 把前端重写为异步;实际的 GPU 提交仍然是同步的。
  3. 把 GPU 提交重构为异步,不再轮询,而是监听固件事件并将任务与 fence 关联
  4. 实现一些唾手可得的优化,例如批量提交任务。

这些都是相当常规的工程工作,LLM 完全有能力胜任。

我发现唯一值得一提的是,Codex 为了调试自己的代码为何不能工作,大量使用了 hypervisor,包括捕获完整的地址空间并与已知正确的样本进行比较。这种系统化的调试方式,正是 Codex 成为我最喜欢的编码智能体的原因。

用户空间

A18 Pro 的用户空间与 M1/M2 大不相同:它有新的描述符格式、新的 ISA,以及一大堆其他新东西。除了是一个为运行 Metal 而设计的分块延迟渲染器之外,它就是另一款 GPU。

好消息是,用户空间逆向有一套定义非常清晰的流程:写一个小的 Metal 程序,编译,运行,看看什么变了,然后拆开它,开始摆弄各个比特,直到弄懂它们的作用。如果你觉得这听起来正是那种枯燥、重复、机械、而 LLM 非常擅长的工作,那你说对了。

逆向工作分两个阶段进行。第一阶段,我让 Claude 查看它能找到的每一个 Metal 程序,尝试构建反汇编器、汇编器,并理解 GPU 驱动所需的所有其他描述符/命令流等的格式。为求完整,我让 Claude 把所有东西都枚举出来,包括 Linux 用不上的(比如曲面细分)。这一步成功了,但 Claude 能反汇编再重新汇编程序,并不意味着它知道如何自己构建一个。当我们试图弥合这一差距,即充分理解每条指令,以便真正能编译我们自己的任意程序时,Claude 表现得很糟糕,基本毫无进展。

此时,Niklas 完成了他为 M4 Mac Mini 编写的 drm-shim,加入我一起进行第二阶段的用户空间逆向。对于如何真正完成用户空间驱动,我们采用了两种不同的方法:

我的方法是优先进行硬件逆向,专注于弄清楚硬件和所有指令是如何工作的。然后我写一份规范,让 LLM 去实现,希望最终实现完整的 OpenGL 和 Vulkan 合规。这意味着我的 LLM 大部分时间都花在硬件上编写实验,而不是真正实现 Mesa 代码。想法是一旦我理解了硬件,其余一切就水到渠成。

Niklas 采取了不同的方法,我称之为“Mesa 优先”。本质上,他先尝试把 Mesa 搭起来,只在为了构建某些功能时才进行逆向。他的时间分配在构建和测试 Mesa 以及进行逆向两方面。

事实证明,Niklas 的进展比我快得多,因为我的智能体以追求完整为名,在细枝末节、无关紧要的任务上花了大量时间。相比之下,他的智能体因为必须真正构建 Mesa 而有了落脚点,因此对时间和资源的使用有效得多。他推进得比我快太多,以至于最后我只能通过调查他尚未理解的任何行为来支持他的工作。

这是我注意到的 Codex 的一大局限。我能想到的最贴切的词是“迂腐”:它始终极其周密,这在某些场景下是很大的优势,但有时它会陷入某个随机旁枝的细节,损害了整体目标。

在逆向过程中,我们发现了硬件支持、但 Metal 不支持的行为;这是通过直接摆弄不同指令的比特,并根据已知存在的东西推断可能存在什么而发现的,就像 Alyssa Rosenzweig 逆向 M1/M2 时所做的那样。这包括:

  • 原生的单指令 64 位加法
  • 高达 128x 的各向异性过滤(Metal 上限为 16x)
  • 矩阵单元的一种新模式
  • uniform_mov 的 7 位立即数支持

Mesa 开发

在构建用户空间图形时,有几件事对我们非常有利。最值得一提的是,Khronos 一致性测试套件(CTS)本身就是一个详尽的测试语料库,我们的驱动必须通过它。换句话说,与 LLM 合作时最难也最敏感的部分,即给它们提供良好的测试作为依据,已经替我们完成了。

此外,Mesa 已有很好的抽象,让我们的工作轻松很多。现代 Linux 上的 OpenGL 栈如下所示:

现代 Linux 图形栈示意图。

我们只需要在 Gallium(Mesa 的内部 API)与 AGX 硬件语义之间进行转换。这个过程中最大的部分之一,是把 NIR(Mesa 的内部 IR,与 LLVM IR 十分相似)翻译成 AGX 的专有 ISA。另外,这个编译器还可以复用于未来的 Vulkan 驱动。

Niklas 逐个迭代 OpenGL 特性,边做边逆向用户空间,最终实现了完整的 OpenGL ES 3.0 合规(未通过的测试属于可选扩展):

OpenGL ES 3.0 一致性测试套件在我们的驱动上通过。

在整个过程中,我们受益于已有的 M1/M2 工作:虽然具体的硬件语义不同,但整体形态相似,因此许多正确的取舍和决策已经有人替我们做好了。在构建这个驱动的整个过程中,我越来越清楚地认识到,Alyssa Rosenzweig 和其他构建 M1/M2 驱动的人简直是奇才,向他们致敬!

交付成果

Mesa:https://github.com/niklassheth/mesa

Linux 内核驱动:https://github.com/GravityLinux/linux/gravity-m4

用户空间逆向文档(一堆糟糕的 LLM 垃圾内容,但能用):Cody Niklas

剩余工作

Vulkan 1.4、OpenGL 4.6、OpenGL ES 3.2、OpenCL 3.1、Direct3D 12(通过 Proton)以及光线追踪都在范围之内。我们希望我们的驱动能与世界上最好的图形驱动相媲美。

此外,Niklas 和我希望把这一切都上游化,但存在一些重大障碍。我们使用的是未经修改的 Asahi UAPI,因此 Mesa 上游化不存在策略问题,但它需要更多的测试、人工评审,并需要重构成可评审的 PR。我们也预计会遇到大量质疑,因为这很可能是有史以来第一个完全由 LLM 编写的 GPU 驱动,我们的代码将被要求达到比人类编写的代码更高的标准。我们已为这些挑战做好准备,但它们主要是人际层面而非技术层面的问题,这是 LLM 帮不上忙的。

Linux 内核驱动会是更大的问题,因为 M1/M2 驱动尚未上游化,而且实际上我们没有能力改变这一点。我们认为这里最好的办法就是等待那个驱动上游化,然后在 M1/M2 上游化之后(经过所有必要的重构、评审、拆分等等)再上游化我们的驱动。遗憾的是,这可能要等一段时间。

我什么时候能用上?

年轻的蚱蜢,请耐心等待,我们期待着很快把这些代码交到你手中,等待的时间可能比你预想的短得多。毕竟,苹果落地离树不远。

加入我们

如果你有兴趣参与其中,欢迎加入我们的 Discord 服务器。欢迎来讨论想法、聊天,或者只是闲逛!

附记:真的吗,Sam?

我在内核逆向任务中使用了搭载 GPT-5.6 Sol(后来 GPT-6 Astra 发布后改用它)的 Codex。我遇到的最大问题之一是过于激进的网络安全限制(我没有加入可信访问计划)。

百分之九十九的情况下,一句简单的 /goal resume 或“继续”就足以让 LLM 继续工作(这也说明这些限制过于激进),但它们仍然打断了我的无人值守工作流。我写了最快、最笨的解决方案:一个守护进程,每分钟截一次屏,与上一张截图比较,如果相同(因为 Codex 停止了进展),就输入 /goal resume。我不小心把它在几个群聊里开着没关:

/goal 恢复守护进程在群聊中刷屏。

它一直在继续。

话虽如此,GPT-6 Astra 和 GPT-5.6 Sol 实在强得离谱,是固件 ABI 逆向方面迄今表现最好的,所以这个小花招绝对值得。

附记:M4、A18 Pro 与 M5 的对比

据我所知,M4 和 A18 Pro 的用户空间实现实际上是相同的,我能找到的唯一区别是有一个值在 M4 上略大,这与它有更多核心相符。两者的固件 ABI 则差异显著:A18 Pro 上的第二个 RTKit 协处理器让一切变得更复杂。从这个角度看,就固件而言,A18 Pro 更像 M5,而不是 M4。M5 的用户空间与 M4 有一些相似之处,ISA 大体上是其超集,但某些部分完全不同,例如纹理描述符。M5 的用户空间已被部分逆向;其固件 ABI 已被完全逆向;已构建并充分测试了一个原型 drm-shim;我认为把该原型升级为完整的 Rust 驱动不会花太久。我的主要目标仍然是 M4 Mac Mini 和 MacBook Neo。

脚注


  1. Metal 辅助程序

    我不确定 Claude 为何在页眉里写这不是洁净室,我想它是被 agx-re 中“不要查看苹果二进制文件”的指令搞糊涂了。文档本身显然不包含任何受污染的信息。↩︎

  2. 简要列表:

    • 如果我们让固件崩溃,唯一的恢复方式是完全重启
    • 如果任务握手有任何问题,不会报告任何错误,任务会被 ACK,然后永远不会完成。
    • 有些问题会让输出按预期变化,同时还会在不该被触碰的页面上造成任意损坏。

    ↩︎

  3. https://github.com/mischa85/apple-gpu-firmware-abi ↩︎

  4. 如果我们发现某个页面包含着色器,就会丢弃该页面并替换成我们自己的着色器,作为额外的版权保护措施。↩︎