ByteScope

Mach-O(ユニバーサル)ファイル形式

複数アーキテクチャを束ねた macOS のユニバーサル(fat)Mach-O バイナリ。

MIME タイプ
application/x-mach-binary
拡張子
.dylib, .bundle
カテゴリ
実行ファイル・バイトコード

マジックナンバー(ファイル署名)

0x00000000CAFEBABE····'CAFEBABE' fat magic

Mach-O(ユニバーサル)形式について

ユニバーサルバイナリはバイナリではなくラッパーです。8 バイトのヘッダの後ろにアーキテクチャごとの 20 バイトの記述子が並び、その後に完全で独立した Mach-O イメージそのものが、記述子の示すオフセットにページ境界で揃えて置かれます。ユニバーサルなファイルのサイズが巧妙なマージではなく各スライスの単純な合計に近いのも、lipo がコードに一切触れずにアーキテクチャを足したり抜いたりできるのも、そのためです。fat ヘッダ全体は、中のスライスが何であろうとビッグエンディアンです。NeXT 時代の判断ですが、この形式が 3 度の CPU 移行を無改造で乗り切ったおかげで今も残っています。

ヘッダーの構造(バイト単位)

オフセット長さフィールド意味
04magic (FAT_MAGIC)CA FE BA BE、ビッグエンディアンで読めば 0xCAFEBABE。Java の class ファイルとまったく同じ 4 バイトです。変種の CA FE BA BF(FAT_MAGIC_64)は、下のアーキテクチャ記述子が 64 ビットのオフセットとサイズを使うことを意味し、4 GB を超えるバイナリのためのものです。
44nfat_arch後ろに続くアーキテクチャ記述子の数をビッグエンディアンで。現実には 1 から 4 程度で、圧倒的に多いのは x86-64 と arm64 の 2 つです。個数が小さいので上位 2 バイトはゼロになり、その性質こそがこのヘッダを Java の class ファイルから切り分けます。
84fat_arch[0].cputype最初のスライスの CPU。0x01000007 が x86-64、0x0100000C が arm64、0x01000000 のフラグが無ければ 32 ビット版です。目の前のマシンにこのファイルが対応しているかどうかを、ビルドシステムが読むのはこのフィールドです。
124fat_arch[0].cpusubtypeそのアーキテクチャ内の細分で、x86-64 ALL なら 3、arm64e なら 0x80000002。2 つのスライスが同じ cputype を持ち、ここだけが違うこともあり得ます。arm64 と arm64e が同居できるのはそのおかげです。
164fat_arch[0].offsetこのスライス自身の Mach-O イメージが始まるファイルオフセットをビッグエンディアンで。そこへシークすれば CF FA ED FE が見つかるはずです。スライス内部のすべては fat ファイルの先頭ではなくその位置を基準に測られているので、スライスをバイト単位でそのまま取り出しても動きます。
204fat_arch[0].sizeそのスライスのバイト長。最後のアーキテクチャのオフセット + サイズは通常ファイルサイズに一致し、それより後ろにあるものはパディングか追記された署名です。
244fat_arch[0].alignアライメントを 2 のべき乗で表したもの。14 ならスライスは 16384 バイト境界から始まり、これは Apple シリコンのページサイズです。スライスとスライスの間にゼロバイトが長々と続くのも、ユニバーサルバイナリが各部分の合計より少し大きくなるのもこのためです。
2820fat_arch[1]次の記述子で、同じ 5 つのフィールドが同じ順で並びます。記述子は 1 つあたり 20 バイト固定(FAT_MAGIC_64 形式なら 32 バイト)なので n 番目は 8 + 20n から始まり、リストの終端マーカーはありません。境界を決めるのは nfat_arch だけです。

似た形式との見分け方

紛らわしい形式見分け方
Java class ファイル30 年にわたってマジックナンバー表を混乱させてきた衝突で、どちらも CA FE BA BE で始まります。4〜7 バイト目を 16 ビット 2 つとして読んでください。Java の class ファイルは minor_version、major_version の順で、本物の class ファイルなら major_version は最低でも 45 です。fat バイナリはそこに nfat_arch という小さな個数が入るので、major_version に当たる位置はゼロになります。まず major_version が非ゼロかを見れば綺麗に決着します。
Mach-O 実行ファイル単体の Mach-O は CF FA ED FE か CE FA ED FE で始まり、ヘッダのフィールドはリトルエンディアンです。fat のラッパーは CA FE BA BE で始まり、全フィールドがビッグエンディアン。スライスを取り出して個別に調べれば、それは単体のバイナリです。fat ヘッダはコピーには付いてきませんが、それが正しい挙動です。
FAT_MAGIC_64 (CAFEBABF)32 ビット版とは 1 ビットしか違わず、取り違えても派手には失敗せず静かに壊れます。記述子が 20 バイトではなく 32 バイトになり、オフセットとサイズが 64 ビットになるからです。短いレイアウトを前提にしたパーサーは、2 つ目のアーキテクチャのフィールドを 1 つ目の記述子の途中から読み、もっともらしいゴミを報告します。
Java JARこの衝突が人の目に触れる経路はたいていこれなので、書いておく価値があります。JAR は ZIP(50 4B 03 04)であり、その中にある CA FE BA BE は圧縮された class ファイルであって、Apple とは何の関係もありません。効くのはオフセット 0 に何があるかです。

