什麼是 Mach-O(Mach Object)格式?歷史起源、二進位結構與技術規格

摘要

Mach-O(Mach Object)是 Apple 生態系統使用的二進位檔案格式,主要用於表示目標檔案、可執行檔、動態程式庫及其他類型的二進位映像。Mach-O 提供一套結構化機制,用於描述處理器架構、檔案類型、機器指令與資料區域、符號資訊、重定位、動態連結,以及程式載入與執行所需的相關資訊。

與可執行與可連結格式(Executable and Linkable Format,ELF)不同,Mach-O 透過載入命令(Load Commands)組織大量與載入及連結相關的控制資訊。這些命令可以描述檔案中的哪些區域需要映射至記憶體、程式依賴哪些動態程式庫,以及符號表和其他連結資料的位置。

Mach-O 的歷史與 Mach 系統、NeXT 作業系統以及 Mac OS X 的形成密切相關。隨著 Apple 平台的發展,Mach-O 逐漸支援多種處理器架構,包括 PowerPC、x86-64 與 ARM64。

本文研究 Mach-O 的歷史來源、二進位檔案結構、標頭、載入命令、區段與節區、符號表、重定位機制、動態連結、通用二進位檔案、程式載入流程,以及 Apple 平台的程式碼簽署機制。

本文主要依據 Apple 公開的 XNU 原始碼、Mach-O 結構定義及動態連結器 dyld 的相關文件進行分析。文中涉及位址、配置及工具輸出的範例,若未註明實際來源,均屬技術示意,不代表特定 Mach-O 檔案的分析結果。

關鍵詞: Mach-O、Mach Object、二進位格式、可執行檔、Apple、macOS、XNU、dyld、載入命令、區段、節區、重定位、通用二進位檔案、ABI。


一、什麼是 Mach-O?

1.1. 定義

Mach-O 是一種目標檔案及二進位執行映像格式,用於儲存連結器、載入器及作業系統在建置或執行程式時所需要的資訊。

依照檔案用途,Mach-O 可以包含:

  • 已編譯的機器指令。
  • 靜態資料與唯讀資料。
  • 需要初始化的記憶體區域。
  • 符號表。
  • 重定位資訊。
  • 動態程式庫相依資訊。
  • 程式進入點。
  • 程式碼簽署資料。
  • 平台及執行環境相關中繼資料。

Mach-O 是二進位檔案格式,而不是程式語言、處理器指令集或獨立的機器碼種類。

Mach-O 所儲存的機器碼仍取決於目標處理器架構,例如 x86-64 或 ARM64。

1.2. Mach-O 的使用範圍

Mach-O 廣泛用於 Apple 平台的各類二進位產物。

類型 主要用途
目標檔案 .o 編譯與連結過程中的中間產物
可執行檔 由作業系統載入並執行的程式
動態程式庫 .dylib 提供其他程式使用的機器碼與資料
二進位套件(Bundle) 可依平台機制動態載入的程式組件
dSYM 檔案 可包含用於除錯資訊的 Mach-O 映像
系統二進位映像 部分作業系統及執行環境組件

Mach-O 使用於 macOS、iOS、iPadOS、watchOS、tvOS 及其他相關 Apple 平台。

但不同平台對二進位檔案的執行權限、程式碼簽署及載入方式,可能存在不同的技術與安全要求。

1.3. Mach-O 與 .app 應用程式套件

macOS 應用程式通常以副檔名為 .app 的套件形式分發。

例如:

Example.app/
└── Contents/
    ├── Info.plist
    ├── MacOS/
    │   └── Example
    └── Resources/

其中:

  • Example.app 是應用程式套件。
  • Info.plist 包含應用程式的描述資訊。
  • Contents/MacOS/Example 可以是一個 Mach-O 可執行檔。

因此,**Mach-O 描述的是二進位檔案本身,而非整個 .app 目錄的格式。**


二、歷史來源與發展

2.1. 與 Mach 系統的關係

Mach-O 的名稱源自 Mach Object。

Mach 是一系列在二十世紀八○年代於卡內基美隆大學(Carnegie Mellon University)研究的作業系統核心設計。

Mach 涉及工作(Task)、執行緒(Thread)、虛擬記憶體及程序間通訊等系統機制。

Mach-O 則與使用 Mach 技術的作業系統環境有關,特別是 NeXT 系統,以及後來 Apple 所採用的 Darwin 和 Mac OS X 技術體系。

不過,Mach-O 不是 Mach 核心本身,也不是 CPU 指令集的一部分。

作業系統核心與可執行檔格式屬於不同的技術層次,兩者透過程式載入和記憶體管理機制相互配合。

2.2. NeXTSTEP 與 Mac OS X

NeXTSTEP 是 NeXT 公司開發的作業系統,其基礎包含 Mach 相關技術及 Unix 系統的多項特徵。

Mach-O 與 NeXT 的軟體環境具有歷史關聯,並隨相關技術演進而成為 Apple 平台的重要二進位格式。

