Building a Linux GPU Driver for the M4 Mac Mini in One Month
https://codyho.dev/blog/gpu-driver/📌 一個月逆向出M4 Mac Mini的Linux GPU驅動,AI協同加速逆向工程
TL;DR:兩位工程師靠hypervisor擷取硬體trace,一個月內逆向出Apple GPU韌體並做出可跑Minecraft的Linux驅動。
正常情況下,寫一個GPU驅動要花上好幾年。Cody Ho和Niklas卻在大約一個月內,替M4 Mac Mini與MacBook Neo做出一份完全符合OpenGL ES 3.0規範的驅動,讓Chrome、Firefox能跑WebGL、Minecraft能飆到200fps。
🤔 延續上一個逆向專案,這次目標是GPU
這個專案是作者先前打造一支用來逆向macOS的hypervisor之後的延伸。GPU幾乎是任何現代系統的必需品,沒有它一切都得靠CPU渲染,效能與能耗都差上好幾個量級。他們的目標是替M4 Mac Mini與MacBook Neo做出符合規範的OpenGL(接下來還打算做Vulkan)驅動,原本樂觀地想在幾天內做完,最後花了幾週。
🧩 User Space與Kernel Space的分工
現代GPU驅動一律拆成兩塊:kernel space負責跟硬體/韌體溝通、配置緩衝區與排程,user space則負責真正理解GPU的運作方式並填滿那些緩衝區。
在Apple Silicon上,kernel driver不會直接碰硬體,而是跟一顆執行自訂RTOS「RTKit」的GPU韌體溝通,所以第一步不是碰硬體,而是先搞懂韌體的ABI。作者形容Apple的做法像是「把一個正常的kernel driver切一半,一半塞進GPU韌體裡,另一半靠共享記憶體結構跟host端溝通」,許多結構裡韌體自己擁有的欄位跟host端控制的欄位是交錯排列的,必須靠逆向工程才能搞懂哪些欄位不能亂動。作者對比了Asahi Lina過去逆向M1/M2韌體ABI的知名成果,指出A18 Pro的韌體結構比M1/M2還複雜:結構數量多了1.5倍、指標數量多了2倍,送出工作的流程也更複雜。
作者採用的方法延續M1/M2那套逆向路線:先觀察macOS實際怎麼做,重播(replay)一遍,再自己動手重現。這一切都靠先前打造的hypervisor擷取硬體trace來實現。有趣的是,當作者向LLM(Codex)描述這個「重播」策略時,Codex把它照字面執行:等待第一個韌體可見事件(稱為「kick」)、存下整份GPU記憶體狀態,重開機後把存好的狀態複製回host記憶體、執行kick、觀察輸出頁面的變化,再嘗試順著指標重建出物件。隨著多次實驗,Codex逐步減少需要複製回去的頁面數量,直到完全靠自己從頭建構出所有物件。作者也提到,Codex展現出不錯的判斷力,知道什麼時候該再戳一下硬體、什麼時候該直接跑hypervisor自己去擷取狀態。
📊 卡關兩次:render時機問題與336MB的compute trace
過程中遇到三個主要問題,都源自難以取得一份「乾淨」的host端工作紀錄。第一個問題是韌體啟動後才送出的render工作:韌體啟動前預先排入的工作能正常完成,但一旦韌體開始運作,之後送進去的工作只會被ACK後直接丟棄,不會真正執行。作者後來讓Codex改用韌體生命週期中最早的一次擷取(韌體剛啟動後的第一筆),Codex幾乎馬上就找出問題,是缺了一個byte的descriptor,這個問題花了幾天排除。
第二個、也是唯一真正卡住進度的問題是compute工作。AGX支援render與compute兩種工作,在一般GUI流程裡,compute工作要等大量render工作跑完後才會被排程,導致很難拿到一份乾淨的compute擷取。Codex最終拿到的一份compute trace高達336MB,完全無法重播,即便嘗試靠分析內容自己重建物件,也花了超過一週仍未成功,內容太雜亂難以梳理。最後的解法反而來自另一個Codex session:開機進入單使用者模式關掉GUI(就不會有render工作)、安裝一個LaunchDaemon在Metal(Apple的專屬圖形框架)剛可用的最早時機執行、跑一個極小的自製Metal程式,成功擷取並重播了這份純compute的trace。
💡 AI在逆向工程中的角色
這篇文章讀起來最有意思的地方,是Codex並非單純被動執行指令,而是能自主決定何時該繼續戳硬體、何時該換一種擷取策略,甚至在被引導到正確的擷取時間點後,很快就自己定位出缺失的位元組。作者也強調整個過程屬於乾淨室(clean room)逆向:從頭到尾沒有查看任何Apple的二進位檔案,只依賴hypervisor擷取的硬體trace以及自己撰寫的shader,且所有實驗紀錄都已公開(文中提到的twin agx-re repos),供外界驗證來源正當性。
⚠️ 驅動還沒到能交給一般使用者的階段
作者明確表示,這份驅動目前還未準備好給終端使用者使用,團隊的目標是盡快把它推向可用狀態。目前完成的是OpenGL ES 3.0的user-space驅動與對應的Linux kernel driver,Vulkan支援則是「接下來」的計畫。
🎯 實務啟示
這個案例展示了把LLM Agent用在需要大量試錯、狀態擷取與逐步重建物件的逆向工程流程中的可行性:只要人類負責把任務拆解成可驗證的小步驟(例如「先抓最早的一次擷取」),Agent就能在龐大又雜亂的硬體trace裡做出有效判斷,加速原本以年為單位的工作。
🔗 來源
- 標題:Building a Linux GPU Driver for the M4 Mac Mini in One Month
- 作者/機構:ADevWithAnIdea
- 連結:https://codyho.dev/blog/gpu-driver/
#GPU #ReverseEngineering #LinuxKernel #AppleSilicon #OpenGL #Vulkan #AIAssistedCoding #Codex #DriverDevelopment #SystemsProgramming
原始資料 Hacker News · 收集於 2026-09-16
摘要原文
I Came, I Prompted, I Left Part 2: Building a GPU Driver From Scratch in One Month Previous blog post: 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: Most importantly, the driver is fast enough to run Minecraft at 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. 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. 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: 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 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 ). 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. 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. 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: 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: What the F@!#, Apple. Note how the A18 has: 1.5x as many structs twice as many pointers a significantly more complicated process for submitting work 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 . 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. 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. 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. 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. 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. The fix actually was given to me in another Codex session. In essence: 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 Capture and replay this tiny, pure compute trace. The trace was captured successfully. (400 points, 248 comments on Hacker News)
由 claude-code/sonnet 自動生成