开发者一月内在M4 Mac Mini上从零构建Linux GPU驱动
Cody Ho:一个月在 M4 Mac Mini 上构建 Linux GPU
硬核工程复盘,展示了利用hypervisor和LLM进行底层硬件逆向的完整工作流,对关注Apple Silicon底层开发的同学极具参考价值。
I Came, I Prompted, I Left Part 2: Building a GPU Driver From Scratch in One Month
我来了,我提示了,我离开了 第二部分:一个月内从零构建 GPU 驱动程序
Previous blog post: https://codyho.dev/blog/hypervisor-macbook-neo/
上一篇博客文章:https://codyho.dev/blog/hypervisor-macbook-neo/
What We Did
我们做了什么
TL;DR: Niklas and I built a fully OpenGL ES 3.0 compliant GPU driver for the M4 Mac Mini and MacBook Neo in about a month, a process which normally takes years. Here is Chrome and Firefox running WebGL on the M4 Mac Mini with working compositing:
简而言之:Niklas 和我在大约一个月的时间里为 M4 Mac Mini 和 MacBook Neo 构建了一个完全符合 OpenGL ES 3.0 标准的 GPU 驱动程序,而这个过程通常需要数年。以下是 Chrome 和 Firefox 在 M4 Mac Mini 上运行 WebGL 且合成工作正常的画面:
Most importantly, the driver is fast enough to run Minecraft at 200fps:
最重要的是,该驱动程序的速度足以让《我的世界》以 200fps 运行:
Building this driver involved reverse engineering the AGX’s (Apple’s name for the GPU) incredibly complicated firmware ABI and user-space components. This was all done in a transparent, verifiably clean room manner using well established techniques. The code is not yet ready for end users, but we are looking to get it to end users as soon as possible.
构建此驱动程序涉及逆向工程 AGX(Apple 对 GPU 的命名)极其复杂的固件 ABI 和用户空间组件。这一切都是使用成熟的技术,以透明且可验证的干净室方式完成的。代码尚未准备好供最终用户使用,但我们正致力于尽快将其提供给最终用户。
How We Did It
我们是如何做到的
Previously, I built a hypervisor to reverse engineer macOS. Now the goal became to actually do something useful with it, and what better target than writing a GPU driver. The GPU is effectively a requirement for any modern system, otherwise everything needs to be CPU rendered which is orders of magnitude slower and less power efficient. Our goal was to implement conformant OpenGL (and soon, Vulkan) drivers for the M4 Mac Mini and MacBook Neo.
此前,我构建了一个用于逆向工程 macOS 的管理程序。现在的目标变成了真正利用它做一些有用的事情,还有什么比编写 GPU 驱动程序更好的目标呢?GPU 实际上是任何现代系统的必要条件,否则所有内容都需要通过 CPU 渲染,其速度慢几个数量级且能效更低。我们的目标是为 M4 Mac Mini 和 MacBook Neo 实现兼容的 OpenGL(以及即将推出的 Vulkan)驱动程序。
Normally, building a GPU driver is an endeavor that takes years; our goal was to do it in days. It turns out that days was overly optimistic, but weeks is still a massive improvement. In those weeks we have:
通常,构建 GPU 驱动程序是一项需要数年的工作;我们的目标是在几天内完成。事实证明,几天过于乐观,但几周仍然是巨大的进步。在这几周里,我们做到了:
- Reverse engineered the M4, A18 Pro, and (mostly) M5 user space using only live probing, discovering hardware-supported features and instructions not emitted by Apple’s driver
- Built a fully working user-space driver, including a new custom IR/shader compiler, command stream builder, and many more components
- Reverse engineered, from scratch, the full AGX firmware ABI using traces from the hypervisor I previously built
- Implemented a full Linux kernel driver for said firmware ABI
- 仅使用实时探测,逆向工程了 M4、A18 Pro 和(大部分)M5 的用户空间,发现了 Apple 驱动程序未发出的硬件支持功能和指令
- 构建了一个完全可用的用户空间驱动程序,包括新的自定义 IR/着色器编译器、命令流构建器以及许多其他组件
- 基于我之前构建的管理程序的跟踪记录,从零开始逆向工程了完整的 AGX 固件 ABI
- 为该固件 ABI 实现了完整的 Linux 内核驱动程序
Throughout this process, we have not looked at any Apple binaries, only hardware traces (from our hypervisor) and shaders we built ourselves. For user-space graphics RE, we were careful to treat any required Apple blobs as opaque objects. We had a friend write documentation on these blobs 1 so we could write a clean room implementation ourselves (which was mostly built by just blindly trying stuff until it worked). We have published all of our experiments so that anyone can verify the provenance of our work (see the twin agx-re repos under Deliverables).
在整个过程中,我们没有查看任何 Apple 的二进制文件,只使用了硬件追踪数据(来自我们的 Hypervisor)以及我们自己构建的着色器。对于用户态图形逆向工程,我们谨慎地将任何必需的 Apple 二进制块视为不透明对象。我们请一位朋友为这些二进制块编写了文档[1],以便我们能够自行编写一个干净室实现方案(该方案主要通过盲目尝试直到成功来构建)。我们已经发布了所有实验结果,以便任何人都可以验证我们工作的来源(请参阅“交付物”下的 twin agx-re 仓库)。
This blog post is divided into two parts, user and kernel space. This mirrors the split in all modern GPU drivers: the kernel is responsible for interfacing with the firmware, allocating buffers, and managing scheduling, while the actual contents of those buffers and what is being scheduled are opaque. User space is responsible for actually understanding how the GPU works and filling those buffers with stuff.
这篇博客文章分为两部分:用户态和内核态。这反映了所有现代 GPU 驱动程序的分工:内核负责与固件接口、分配缓冲区以及管理调度,而这些缓冲区的实际内容以及被调度的具体事项是不透明的。用户态则负责真正理解 GPU 的工作原理,并用具体内容填充这些缓冲区。
Kernel Space
内核态
On Apple Silicon, the kernel driver does not interface directly with the hardware. Instead, it talks to the GPU firmware running a custom RTOS called RTKit. That means that the first step to a kernel driver is not talking to hardware, it’s figuring out the firmware ABI.
在 Apple Silicon 上,内核驱动程序并不直接与硬件交互。相反,它与运行着名为 RTKit 的自定义实时操作系统(RTOS)的 GPU 固件进行通信。这意味着内核驱动程序的第一步不是与硬件对话,而是弄清楚固件的应用程序二进制接口(ABI)。
The firmware ABI was by far the most annoying part of this project, because rather than doing the sane thing of coming up with a reasonable ABI with nice interfaces, Apple essentially took a regular kernel driver, cut it in half, and then put half of it in the AGX and called it firmware, with the other half of the kernel driver communicating using shared structs in memory. Many of these structs have firmware owned fields (which we must never modify and which we must learn from reverse engineering) interleaved with host controlled fields. For an idea of how complicated the ABI is, this is what the shared memory tree looks like on the M1/M2:
固件 ABI 是这个项目中最为烦人的部分,因为 Apple 没有采取合理的方式设计具有良好接口的合理 ABI,而是基本上拿了一个普通的内核驱动程序,将其一分为二,然后将其中一半放入 AGX 并称之为固件,另一半内核驱动程序则通过内存中的共享结构体进行通信。许多这些结构体包含由固件拥有的字段(我们绝不能修改,且必须通过逆向工程来学习),并与主机控制的字段交错排列。为了了解 ABI 的复杂程度,以下是 M1/M2 上共享内存树的样子:
Asahi Lina famously figured all of this out over grueling 12-hour days to build the M1/M2 kernel driver, an amazing technical accomplishment. Unfortunately, the A18 Pro firmware ABI (I started my RE work on the MacBook Neo and later pivoted to the M4 Mac Mini) is significantly more complicated than the already very complicated M1 firmware ABI:
Asahi Lina 以著名的毅力,在长达 12 小时的艰苦工作中破解了所有这些细节,从而构建了 M1/M2 的内核驱动程序,这是一项令人惊叹的技术成就。不幸的是,A18 Pro 的固件 ABI(我最初在 MacBook Neo 上开始逆向工程工作,后来转向 M4 Mac Mini)比已经非常复杂的 M1 固件 ABI 要复杂得多:
What the F@!#, Apple. Note how the A18 has:
真是见鬼,Apple。注意 A18 拥有:
- 1.5x as many structs
- twice as many pointers
- a significantly more complicated process for submitting work
- 1.5 倍多的结构体
- 两倍的指针
- 更复杂的任务提交流程
There are many other issues that add friction to the RE process 2. I did have some documentation on the firmware ABI, but it was highly incomplete and honestly was not very useful 3.
还有许多其他问题会给 RE(逆向工程)流程增加摩擦。我确实有一些关于固件 ABI 的文档,但它们非常不完整,说实话也没什么用。
My approach was simple and based on the approach used to successfully reverse engineer the M1/M2 machines: watch what macOS did, replay it, then try to do it ourselves, which is made possible by the hypervisor.
我的方法很简单,基于成功逆向工程 M1/M2 机器所使用的方法:观察 macOS 做了什么,重放这些操作,然后尝试自己实现,这得益于 hypervisor 提供的可能性。
When I described this approach to the LLM, it took replay extremely literally: the first thing it did was wait for the first firmware visible event (these are called “kicks”), then saved a copy of the entire GPU memory state. After a reboot, it copied the saved memory state straight back into host memory, performed the kick, and saw the output pages change. It would then try to reconstruct these objects in code, following all the pointers and making sense of the contents. Over successive experiments, Codex would reduce the number of pages it copied until there was no more replayed state and everything was built from source. 4 Amazingly, I noticed Codex had good taste regarding when it should poke the hardware some more and when it should just run the hypervisor and capture the state itself.
当我向 LLM 描述这种方法时,它对“重放”的理解极其字面化:它做的第一件事是等待第一个可见的固件事件(这些被称为“kicks”),然后保存整个 GPU 内存状态的副本。重启后,它将保存的内存状态直接复制回主机内存,执行 kick 操作,并观察到输出页面发生变化。随后,它会尝试通过代码重建这些对象,追踪所有指针并理解其内容。随着实验的推进,Codex 逐渐减少它复制的页面数量,直到不再需要重放任何状态,所有内容都从源代码构建。令人惊讶的是,我注意到 Codex 在何时该进一步探测硬件、何时该仅运行 hypervisor 并自行捕获状态方面,有着良好的判断力。
There were three major issues, and all were caused by our inability to get a clean capture of host work:
存在三个主要问题,且均由我们无法干净地捕获主机工作引起:
The first issue was render work submitted after the GPU firmware started. We could prestage work before the firmware started, start the GPU, and that work would be completed as expected, but once the firmware started any work submitted would just be ACKed and retired without actually doing anything. Once the firmware has started, capturing state is much harder because everything becomes dynamic and the firmware becomes a stateful object with state you can’t easily replay.
第一个问题是 GPU 固件启动后提交的渲染工作。我们可以在固件启动前提前预置工作,启动 GPU,这些工作会按预期完成;但一旦固件启动,任何提交的工作都只会收到 ACK 并被回收,而不会实际执行任何操作。固件启动后,捕获状态变得更加困难,因为一切变得动态化,固件成为一个有状态的对象,其状态难以轻易重放。
I had to step in at this point and examine Codex’s process. It turns out it was trying to replay a capture very late in the AGX’s lifecycle, where there had already been many previous events. When I told it to choose a capture far earlier in the AGX’s lifecycle, the very first capture after firmware start, Codex was able to almost immediately discover the issue (it was missing a single byte descriptor). This took a few days.
我不得不在此时介入并检查 Codex 的处理过程。原来,它试图在 AGX 生命周期的很晚阶段进行重放捕获,那时已经发生了许多先前的事件。当我告诉它选择 AGX 生命周期中更早的捕获点——即固件启动后的第一次捕获——Codex 几乎立即发现了问题(它缺少一个单字节描述符)。这花了几天的时间。
The second, and only major blocking, issue was compute. The AGX, broadly, supports two kinds of work: compute and render. In the regular GUI path, compute work is only scheduled after a significant amount of render work was already executed. Thus, it took a long time to get a clean capture of a compute workload, and when Codex finally did it was 336 MB and impossible to replay (it tried, for a long time). It also tried to construct the objects itself by looking at the capture, and spent over a week doing this, but was ultimately unsuccessful. There was just too much nonsense to sift through. This was exacerbated by issues on my side– after getting render working, I expected that submitting compute work would be simpler (the firmware ABI for compute is indeed simpler, so I was correct here), but lost my humility and thought it would be a cakewalk that would only take a few hours. Thus, I didn’t scaffold out the task properly for the LLM.
第二个,也是唯一的主要阻塞问题是计算。AGX 总体上支持两种工作:计算和渲染。在常规 GUI 路径中,计算工作仅在大量渲染工作已经执行之后才会被调度。因此,要获取计算负载的干净捕获需要很长时间,而当 Codex 最终做到时,捕获大小为 336 MB 且无法重放(它尝试过,但花了很长时间)。它还试图通过查看捕获来自行构建对象,并为此花费了一周多的时间,但最终未能成功。需要筛选的杂乱信息实在太多。这一问题因我自身的问题而加剧——在让渲染正常工作后,我以为提交计算工作会更简单(计算方面的固件 ABI 确实更简单,所以我在这点上没错),但我失去了谦逊,以为这只是一项只需几小时就能轻松完成的任务。因此,我没有为 LLM 妥善搭建任务框架。
The fix actually was given to me in another Codex session. In essence:
解决方案实际上是在另一次 Codex 会话中给我的。本质上:
- Disable the GUI by booting into single-user mode; this means no render work would be done.
- Install a LaunchDaemon to run at the earliest possible point, the moment Metal (Apple’s proprietary graphics framework) became available.
- Run a tiny Metal program that we supplied
- 通过启动到单用户模式禁用 GUI;这意味着不会进行任何渲染工作。
- 安装一个 LaunchDaemon,以便在尽可能早的时间点运行,即 Metal(Apple 专有的图形框架)可用之时。
- 运行我们提供的一个小型 Metal 程序
更进一步:量化金融体系
看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力