1997 年 Apple 收購 NeXT 後,NeXT 的多項技術成為新一代 Macintosh 作業系統的基礎。

2001 年,Mac OS X 10.0 正式發行。

Mach-O 隨之成為 Mac OS X 以及後續 macOS 發展中的重要二進位格式。

2.3. 處理器架構的演變

Mach-O 經歷 Apple 多次重要的處理器架構轉換。

歷史階段 代表架構 技術意義
NeXT 系統時期 Motorola 68k、x86 等 Mach 與 NeXT 軟體環境的早期發展
Mac OS X 早期 PowerPC Mach-O 於新一代 Macintosh 系統中廣泛使用
Intel Mac 時期 x86、x86-64 支援 Intel 處理器
Apple silicon 時期 ARM64 支援 Apple 自行設計的 ARM64 處理器

多架構支援需求,促進了通用二進位檔案(Universal Binary)的使用。

2.4. 32 位元與 64 位元 Mach-O

Mach-O 定義了 32 位元與 64 位元兩種主要的 Mach 標頭格式。

Apple 在 mach-o/loader.h 中定義:

struct mach_header;
struct mach_header_64;

兩者除了支援不同的檔案結構需求外,也具有不同的標頭大小。

對於 64 位元 Mach-O,其區段與節區結構使用 64 位元欄位描述虛擬位址及部分大小資訊。


三、Mach-O 的整體檔案結構

3.1. 結構概觀

典型 Mach-O 的邏輯構成可以表示如下:

┌─────────────────────────────────┐
│ Mach Header                     │
│ - Magic                         │
│ - CPU Type                      │
│ - File Type                     │
│ - Number of Load Commands       │
├─────────────────────────────────┤
│ Load Commands                   │
│ - Segment Commands              │
│ - Symbol Table Information      │
│ - Dynamic Linking Information   │
│ - Entry Point Information       │
├─────────────────────────────────┤
│ Segment Contents                │
│ - Machine Code                  │
│ - Read-only Data                │
│ - Writable Data                 │
├─────────────────────────────────┤
│ Link-edit Data                  │
│ - Symbols                       │
│ - Fixups / Bindings             │
│ - Other Metadata                │
├─────────────────────────────────┤
│ Code Signature, if present      │
└─────────────────────────────────┘

此圖僅用於說明概念上的資料分類。

實際內容未必依照圖示順序連續排列。各個資料區域的位置,應依照相關結構所記錄的偏移量及大小判定。

3.2. 三項主要結構

Mach-O 可以從三個核心部分開始理解。

Mach 標頭(Mach Header)

用於描述檔案格式、處理器架構、檔案類型及載入命令的基本資訊。

載入命令(Load Commands)

描述載入器、連結器及分析工具需要理解的二進位結構及相關中繼資料。

區段與節區(Segments and Sections)

負責組織機器碼、資料及其他需要在連結或載入階段處理的內容。

符號表、重定位與動態連結資料則透過相關結構進行定位和解讀。


四、Mach 標頭(Mach Header)

4.1. 64 位元標頭結構

Apple 於 mach-o/loader.h 中定義:

struct mach_header_64 {
    uint32_t magic;
    cpu_type_t cputype;
    cpu_subtype_t cpusubtype;
    uint32_t filetype;
    uint32_t ncmds;
    uint32_t sizeofcmds;
    uint32_t flags;
    uint32_t reserved;
};

其中 cpu_type_t 與 cpu_subtype_t 在相關定義中使用 32 位元整數表示。

mach_header_64 的大小為 32 位元組。

4.2. 欄位配置

偏移量 欄位 大小 說明
0x00 magic 4 位元組 格式識別值
0x04 cputype 4 位元組 CPU 架構
0x08 cpusubtype 4 位元組 CPU 子類型
0x0C filetype 4 位元組 Mach-O 檔案類型
0x10 ncmds 4 位元組 載入命令數量
0x14 sizeofcmds 4 位元組 載入命令總大小
0x18 flags 4 位元組 檔案屬性旗標
0x1C reserved 4 位元組 保留欄位

總大小:

32 位元 Mach-O 標頭包含前七個欄位,不含 reserved,因此大小為 28 位元組。

4.3. 魔術數字(Magic Number)

Mach-O 使用特定數值辨識標頭格式及位元組順序。

常數 數值
MH_MAGIC 0xFEEDFACE
MH_CIGAM 0xCEFAEDFE
MH_MAGIC_64 0xFEEDFACF
MH_CIGAM_64 0xCFFAEDFE

CIGAM 是對應 magic 值的位元組順序反轉形式。

例如,在小端序(Little-endian)系統中,32 位元數值 0xFEEDFACF 會依序儲存為:

CF FA ED FE

因此,解析 Mach-O 時,必須正確處理實際位元組順序,而不能只比較以文字表示的十六進位數值。

4.4. 檔案類型(File Type)

filetype 欄位用於識別 Mach-O 的用途。

