Mach-O(universal/fat)檔案格式
把多個架構包在一起的 macOS universal(fat)Mach-O 執行檔。
- MIME 類型
- application/x-mach-binary
- 副檔名
- .dylib, .bundle
- 分類
- 執行檔與位元碼
Magic number(檔案簽章)
| 0x00000000 | CAFEBABE | ···· | 'CAFEBABE' fat magic |
關於 Mach-O(universal/fat)格式
universal binary 是個外殼,不是一個執行檔:8 位元組的標頭之後,每個架構各有一筆 20 位元組的描述子,再後面才是完整、彼此獨立的 Mach-O 映像本體,各自對齊在描述子指名的位移上。所以 universal 檔案的大小大致就是各片相加,而不是什麼精巧的合併;lipo 也因此能加減一個架構而完全不動到任何程式碼。整個 fat 標頭一律是大端序,不管裡面的 slice 是什麼——這是 NeXT 年代的決定,之所以留到現在,是因為這個格式原封不動撐過了三次 CPU 轉換。
檔頭結構,一個位元組一個位元組看
| 位移 | 長度 | 欄位 | 意義 |
|---|---|---|---|
| 0 | 4 | magic (FAT_MAGIC) | CA FE BA BE,以大端序讀是 0xCAFEBABE——和 Java class 檔一模一樣的 4 個位元組。變體 CA FE BA BF(FAT_MAGIC_64)代表底下的架構描述子改用 64 位元的位移與大小,給超過 4 GB 的執行檔用。 |
| 4 | 4 | nfat_arch | 後面有幾筆架構描述子,大端序。實際上是 1 到 4 左右——兩個是壓倒性的常態,x86-64 加 arm64。正因為這個數字很小,它的高位兩個位元組是 0,而這正是把這個標頭和 Java class 檔區分開來的關鍵性質。 |
| 8 | 4 | fat_arch[0].cputype | 第一片的 CPU:0x01000007 是 x86-64、0x0100000C 是 arm64,沒有 0x01000000 旗標的則是 32 位元形式。建置系統要判斷這個檔案支不支援眼前這台機器,讀的就是這個欄位。 |
| 12 | 4 | fat_arch[0].cpusubtype | 該架構內的變體——x86-64 ALL 是 3,arm64e 是 0x80000002。兩片可以有相同的 cputype、只差在這裡,arm64 和 arm64e 能同時存在就是靠這個。 |
| 16 | 4 | fat_arch[0].offset | 這一片自己的 Mach-O 映像從哪個檔案位移開始,大端序。seek 過去應該要看到 CF FA ED FE。片內所有東西都是從那個點起算,不是從 fat 檔案的開頭起算,所以一片可以逐位元組複製出來、照樣跑得動。 |
| 20 | 4 | fat_arch[0].size | 那一片的位元組長度。最後一個架構的位移加大小通常就等於檔案大小;再後面的東西不是填補就是追加的簽章。 |
| 24 | 4 | fat_arch[0].align | 以 2 的次方表示的對齊——14 代表這一片從 16384 位元組的邊界開始,那正是 Apple silicon 的頁面大小。這就是為什麼片與片之間會有一大段零位元組,也是為什麼 universal binary 比各部分相加還大一點。 |
| 28 | 20 | fat_arch[1] | 下一筆描述子,同樣五個欄位、同樣順序。描述子固定 20 個位元組(FAT_MAGIC_64 形式則是 32),所以第 n 筆從 8 + 20n 開始,而且沒有清單結束標記——nfat_arch 是唯一的邊界。 |
怎麼跟長得像的格式分開
| 容易搞混的 | 怎麼分 |
|---|---|
| Java class 檔 | 這個撞號讓 magic number 對照表困擾了三十年:兩者開頭都是 CA FE BA BE。把位元組 4-7 當成兩個 16 位元值來讀。Java class 檔是 minor_version 接 major_version,而任何真實的 class 檔 major_version 至少是 45。fat binary 那個位置放的是 nfat_arch,一個小數字,所以 major_version 該在的那兩個位元組是 0。先檢查 major_version 是否非零,這件事就乾乾淨淨解決了。 |
| Mach-O 執行檔 | thin 的 Mach-O 開頭是 CF FA ED FE 或 CE FA ED FE,標頭欄位是小端序;fat 外殼開頭是 CA FE BA BE,每個欄位都是大端序。如果你把某一片抽出來單獨看,它就是一個 thin 執行檔——fat 標頭不會跟著複製過去,而這是正確的行為。 |
| FAT_MAGIC_64 (CAFEBABF) | 和 32 位元形式只差一個位元,而搞混它們是無聲的、不是大聲的:描述子變成 32 個位元組而不是 20,位移與大小都是 64 位元。假設短版面配置的解析器,會從第一筆描述子的中間讀出第二個架構的欄位,然後報出一堆看起來很像樣的垃圾。 |
| Java JAR | 值得一提,因為這個撞號通常就是這樣找上門的:JAR 是 ZIP(50 4B 03 04),裡面那個 CA FE BA BE 是一個壓縮過的 class 檔,跟 Apple 一點關係都沒有。位移 0 出現什麼,那才算數。 |
常見的壞法
- 最後一片被截斷。fat 標頭在最前面,把每個架構的位移與大小都列了出來,所以下載一半的檔案照樣回報「universal binary,2 個架構」,而最後一片的位元組提早結束。把最後一筆描述子的位移加大小和實際檔案大小比一比。
- 描述子和實際的片對不上了。就地編輯某一片、長度變了卻沒更新 fat 標頭,下一個架構的位移就會指到前一片的中間,而第二片的 magic 也不再是 CF FA ED FE。每個 slice 位移都應該正好落在一個 Mach-O magic 上。
- 前面被塞了垃圾。錯誤頁或 BOM 被存在執行檔前面,每個 slice 位移都偏移了同樣的量,所以檔案還是被認成 CAFEBABE 開頭的某種東西,而裡面沒有一樣解析得出來。修法是把 magic 前面的東西刪掉,不是去改描述子。
- 程式碼簽章逐片失效。每一片是各自簽章的,所以只修補其中一個架構,其他片依然有效,做出一個「在 Intel Mac 上跑得動、在 Apple silicon 上被殺掉」的執行檔,反過來也一樣——這種令人困惑的症狀常常害人跑去找硬體問題。
這個格式現在還在用嗎
universal binary 是 Apple 用來度過 CPU 轉換、又不必出兩份下載的機制:68k 到 PowerPC、PowerPC 到 Intel、Intel 到 Apple silicon,三次都跑在同一個標頭上。今天的 universal 檔案通常是 x86-64 加 arm64,多數 Mac 應用程式,以及在「只支援 Apple silicon」之前的 Homebrew bottle 裝的都是這個,Xcode 在專案同時鎖定兩者時吐的也是它。代價是體積——檔案裡帶著完整的程式副本——所以 App Store 會在下載前先做瘦身,多數自用工具現在也只出 arm64。即使如此這個格式應該不會消失,因為 arm64 和 arm64e 映像共存在同一個檔案裡靠的也是它,而且 lipo 讓「加一個架構」或「拿掉一個架構」變成改寫標頭而不是重新建置。
在這個站上打開 Mach-O(universal/fat)檔
這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。
相關格式
容易和 Mach-O(universal/fat)搞混的格式,可以比對它們的開頭位元組。
| 格式 | 開頭位元組 | 副檔名 |
|---|---|---|
| Mach-O 執行檔 | CF FA ED FE | .dylib |
| Java class 檔 | CA FE BA BE | .class |
| ELF 執行檔 | 7F 45 4C 46 | .elf |