よくある壊れ方

  • 最後のスライスが途中で切れている。fat ヘッダは先頭にあり、全アーキテクチャのオフセットとサイズを列挙しているので、途中までのダウンロードでも「2 アーキテクチャのユニバーサルバイナリ」と正しく報告されつつ、最後のスライスのバイトだけが足りません。最後の記述子のオフセット + サイズを実際のファイルサイズと比べてください。
  • 記述子とスライスが食い違っている。スライスをその場で編集して長さが変わったのに fat ヘッダを更新しないと、次のアーキテクチャのオフセットが 1 つ前のスライスの途中を指すことになり、2 つ目のスライスのマジックナンバーが CF FA ED FE と読めなくなります。どのスライスのオフセットも、Mach-O のマジックナンバーに着地するはずです。
  • 先頭に付いたゴミ。エラーページや BOM がバイナリの前に保存されると、全スライスのオフセットが同じ分だけずれるので、ファイルは CAFEBABE 系だと識別され続けるのに、中身は何一つ解決できません。直し方は記述子の修復ではなく、マジックナンバーより前を削ることです。
  • スライスごとにコード署名が無効化される。各スライスは独立に署名されているので、片方のアーキテクチャにパッチを当てても、もう片方は有効なままです。結果、Intel Mac では動くのに Apple シリコンでは殺される(またはその逆の)バイナリができあがります。ハードウェアの問題を疑いに行かせる、あの紛らわしい症状です。

この形式の現在地

ユニバーサルバイナリは、ダウンロードを 2 種類用意せずに CPU 移行を乗り切るために Apple が使ってきた仕組みです。68k から PowerPC、PowerPC から Intel、Intel から Apple シリコン。どれも同じヘッダで走りました。今日ユニバーサルと言えばたいてい x86-64 と arm64 の組で、Apple シリコン専用ビルド以前の Mac アプリや Homebrew の bottle が持っているもの、そして Xcode が両方を対象にしたときに出すものです。代償はサイズで、ファイルがプログラムの完全な複製を抱えることになります。だから App Store は配信前に不要なスライスを削ぎますし、社内ツールの多くは今や arm64 だけを配ります。それでもこの形式が消えそうにないのは、arm64 と arm64e のイメージを 1 ファイルに同居させる手段でもあるからであり、lipo によってアーキテクチャの追加や削除が再ビルドではなくヘッダの書き換えで済むからです。

Mach-O(ユニバーサル)ファイルをこのサイトで開く

どれもブラウザ内で動きます。ファイルが端末から出ることはありません。

関連する形式

Mach-O(ユニバーサル)と混同しやすい形式です。先頭バイトを見比べてください。

形式先頭バイト拡張子
Mach-O 実行ファイルCF FA ED FE.dylib
Java class ファイルCA FE BA BE.class
ELF 実行ファイル7F 45 4C 46.elf

よくある質問

ファイルのマジックナンバーとは何ですか?

ファイル先頭の数バイトで、名前が何であろうとその形式を示すものです。PNG は必ず 89 50 4E 47 で始まり、ZIP は 50 4B 03 04 で始まります。OS もブラウザも file コマンドも、拡張子を信用せずこのバイト列を読みます。だからファイル名を変えても中身の正体は変わりません。

マジックナンバーを自分で確認する方法は?

macOS や Linux なら xxd -l 16 ファイル名 で先頭 16 バイトが表示されます。Windows なら PowerShell で Format-Hex -Path ファイル名 -Count 16 です。あるいはこのサイトのファイル形式判別ツールにドロップすれば、ローカルでバイト列を読んで形式を教えてくれます。アップロードはありません。

2 つの形式が同じマジックナンバーを持つのはなぜ?

一方が他方の上に作られているからです。docx、xlsx、pptx、apk、jar、epub はすべて中身が ZIP アーカイブなので ZIP の署名で始まり、区別するには内部のエントリを見る必要があります。Java の class ファイルと macOS のユニバーサルバイナリはどちらも CA FE BA BE で始まり、直後のフィールドで見分けます。

マジックナンバーがまったくないファイルもある?

あります。XML、HTML、JSON、SVG、CSV といったテキストベースの形式には固定のバイト署名がないため、構造から判別します。先頭の <?xml 宣言、<svg 要素、あるいは { に続く引用符付きのキーなどです。これは確定ではなく推測なので、このリファレンスではそうした形式をプローブ判別として示しています。

マジックナンバーが一致すればそのファイルは安全?

いいえ。わかるのは形式であって、中身が無害かどうかではありません。正規の PDF や docx でも悪意のあるペイロードを含んでいることがあります。特に拡張子とバイト列が食い違うときに真の形式を知るのは有用な第一段階ですが、マルウェアスキャンではありません。

Mach-O(ユニバーサル)かもしれないファイルをお持ちですか?ファイル形式判別ツールで調べる — マジックナンバーをローカルで読み取ります。アップロードは一切ありません。またはすべての形式を見る。