常數 數值 說明
MH_OBJECT 0x1 可重定位目標檔案
MH_EXECUTE 0x2 可執行檔
MH_CORE 0x4 核心傾印映像
MH_DYLIB 0x6 動態程式庫
MH_DYLINKER 0x7 動態連結器
MH_BUNDLE 0x8 動態載入套件
MH_DSYM 0xA 除錯資訊映像
MH_KEXT_BUNDLE 0xB 具有歷史及平台限制的核心延伸套件類型
MH_FILESET 0xC 包含多個 Mach-O 組件的檔案集合映像

這些常數定義於 Apple 的 Mach-O 標頭檔。

並非所有檔案類型都可以像一般應用程式一樣直接執行。


五、載入命令(Load Commands)

5.1. 定義

載入命令是 Mach-O 最重要的結構特徵之一。

Mach 標頭後方包含一系列載入命令,用於描述檔案中的相關區域,以及載入、連結或其他處理所需的資訊。

Apple 定義的基本結構為:

struct load_command {
    uint32_t cmd;
    uint32_t cmdsize;
};

兩個欄位總共佔用 8 位元組。

  • cmd:載入命令的類型。
  • cmdsize:整個命令的大小,包括相關附加資料。

解析器通常透過 cmdsize 計算下一個載入命令的位置。

5.2. 對齊規則

依照 Apple 的 Mach-O 結構定義:

  • 32 位元 Mach-O 的載入命令大小須為 4 位元組的整數倍。
  • 64 位元 Mach-O 的載入命令大小須為 8 位元組的整數倍。

必要時使用值為零的填充位元組。

因此,若某個 64 位元載入命令的原始資料長度為 27 位元組,其最終大小應依對齊要求調整。

若 cmdsize 不合法,可能導致解析器讀取超出檔案範圍,甚至在錯誤的實作中出現無限迴圈。

5.3. 常見載入命令

載入命令 主要功能
LC_SEGMENT 描述 32 位元區段
LC_SEGMENT_64 描述 64 位元區段
LC_SYMTAB 符號表與字串表的位置
LC_DYSYMTAB 傳統動態連結相關符號資訊
LC_LOAD_DYLIB 宣告動態程式庫相依性
LC_ID_DYLIB 宣告動態程式庫識別資訊
LC_LOAD_DYLINKER 指定動態連結器
LC_MAIN 提供程式進入點資訊
LC_RPATH 補充程式庫搜尋路徑
LC_UUID 二進位映像的唯一識別資訊
LC_CODE_SIGNATURE 程式碼簽署資料的位置
LC_BUILD_VERSION 建置平台、最低系統版本與 SDK 資訊
LC_DYLD_EXPORTS_TRIE 匯出符號 Trie 結構的位置
LC_DYLD_CHAINED_FIXUPS 鏈式修正資訊
LC_FILESET_ENTRY 檔案集合中的組件資訊

不是每一個 Mach-O 都包含上述所有命令。

不同檔案類型,例如 MH_OBJECT 與 MH_EXECUTE,具有不同的技術需求。

5.4. 動態連結器必須理解的命令

Mach-O 定義:

#define LC_REQ_DYLD 0x80000000

此旗標用來標示動態連結器必須理解的載入命令類型。

依照 Apple 的定義,如果 dyld 遇到帶有必要性旗標、但自身無法辨識的命令,可能拒絕載入該映像。

這種機制使格式可以加入具有必要語義的新命令,而不讓舊版載入器錯誤地忽略它們。


六、區段(Segment)與節區(Section)

6.1. 區段的概念

區段用於描述二進位檔案與程序虛擬記憶體之間的映射關係。

64 位元 Mach-O 使用以下結構:

struct segment_command_64 {
    uint32_t cmd;
    uint32_t cmdsize;
    char segname[16];
    uint64_t vmaddr;
    uint64_t vmsize;
    uint64_t fileoff;
    uint64_t filesize;
    vm_prot_t maxprot;
    vm_prot_t initprot;
    uint32_t nsects;
    uint32_t flags;
};

此結構的固定部分為 72 位元組。

如果區段包含節區,其 section_64 紀錄會接續在該區段命令之後。

6.2. 區段命令欄位

欄位 大小 說明
cmd 4 位元組 命令類型
cmdsize 4 位元組 命令及節區資料的總大小
segname 16 位元組 區段名稱
vmaddr 8 位元組 虛擬記憶體位址
vmsize 8 位元組 記憶體配置大小
fileoff 8 位元組 檔案偏移量
filesize 8 位元組 檔案中儲存的資料大小
maxprot 4 位元組 允許的最大記憶體保護權限
initprot 4 位元組 初始記憶體保護權限
nsects 4 位元組 節區數量
flags 4 位元組 區段旗標

在許多情況下:

當記憶體大小大於檔案中實際儲存的內容時,額外區域可能依照載入規則以零值初始化。

6.3. 節區的概念

節區用於組織區段內的機器碼、資料或中繼資訊。

Mach-O 64 位元節區定義如下:

struct section_64 {
    char sectname[16];
    char segname[16];
    uint64_t addr;
    uint64_t size;
    uint32_t offset;
    uint32_t align;
    uint32_t reloff;
    uint32_t nreloc;
    uint32_t flags;
    uint32_t reserved1;
    uint32_t reserved2;
    uint32_t reserved3;
};

固定結構大小為 80 位元組。

欄位 功能
sectname 節區名稱
segname 所屬區段名稱
addr 節區的虛擬位址
size 節區大小
offset 實際檔案資料的偏移量,適用於具有檔案內容的節區
align 對齊需求的指數
reloff 重定位紀錄的偏移量
nreloc 重定位紀錄數量
flags 節區類型與屬性
reserved1–3 依節區類型而有不同用途的附加欄位

6.4. 常見區段

區段名稱 典型內容
__PAGEZERO 不映射的低位址範圍,用於降低空指標存取風險
__TEXT 可執行程式碼與相關唯讀資料
__DATA 可寫入資料及相關結構
__DATA_CONST 載入或連結處理後可施加保護的資料
__LINKEDIT 符號、修正及其他連結資訊

上述名稱常見於 Apple 平台,但並不代表 Mach-O 只能使用這些區段名稱。

實際配置可能隨作業系統、連結器版本及處理器架構而改變。

6.5. 常見節區

在 __TEXT 中:

節區 典型用途
__text 機器指令
__cstring C 字串
__const 部分常數資料
__stubs 特定連結機制使用的呼叫存根

在資料區段中:

節區 典型用途
__data 已初始化資料
__bss 未初始化資料
__common 傳統共用資料物件
__la_symbol_ptr 傳統延遲符號指標區域

節區的名稱、存在與位置均可能因工具鏈和 ABI 而不同。


七、符號表與重定位機制

7.1. 符號表(Symbol Table)

Mach-O 可以透過符號表描述函式、資料及其他可被識別的程式位置。

LC_SYMTAB 使用以下結構:

struct symtab_command {
    uint32_t cmd;
    uint32_t cmdsize;
    uint32_t symoff;
    uint32_t nsyms;
    uint32_t stroff;
    uint32_t strsize;
};

其中:

  • symoff:符號表在檔案中的偏移量。
  • nsyms:符號數量。
  • stroff:字串表的偏移量。
  • strsize:字串表大小。

64 位元 Mach-O 傳統符號紀錄使用 nlist_64 結構。

主要欄位包括:

  • n_strx
  • n_type
  • n_sect
  • n_desc
  • n_value

nlist_64 的大小為 16 位元組。

7.2. 符號解析

假設程式需要呼叫外部程式庫提供的 calculate 函式。

當編譯器產生目前模組的目標檔案時,可能尚無法得知該函式在最終映像中的位置。

連結器負責根據符號名稱、可見性、繫結規則及其他條件,建立對應關係。

若符號由動態程式庫提供,部分資訊可能保留至 dyld 載入或連結階段處理。

7.3. 重定位(Relocation)

重定位是指根據最終位址或符號資訊,調整尚未確定的位址參照。

在 MH_OBJECT 檔案中,節區可以透過:

reloff
nreloc

描述重定位紀錄的位置與數量。

具體重定位種類取決於 CPU 架構。

例如,x86-64 與 ARM64 對函式呼叫和相對位址的指令編碼不同,因此其重定位類型和運算規則也不同。

7.4. 重定位與修正(Fixup)

重定位與修正具有密切關係,但不應在所有情況下視為完全相同的術語。

重定位(Relocation)

通常用於描述在連結或載入過程中,需要根據位址與符號資訊調整參照的紀錄或運算。

修正(Fixup)

是更廣泛的用語,表示需要被調整的二進位位置或相關操作。

在現代 Mach-O 中,Fixup 亦可指 dyld 使用的特定資料結構,例如鏈式修正(Chained Fixups)。

因此,傳統 Mach-O 目標檔案的重定位紀錄,與現代執行映像中的鏈式修正資訊,不應被視為相同的資料結構。


八、Mach-O 的動態連結機制

8.1. 動態連結器 dyld

dyld 是 Apple 平台動態連結機制的重要組件。

當可執行程式需要使用外部動態程式庫時,Mach-O 會記錄相關資訊,以協助系統定位程式庫並處理二進位參照。

常見命令包括:

LC_LOAD_DYLIB
LC_LOAD_DYLINKER
LC_RPATH

這些命令分別與動態程式庫相依性、動態連結器及程式庫搜尋路徑有關。

8.2. 位址重新基準化(Rebase)

重新基準化用於處理映像實際載入位置與原先假定位址之間的差異。

若映像中的某些指標指向同一映像內的資料,當載入基準發生變化時,這些指標可能需要相應調整。

這類操作通常稱為 Rebase。

8.3. 符號繫結(Bind)

符號繫結是將某個參照與相應的符號定義連接起來的過程。

在動態連結環境中,符號可能由另一個程式庫提供。

dyld 必須根據二進位中記錄的資訊及平台規則,處理相應的符號參照。

