Mach-O 文件結構

一、理解可執行文件

1. 可執行文件
  1. 進程,其實就是可執行文件在內存中加載得到的結果;
  2. 可執行文件必須是操作系統可理解的格式,而且不同系統的可執行文件的格式也是不同的;
2. 不同平臺的可執行文件

二、理解 Mach-O 文件

作爲iOSiPadOSmacOS平臺的可執行文件格式,Mach-O文件涉及 App 啓動運行、bitcode分析、 crash符號化等諸多多個功能:

1. Mach-O 文件
  1. Mach-O文件是iOSiPadOSmacOS平臺的可執行文件格式。對應系統通過應用二進制接口 (application binary interface,縮寫爲ABI) 來運行該格式的文件;
  2. Mach-O格式用來替代BSD系統中的a.out格式,保存了在編譯和鏈接過程中產生的機器代碼和數據,從而爲靜態鏈接和動態鏈接的代碼提供單一文件格式。
  3. Mach-O提供了更強的擴展性,以及更快的符號表信息訪問速度;
2.Mach-O 格式的常見文件類型
  1. Executable:可執行文件 (.out .o);
  2. Dylib:動態鏈接庫;
  3. Bundle:不能被鏈接,只能在運行時使用dlopen()加載;
  4. Image:包含ExecutableDylibBundle
  5. Framework:包含Dylib、資源文件和頭文件的文件夾;

三、Mach-O 文件結構

1. 查看 Mach-O 的兩種方法
  1. 使用MachOView軟件,可直接查看MachO文件的結構;
  2. 使用終端命令objdump
2. 查看 Mach-O 文件結構

使用MachOView查看Mach-O,效果如下:

Mach-O文件中包含三個主要的部分:

  1. Header:頭部,描述CPU類型、文件類型、加載命令的條數大小等信息;
  2. Load Commands:加載命令,其條數和大小已經在header中被提供;
  3. Data:數據段;

其他的信息還有:

  1. Dynamic Loader Info:動態庫加載信息
  2. Function Starts:入口函數
  3. Symbol Table:符號表
  4. Dynamic Symbol Table: 動態庫符號表
  5. String Table:字符串表

四、Mach Header(可執行文件頭)

1. 功能總結
  1. Header是鏈接器加載時最先讀取的內容,因爲它決定了一些基礎架構系統類型等信息;
  2. Header包含整個Mach-O文件的關鍵信息,如CPU類型文件類型加載命令的條數大小等信息,使得系統能夠迅速定位Mach-O文件的運行環境;
  3. Header針對32位和64位架構的CPU,分別對應mach_headermach_header_64的結構體;
2. 源碼分析

Header被定義在loader.h文件中,具體代碼如下:

struct mach_header_64 {
    uint32_t    magic;          // 32位或者64位,系統內核用來判斷是否是mach-o格式
    cpu_type_t  cputype;        // CPU架構類型,比如ARM
    cpu_subtype_t   cpusubtype; // CPU的具體類型,例如arm64、armv7
    uint32_t    filetype;       // mach-o文件類型, 可執行文件、目標文件或者靜態庫和動態庫
    uint32_t    ncmds;          // LoadCommands加載命令的條數(加載命令緊跟header之後)
    uint32_t    sizeofcmds;     // 全部LoadCommands加載命令的大小
    uint32_t    flags;          // 標誌位標識二進制文件支持的功能,主要是和系統加載、鏈接有關
    uint32_t    reserved;       // 保留字段(相比於32位多出的字段)
    };
複製代碼

由於可執行文件目標文件或者靜態庫動態庫等都是Mach-O格式,所以才需要filetype來說明。常用的文件類型有以下幾種:

#define MH_OBJECT   0x1     /* 目標文件*/
#define MH_EXECUTE  0x2     /* 可執行文件*/
#define MH_DYLIB    0x6     /* 動態庫*/
#define MH_DYLINKER 0x7     /* 動態鏈接器*/
#define MH_DSYM     0xa     /* 存儲二進制文件符號信息,用於debug分析*/
複製代碼
3.MachOView 演示

五、Load Commands

1. 功能總結
  1. Load Commands是加載命令的列表,用於描述Data在二進制文件和虛擬內存中的佈局信息;
  2. Load Commands記錄了很多信息,例如動態鏈接器的位置、程序的入口、依賴庫的信息、代碼的位置、符號表的位置等;
  3. Load commands由內核定義,不同版本的command數量不同,其條數和大小記錄在header中;
  4. Load commandstype是以LC_爲前綴常量,譬如LC_SEGMENTLC_SYMTAB等;
2.. 代碼分析

Load Command被定義在loader.h文件中,具體代碼如下:

struct load_command {
    uint32_t cmd;       /* 加載命令的類型 */
    uint32_t cmdsize;   /* 加載命令的大小 */
};
複製代碼

每個Load Command都有獨立的結構,但是所有結構的前兩個字段是固定的。比如LC_SEGMENT_64,這是一個讀取segmentsection有關命令,具體代碼如下:

struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;          // 表示加載命令類型
    uint32_t    cmdsize;      // 表示加載命令大小(還包括了緊跟其後的nsects個section的大小)
    char        segname[16];  // 16個字節的段名字
    uint64_t    vmaddr;       // 段的虛擬內存起始地址
    uint64_t    vmsize;       // 段的虛擬內存大小
    uint64_t    fileoff;      // 段在文件中的偏移量
    uint64_t    filesize;     // 段在文件中的大小
    vm_prot_t   maxprot;      // 段頁面所需要的最高內存保護(4 = r,2 = w,1 = x)
    vm_prot_t   initprot;     // 段頁面初始的內存保護
    uint32_t    nsects;       // 段中section數量
    uint32_t    flags;        // 標誌位
};
複製代碼

六、Data

1. 功能總結
  1. Data中存儲了實際的數據與代碼,主要包含方法、符號表、動態符號表、動態庫加載信息 (重定向、符號綁定等) 等;
  2. Data中的排布完全按照Load Command中的描述;
  3. DataSegment(段)和 Section (節)的方式來組成,通常,Data擁有多個segment,每個segment可以有零到多個section節;
  4. 不同的segment都有一段虛擬地址映射到進程的地址空間;

幾乎所有的Mach-O文件都包含3segment

  1. __TEXT:代碼段,只讀可執行,存儲函數的二進制代碼(__text)常量字符串(__cstring)OC的類/方法名等信息
  2. __DATA:數據段, 可讀可寫,存儲OC的字符串(__cfstring),以及運行時的元數據:class/protocol/method,以及全局變量,靜態變量等;
  3. __LINKEDIT:只讀,存儲啓動App需要的信息,如 bind & rebase 的地址、函數的名稱和地址等信息​;
2. 源碼分析

Data區中,Section佔了很大的比例,而且在Mach-O中集中體現在__TEXT__DATA兩段裏。

Section被定義在loader.h文件中,具體代碼如下:

struct section_64 { /* for 64-bit architectures */
    char        sectname[16];   // 當前section的名稱
    char        segname[16];    // section所在的segment名稱
    uint64_t    addr;       // 內存中起始位置
    uint64_t    size;       // section大小
    uint32_t    offset;     // section的文件偏移
    uint32_t    align;    // 字節大小對齊
    uint32_t    reloff;     // 重定位入口的文件偏移
    uint32_t    nreloc;   // 重定位入口數量
    uint32_t    flags;      // 標誌,section的類型和屬性
    uint32_t    reserved1;  // 保留(用於偏移量或索引)
    uint32_t    reserved2;  // 保留(用於count或sizeof)
    uint32_t    reserved3;  // 保留
};
複製代碼

七、理解大小端模式

分析Mach-O文件時,經常會看到內存地址相關的內容,這裏就涉及到了大小端模式的概念;

  1. 小端模式:數據的低字節,保存在內存的低地址;
  2. 大端模式:數據的低字節,保存在內存的高地址;

iOS設備的處理器是基於ARM架構的,默認是採用小端模式 (低字節放低位)讀取數據的,而網絡和藍牙傳輸數據通常是用的大端模式 (低字節放高位):

下面以unsigned int value = 0x12345678爲例,分別看看在兩種字節序下其存儲情況,我們可以用unsigned char buf[4]來表示value

Little-Endian: 低地址存放低位,如下:
低地址 ------------------> 高地址
0x78  |  0x56  |  0x34  |  0x12

Big-Endian: 低地址存放高位,如下:
低地址 -----------------> 高地址
0x12  |  0x34  |  0x56  |  0x78
複製代碼

3bOCcx

八、理解通用二進制文件

1. 基本概念
  1. 通用二進制文件的存儲結構,是將多種架構的Mach-O文件打包在一起,CPU在讀取該二進制文件時可以自動檢測並選用合適的架構;
  2. 通用二進制文件會同時存儲多種架構,所以比單一架構的二進制文件大很多,會佔用大量的磁盤空間。但由於系統運行時會自動選擇最合適的,不相關的架構代碼,不會佔用內存空間,所以執行效率提高了;
  3. 通用二進制格式也被稱爲胖二進制格式;
2. 通用二進制格式分析

通用二進制格式的定義在<mach-o/fat.h>中:

  1. 下載 xnu 後,依次在 xnu -> EXTERNAL_HEADERS ->mach-o中找到該文件。
  2. 通用二進制文件有兩個重要結構體:fat_headerfat_arch

兩個結構體的定義如下:

/*
 - magic:可以讓系統內核讀取該文件時知道是通用二進制文件
 - nfat_arch:表明下面有多個fat_arch結構體,即通用二進制文件包含多少個Mach-O
 */
struct fat_header {
    uint32_t    magic;      /* FAT_MAGIC */
    uint32_t    nfat_arch;  /* number of structs that follow */
};

/*
 fat_arch是描述Mach-O
 - cputype 和 cpusubtype:說明Mach-O適用的平臺
 - offset(偏移)、size(大小)、align(頁對齊)描述了Mach-O二進制位於通用二進制文件的位置
 */
struct fat_arch {
    cpu_type_t  cputype;    /* cpu specifier (int) */
    cpu_subtype_t   cpusubtype; /* machine specifier (int) */
    uint32_t    offset;     /* file offset to this object file */
    uint32_t    size;       /* size of this object file */
    uint32_t    align;      /* alignment as a power of 2 */
};
複製代碼

參考鏈接

  1. xnu
  2. Mach-O 官方源碼
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://juejin.cn/post/7022810233105809439