Mach-O 実行ファイル形式
macOS/iOS の Mach-O 実行ファイル、dylib またはバンドル。
- MIME タイプ
- application/x-mach-binary
- 拡張子
- .dylib, .bundle, .o
- カテゴリ
- 実行ファイル・バイトコード
マジックナンバー(ファイル署名)
| 0x00000000 | CFFAEDFE | ···· | Mach-O 64-bit LE magic |
Mach-O 実行ファイル形式について
Mach-O は macOS と iOS のネイティブなバイナリ形式で、ELF と違って固定長のテーブルを一切持ちません。7〜8 個のフィールドからなるヘッダの後ろに、自分の種別とサイズを名乗る可変長のロードコマンドが並び、ローダーはそれを順に歩いていきます。セグメントの写像、動的リンカのパス、最低 OS バージョン、エントリポイント、コード署名。これらのどれにも予約枠を与えずに 1 つのファイルへ収められるのは、この設計のおかげです。マジックナンバーはエンディアンとワード幅の指標も兼ねていて、CF FA ED FE が 64 ビットのリトルエンディアン、CE FA ED FE が 32 ビット、バイト順が逆の形はビッグエンディアンの PowerPC 時代のものです。
ヘッダーの構造(バイト単位)
| オフセット | 長さ | フィールド | 意味 |
|---|---|---|---|
| 0 | 4 | magic | 64 ビットのリトルエンディアンイメージなら、ディスク上では CF FA ED FE。リトルエンディアンで読めば値 0xFEEDFACF です。CE FA ED FE が 32 ビット形式(0xFEEDFACE)、FE ED FA CF と FE ED FA CE は PowerPC のバイナリが持つビッグエンディアン版。つまりこの 4 バイトだけで、形式もワード幅もバイト順も同時に分かります。 |
| 4 | 4 | cputype | x86-64 が 0x01000007、arm64 が 0x0100000C。上位ビットの 0x01000000 が 64 ビットを意味し、下位ビットが基本アーキテクチャ(x86 が 7、ARM が 12)です。同じアーキテクチャの 32 ビットビルドではそのフラグが落ちるので、7 単体なら i386、12 単体なら 32 ビット ARM です。 |
| 8 | 4 | cpusubtype | アーキテクチャ内の細分で、上位ビットは機能フラグとして使われます。Apple シリコンのバイナリでは 0x80000002 がよく現れます(サブタイプ 2 の arm64e に上位フラグが立った形)。arm64e のシステムバイナリと普通の arm64 アプリを分けているのがこのフィールドです。 |
| 12 | 4 | filetype | 1 が MH_OBJECT(.o)、2 が MH_EXECUTE、6 が MH_DYLIB、8 が MH_BUNDLE、ほかにデバッグ用の相棒である MH_DSYM。手元にあるものがプログラムなのかライブラリなのか読み込み可能なプラグインなのかに、バイトレベルで答えるのがこれです。実際には .dylib も .so も拡張子なしも使われますから。 |
| 16 | 4 | ncmds | ヘッダの後ろに続くロードコマンドの数。ちょっとしたコマンドラインツールで 15〜20 個ほど、大きなフレームワークならもっと多くなります。インデックスが無いので、どれも順番に歩いて処理されます。 |
| 20 | 4 | sizeofcmds | 全ロードコマンドが占める合計バイト数。各コマンドの cmdsize をたどって合計すると、ちょうどこの値に着地しなければなりません。この形式が持つ単体の整合性チェックとして最も強力で、編集された疑いのあるファイルにまず走らせるべき検査です。 |
| 24 | 4 | flags | 属性ビット。イメージが PIE かどうか、2 レベル名前空間の解決を使うかどうか、未定義シンボルが無いかどうか。今どきの典型的な実行ファイルなら 0x00200085 のような値が読めます。 |
| 28 | 4 | reserved | 64 ビットのヘッダにだけあり、常にゼロです。後続を 8 バイト境界に揃えるためだけに存在するので、最初のロードコマンドは 64 ビットファイルでは 32、32 ビットファイルでは 28 に来ます。 |
| 32 | variable | First load command | どのロードコマンドも 4 バイトの cmd と 4 バイトの cmdsize で始まるので、読み手は理解できないコマンドを読み飛ばせます。先頭は通常 LC_SEGMENT_64(0x19)で、切り分けで効いてくるのは LC_LOAD_DYLIB、LC_MAIN、LC_CODE_SIGNATURE、LC_BUILD_VERSION あたりです。 |
似た形式との見分け方
| 紛らわしい形式 | 見分け方 |
|---|---|
| Mach-O(ユニバーサル) | ユニバーサルバイナリは、フィールドが増えた Mach-O ファイルではありません。完全な Mach-O イメージを複数抱えたラッパーです。先頭は CF FA ED FE ではなく CA FE BA BE(ビッグエンディアン)で、単体バイナリのマジックナンバーはもっと奥、各 fat_arch エントリが示すオフセットに現れます。fat のファイルを単体として読むと、いきなり意味不明な cputype が出てきます。 |
| Java class ファイル | 有名な衝突ですが、ぶつかるのは fat のほうだけです。Java の class ファイルも CA FE BA BE で始まります。決着を付けるのは次の 4 バイトで、class ファイルの major_version(6〜7 バイト目)は 45 以上、一方 fat バイナリの nfat_arch は小さな個数なので、その上位半分はゼロになります。 |
| ELF 実行ファイル | どちらもオブジェクトファイルに .o を使い、共有ライブラリも素っ気ない名前で呼ぶので、拡張子は助けになりません。Linux でビルドした .so と macOS でビルドした .dylib は、ディレクトリ一覧では見分けが付きません。決めるのはオフセット 0 で、7F 45 4C 46 なら ELF、CF FA ED FE なら Mach-O です。なお Mach-O のマジックナンバーはディスク上ではバイト順が逆に格納されるので、文書が 0xFEEDFACF と書くものが hex ダンプでは CF FA ED FE と見えます。 |
| Big-endian (CIGAM) Mach-O | PowerPC 時代のファイルは同じマジックナンバーをバイト入れ替えで格納します。FE ED FA CE と FE ED FA CF です。この場合はヘッダも全ロードコマンドもすべてビッグエンディアンになるので、リトルエンディアンのマジックナンバーしか知らないパーサーがこれらを読み違えることはありません。正しく拒否します。 |
よくある壊れ方
- Apple シリコン上では、その場での編集はどんなものでも致命的です。arm64 のコードはアドホック署名であっても署名されているので、テキストセグメントの 1 バイトを変えただけで LC_CODE_SIGNATURE 内のハッシュが無効になり、カーネルは変更後のコードを実行せず、起動時にプロセスを殺します。Mach-O にパッチを当てるとは署名し直すことであり、ELF のようにバイトをこっそり書き換えるのに相当する行為は存在しません。
- 途中切れ。ロードコマンドは先頭にあってきちんとパースできるので、途中までのダウンロードでもアーキテクチャとファイル種別は正しく報告され、切断より先のセグメントデータだけが欠けます。確認方法はロードコマンドを歩き、各セグメントの fileoff + filesize を実際のファイル長と突き合わせることです。
- sizeofcmds がコマンドと合わなくなっている。ncmds と sizeofcmds を直さずにロードコマンドを足したり削ったりすると、ヘッダはもっともらしいまま、走査がセグメントデータへはみ出します。ツールがエラーメッセージではなく、あり得ないロードコマンド種別を報告するのはこのためです。
- fat バイナリから長さを間違えて切り出したスライス。fat ヘッダのオフセットは合っているのに他人のサイズを使ってコピーすると、Mach-O ヘッダは完璧なのに末尾が切れたファイルができます。lipo を使わずに手でアーキテクチャを切り出したときの、いつもの結果です。
- テキストモード転送、あるいは紛れ込んだ BOM。どのバイナリでも同じ話で、改行変換は最初の 0D 0A より後ろを全部ずらし、先頭に付いた BOM はマジックナンバーをオフセット 0 から押し出します。そうなると、コードのバイトは 1 つ残らず揃っているのにファイルは認識されなくなります。
この形式の現在地
macOS と iOS の実行ファイル、フレームワーク、dylib、カーネル拡張、.o ファイルはすべて Mach-O であり、Apple のローダーが受け付ける唯一の形式なので、この場所で何かに取って代わられたことはありません。Mach を経由して NeXTSTEP から受け継いだ形式で、同じ概念に対する語彙(ロードコマンド、セグメントとセクション、dyld)が ELF と違うのはそのためです。実際に今日出荷される Mach-O のほとんどは、アプリバンドルの中の単一アーキテクチャの arm64 イメージです。ユニバーサル形式は、移行期に開発者がビルドするものであり、Homebrew や Rosetta 時代のソフトウェアが今も配っているものです。バイトを覗く側にとって Apple のコード署名規則が意味するのは、Mach-O が実質読み取り専用だということ。分解するのは自由ですが、変更したコピーは署名し直すまで起動しません。
Mach-O 実行ファイルをこのサイトで開く
どれもブラウザ内で動きます。ファイルが端末から出ることはありません。
関連する形式
Mach-O 実行ファイルと混同しやすい形式です。先頭バイトを見比べてください。
| 形式 | 先頭バイト | 拡張子 |
|---|---|---|
| Mach-O(ユニバーサル) | CA FE BA BE | .dylib |
| ELF 実行ファイル | 7F 45 4C 46 | .elf |
| PE(Windows 実行ファイル) | 4D 5A | .exe |