因此,可以概括為:

  • Rebase: 主要處理映像位置變化造成的位址調整。
  • Bind: 主要處理對其他映像或程式庫所提供符號的參照。

8.4. 鏈式修正(Chained Fixups)

現代 Mach-O 支援:

LC_DYLD_CHAINED_FIXUPS

該命令指向以鏈式結構編碼的修正資訊。

相較於單純依賴傳統重定位清單,鏈式修正可以透過特定資料結構描述需要處理的指標及其關聯資訊。

相關內容可能包括:

  • 鏈式修正的起始資訊。
  • 指標格式。
  • 匯入符號資料。
  • Rebase 與 Bind 資訊。
  • 不同處理器架構使用的指標表示方式。

鏈式修正並非所有歷史版本 Mach-O 都必須具有的結構。

8.5. 匯出符號 Trie

Mach-O 亦可以使用:

LC_DYLD_EXPORTS_TRIE

指定匯出符號資料的 Trie 結構。

Trie 是一種樹狀資料結構,可利用字串共同前綴組織資訊。

在匯出符號的情境中,這有助於以結構化方式儲存符號名稱及相關資料。

8.6. dyld 共享快取

Apple 還使用 dyld 共享快取(dyld Shared Cache)來組織部分系統動態程式庫。

共享快取並不等同於單一 Mach-O 檔案。

它可以包含或組織多個 Mach-O 映像,並搭配額外的結構與中繼資料,以支援系統程式庫的載入和相關最佳化。

部分位址修正與連結處理可在快取建置階段事先完成,減少執行階段需要進行的工作。

因此,dyld Shared Cache 應被視為與 Mach-O 密切相關、但具有自身格式及管理機制的系統組件。


九、通用二進位檔案(Universal Binary)

9.1. 定義

通用二進位檔案是指在同一個容器檔案中包含多種處理器架構版本的二進位程式。

例如,某個 macOS 應用程式可能同時支援:

  • x86-64:Intel Mac。
  • ARM64:Apple silicon Mac。

開發者可以分別編譯兩種架構的程式,再將它們整合至同一檔案中。

9.2. 通用二進位檔案的組成

Universal Binary 亦常稱為 Fat Binary。

其外層結構提供架構清單與各個二進位映像的位置。

┌───────────────────────────────┐
│ Fat Header                    │
├───────────────────────────────┤
│ Architecture Table            │
│ ├─ x86_64                     │
│ └─ arm64                      │
├───────────────────────────────┤
│ Mach-O Slice: x86_64          │
├───────────────────────────────┤
│ Mach-O Slice: arm64           │
└───────────────────────────────┘

每個架構切片(Architecture Slice)均包含其對應架構的 Mach-O 映像。

外層架構表不會取代每個切片內部的 Mach 標頭。

9.3. Fat Header

傳統 Fat Header 定義如下:

struct fat_header {
    uint32_t magic;
    uint32_t nfat_arch;
};

其中:

  • magic:Fat 格式識別值。
  • nfat_arch:架構紀錄數量。

傳統 32 位元架構紀錄為:

struct fat_arch {
    cpu_type_t cputype;
    cpu_subtype_t cpusubtype;
    uint32_t offset;
    uint32_t size;
    uint32_t align;
};

此結構佔用 20 位元組。

此外,Mach-O 生態系統也具有使用較大偏移量及大小欄位的 Fat 64 位元格式。

9.4. 使用 lipo 工具

Apple 提供 lipo 工具,用於檢查與操作通用二進位檔案。

查看架構:

lipo -archs program

查看資訊:

lipo -info program

擷取 ARM64 切片:

lipo -thin arm64 program -output program-arm64

合併兩個架構版本:

lipo -create program-x86_64 program-arm64 \
    -output program-universal

最後一項操作要求輸入檔案具備相容的格式與架構資訊。

9.5. 在 Apple 架構轉換中的作用

通用二進位檔案在 Apple 轉換處理器架構的歷史中具有重要作用。

無論是從 PowerPC 轉移至 Intel,或從 Intel 轉移至 Apple silicon,多架構封裝都能讓同一套軟體支援不同世代的硬體。

但 Universal Binary 並不會自動將 x86-64 機器碼轉換成 ARM64 機器碼。

每個架構切片仍必須由相應的編譯工具產生。


十、程式進入點與載入流程

10.1. 程式進入點(Entry Point)

可執行檔必須提供必要資訊,使系統能夠確定程式的啟動位置或啟動方式。

Mach-O 的一種現代機制是:

LC_MAIN

其結構定義如下:

struct entry_point_command {
    uint32_t cmd;
    uint32_t cmdsize;
    uint64_t entryoff;
    uint64_t stacksize;
};
欄位 意義
cmd 載入命令類型
cmdsize 命令大小
entryoff 依命令規定表示的進入點偏移量
stacksize 要求的初始堆疊大小,若為零則依平台預設機制處理

在使用 LC_MAIN 的模式中,entryoff 依照與 __TEXT 區段相關的偏移規則表示。

因此,entryoff 不應直接被視為 C 語言函式 main 的絕對虛擬位址。

10.2. 載入流程概觀

Mach-O 的載入與執行過程可概括如下:

Mach-O 檔案
     │
     ▼
驗證格式與處理器架構
     │
     ▼
讀取 Header 與 Load Commands
     │
     ▼
建立記憶體映射
     │
     ▼
處理動態連結資訊
     │
     ▼
準備執行環境
     │
     ▼
移交控制權至啟動程式碼
     │
     ▼
程式開始執行

此流程為概念上的簡化。

實際工作可能由作業系統核心、dyld、語言執行階段及其他啟動組件共同完成,並非完全按照單一線性順序運作。

10.3. Mach-O 與位址空間配置隨機化

位址空間配置隨機化(Address Space Layout Randomization,ASLR)是一種透過改變程式記憶體配置位置來提升安全性的機制。

當 Mach-O 映像被載入至不同於原始假定位址的位置時,系統可能需要調整相關指標。

這些操作可透過相應的修正資訊與載入機制完成。

但是,ASLR 並非 Mach-O 格式本身獨立提供的功能。

其實際支援程度仍受到作業系統、架構、二進位屬性及安全政策影響。


十一、程式碼簽署與安全機制

11.1. 程式碼簽署(Code Signing)

Mach-O 可以透過以下載入命令記錄程式碼簽署資料的位置:

LC_CODE_SIGNATURE

此命令包含定位簽署資料所需的資訊。

它並不表示整份簽章資料直接儲存在載入命令結構之內。

11.2. 程式碼簽署的用途

程式碼簽署可配合平台機制,用於:

  • 驗證程式碼完整性。
  • 確認簽署身分資訊。
  • 支援作業系統安全政策。
  • 配合軟體發布與安裝程序。

不同 Apple 平台對程式碼簽署的要求並不完全相同。

因此,不能僅因 Mach-O 包含 LC_CODE_SIGNATURE,就推斷作業系統一定允許執行該檔案。

11.3. 二進位格式與執行權限

Mach-O 負責儲存程式碼及相關中繼資料。

真正的驗證、執行授權與安全政策,則由作業系統及相關安全機制決定。

換言之,格式結構正確的 Mach-O,不一定是符合平台執行政策的有效程式。


十二、Mach-O、ELF 與 PE/COFF 比較

12.1. 主要差異

比較項目 Mach-O ELF PE/COFF
主要平台 Apple Linux 與多種 Unix 系統 Windows
格式識別 Magic Number ELF Magic MZ/PE 與 COFF
主要標頭 Mach Header ELF Header DOS/PE/COFF Header
記憶體映射描述 Segment Load Commands Program Headers PE 節區表及相關映像機制
程式碼與資料組織 Segments、Sections Segments、Sections Sections
符號資訊 Mach-O 符號與連結資訊 ELF 符號表 COFF 符號及 PE 匯入/匯出資料
動態連結 dyld ELF 動態連結機制 Windows Loader
多架構封裝 Fat/Universal Binary 一般情況下一個映像對應單一架構 一般情況下一個映像對應單一架構
平台版本資訊 相關 Load Commands ABI、Notes 與平台機制 Header、Subsystem 與中繼資料

12.2. 結構設計的差異

Mach-O 的特色在於透過不同類型的載入命令,描述多種載入及連結資料。

ELF 則明確提供兩種重要視角:

  • 以節區為核心的連結視角。
  • 以程式標頭為核心的執行視角。

PE 則透過多層標頭、節區表及資料目錄,組織可執行映像與動態連結資訊。

三者具有相似的基本目的,但內部格式不能直接互換。

12.3. 修改標頭能否將 Mach-O 轉為 ELF?

一般而言,不能只透過修改 Magic Number 或標頭就完成轉換。

原因在於三者還存在以下差異:

  • 節區與區段的資料結構。
  • 重定位類型。
  • 符號解析規則。
  • 動態程式庫相依資訊。
  • ABI 與函式呼叫慣例。
  • 進入點規範。
  • 作業系統載入機制。

若要將 macOS 程式移植至 Linux,通常需要為目標平台重新編譯及連結,並處理相關作業系統 API 的相容性問題。


十三、使用工具分析 Mach-O

13.1. 建立測試程式

建立 hello.c:

#include <stdio.h>

int mainvoid {
    puts("Hello, Mach-O!");
    return 0;
}

在已安裝適當開發工具的 macOS 環境中:

clang hello.c -o hello

輸出通常是符合目前目標架構的 Mach-O 可執行檔。

13.2. 辨識檔案類型

file hello

可以查看格式、架構及部分二進位屬性。

具體輸出會依作業系統版本、架構及編譯選項而不同。

13.3. 檢查 Mach Header

otool -hv hello

可以觀察:

  • Magic Number。
  • CPU Type。
  • CPU Subtype。
  • File Type。
  • Load Commands 數量。
  • Load Commands 總大小。
  • Flags。

13.4. 檢查載入命令

otool -l hello

此命令是分析 Mach-O 結構的重要工具。

輸出可能包含:

LC_SEGMENT_64
LC_LOAD_DYLIB
LC_MAIN
LC_BUILD_VERSION
LC_CODE_SIGNATURE

實際命令清單取決於二進位檔案的建置方式。

13.5. 檢查動態程式庫相依性

otool -L hello

此命令可列出 Mach-O 中描述的相關動態程式庫。

對於分析動態連結錯誤與部署問題特別有用。

13.6. 檢查符號

nm hello

可使用適當選項進一步篩選符號。

如果二進位檔案已經移除符號資訊(Stripped Binary),其傳統符號表可能不完整,或無法提供原先全部的除錯與分析資訊。

13.7. 檢查架構

lipo -info hello

或:

lipo -archs hello

可以確認檔案具有哪些處理器架構切片。

13.8. 使用 LLVM 工具

LLVM 亦提供可分析 Mach-O 的工具。

例如:

llvm-objdump --macho --private-headers hello

查看反組譯:

llvm-objdump --disassemble hello

若分析 Universal Binary,必須確認工具正在處理預期的架構切片。


十四、Mach-O 解析器與產生器的設計要求

14.1. Mach-O 解析器

一個基本 Mach-O 解析器至少需要完成:

  1. 讀取 Magic Number。
  2. 判斷是單架構 Mach-O 還是 Fat Binary。
  3. 確定位元組順序及標頭類型。
  4. 檢查最小檔案大小。
  5. 解析 Mach Header。
  6. 驗證 ncmds 與 sizeofcmds。
  7. 逐項解析 Load Commands。
  8. 檢查區段與節區的偏移量及大小。
  9. 視需要讀取符號表與其他資料。
  10. 拒絕超出檔案範圍或違反格式規則的結構。

解析器應特別防範整數溢位。

例如,檢查資料範圍時可使用條件:

但實作時不應在未檢查的情況下直接相加。

較安全的檢查方式是:

offset <= fileSize
size <= fileSize - offset

這樣可以避免在無號整數運算中因加法溢位而誤判資料範圍合法。

14.2. Mach-O 產生器

如果編譯器或連結器需要自行產生 Mach-O,至少必須正確建立相關的二進位結構。

對目標檔案而言,主要工作可能包括:

  • 建立 Mach Header。
  • 指定 CPU 架構。
  • 建立相關區段與節區。
  • 配置機器碼和資料。
  • 產生符號資訊。
  • 建立重定位紀錄。
  • 計算檔案偏移量與結構大小。

對可執行映像而言,還可能包括:

  • 建立符合載入要求的區段配置。
  • 提供程式進入點資訊。
  • 記錄動態程式庫相依性。
  • 建立 dyld 所需的中繼資料。
  • 設定目標平台及版本資訊。
  • 在必要情況下產生或加入程式碼簽署資料。

然而,產生結構合法的 Mach-O,不代表已經產生可以在所有 Apple 平台成功執行及分發的應用程式。

後者還涉及 SDK、ABI、執行階段環境與平台安全政策。

14.3. 相容性測試

測試案例 目的
合法 64 位元 Mach-O 驗證基本標頭與載入命令
截斷檔案 防止超出資料範圍
cmdsize 為 0 防止錯誤遍歷及無限迴圈
載入命令未正確對齊 偵測格式規範違反
偏移量超出檔案大小 偵測不合法資料
多架構 Fat Binary 驗證架構切片選擇
不具有實體檔案資料的節區 正確處理 Zerofill 類型
不同架構的重定位 驗證重定位格式
包含 Chained Fixups 正確識別修正資料
包含程式碼簽署資訊 驗證簽署資料的位置與大小

十五、結論

Mach-O 是 Apple 生態系統的重要二進位格式,貫穿程式編譯、連結、載入及執行等階段。

此格式的歷史與 Mach、NeXT 及 Mac OS X 的發展密切相關,並隨 Apple 平台演進而支援不同的硬體架構和系統需求。

Mach-O 最鮮明的特徵,在於以 Mach 標頭、載入命令、區段及節區為基礎的檔案組織方式。

透過這些結構,Mach-O 不僅能夠保存機器碼及資料,也能描述動態連結、程式進入點、目標平台、記憶體配置及部分安全相關資訊。

Universal Binary 使不同架構的 Mach-O 映像可以被封裝於同一檔案,對 Apple 多次處理器架構轉換具有重要作用。

隨著 dyld 技術的發展,鏈式修正與匯出符號 Trie 等結構,也反映 Mach-O 在現代連結與載入需求下的持續演進。

對於編譯器、連結器及作業系統研究而言,理解 Mach-O 有助於掌握機器碼如何被組織為可供系統辨識、映射及執行的二進位映像。

不過,Mach-O 仍只是程式與作業系統之間二進位介面的一部分。程式能否成功執行,還取決於 ABI、CPU 架構、執行階段程式庫及平台政策。


附錄一:技術術語

中文術語 英文術語 說明
Mach-O 格式 Mach Object Apple 生態系統使用的目標檔案與執行映像格式
Mach 標頭 Mach Header 描述檔案基本資訊的結構
載入命令 Load Command 描述載入、連結及檔案處理資訊的紀錄
區段 Segment 用於記憶體映射及資料組織的區域
節區 Section 區段內部的程式碼或資料單位
符號表 Symbol Table 儲存符號名稱及相關資訊的結構
重定位 Relocation 根據位址與符號資訊調整參照
修正 Fixup 需要調整的二進位參照或相關操作
動態連結 Dynamic Linking 透過載入及執行階段機制連結程式庫
動態連結器 Dynamic Linker 負責執行階段動態連結的組件
通用二進位檔案 Universal Binary 在同一容器中儲存多種架構映像的檔案
架構切片 Architecture Slice Fat Binary 內特定架構的 Mach-O 映像
程式進入點 Entry Point 程式啟動執行流程的位置
位址空間配置隨機化 Address Space Layout Randomization 透過變動記憶體配置位置提高安全性
程式碼簽署 Code Signing 用於驗證程式碼完整性及支援平台安全政策的機制
應用程式二進位介面 Application Binary Interface 規範二進位組件互動方式的技術介面

附錄二:參考文獻

A. Apple 官方規格與原始碼

[1] Apple, mach-o/loader.h, XNU Source Code.

Mach-O 標頭、載入命令、區段、節區及相關常數的主要結構定義。

https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h

[2] Apple, mach-o/fat.h.

定義 Fat Binary 的標頭及架構紀錄。

https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/fat.h

[3] Apple, mach-o/nlist.h.

定義 Mach-O 符號紀錄及相關屬性。

https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/nlist.h

[4] Apple, Mach-O Documentation.

Apple 提供的 Mach-O 格式及二進位載入相關技術文件。

https://developer.apple.com/documentation/kernel/mach-o

B. 動態連結與修正機制

[5] Apple, dyld, Open Source Repository.

Apple 動態連結器及相關二進位處理組件的公開原始碼。

https://github.com/apple-oss-distributions/dyld

[6] Apple, MachOFile.cpp.

與 Mach-O 結構分析、驗證及相關處理有關的原始碼。

https://github.com/apple-oss-distributions/dyld/blob/main/common/MachOFile.cpp

[7] Apple, fixup-chains.h.

鏈式修正資料結構及相關常數的定義。

https://github.com/apple-oss-distributions/dyld/blob/main/include/mach-o/fixup-chains.h

[8] Apple, Cache Builder.

描述 dyld 共享快取建置,以及 Rebase、Bind 和其他處理機制的技術資料。

https://github.com/apple-oss-distributions/dyld/blob/main/doc/CacheBuilder.md

C. 通用二進位檔案

[9] Apple Developer Documentation, Building a Universal macOS Binary.

介紹如何建立同時支援 Intel 與 Apple silicon 的 macOS 二進位檔案。

https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary

D. 工具與延伸研究

[10] LLVM Project, LLVM Object File Formats.

LLVM 對多種二進位檔案格式的讀取、產生與分析工具及相關原始碼。

https://llvm.org/

[11] LLVM Project, Mach-O LLD Port.

LLVM LLD 中 Mach-O 連結器的相關技術文件。

https://lld.llvm.org/MachO/index.html

[12] John R. Levine, Linkers and Loaders, Morgan Kaufmann, 1999.

研究連結器、載入器、符號解析、重定位與目標檔案格式的專門著作。


註釋

  1. Apple, mach-o/loader.h,包括 mach_header、mach_header_64、load_command、segment_command_64、section_64 及相關 Mach-O 常數。https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h ↩
  2. Apple, mach-o/fat.h,Fat Binary 的標頭及架構紀錄。https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/fat.h ↩
  3. Apple, mach-o/nlist.h,Mach-O 符號紀錄與相關定義。https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/nlist.h ↩
  4. Apple, dyld,動態連結與 Mach-O 相關處理的公開原始碼。https://github.com/apple-oss-distributions/dyld ↩
  5. Apple, Building a Universal macOS Binary,關於 x86-64、ARM64 與 lipo 的官方說明。https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary ↩
  6. Apple, fixup-chains.h,鏈式修正資料結構定義。https://github.com/apple-oss-distributions/dyld/blob/main/include/mach-o/fixup-chains.h ↩
  7. Apple, Cache Builder,共享快取、Rebase、Bind 及相關建置機制。https://github.com/apple-oss-distributions/dyld/blob/main/doc/CacheBuilder.md ↩

規格適用範圍說明

Mach-O 格式會隨 Apple SDK、作業系統版本及工具鏈持續發展。Apple 公開原始碼中的結構定義,是理解該格式的重要技術依據,但不代表所有內部實作細節都屬於長期穩定且對第三方軟體提供相容性保證的介面。

特別是自行產生 Mach-O 檔案的編譯器或連結器,應針對目標 SDK、處理器架構及作業系統版本進行相容性驗證,而不能假設某個在特定 macOS 版本中有效的二進位布局,必然能被所有 Apple 平台接受。