(二) Mach-O 文件結(jié)構(gòu)

# 進程與二進制格式
# 相關(guān)工具
# Mach-O 文件格式
  ## 示例
  ## Mach-O 頭
  ## Data
    ### Segment(段)
    ### Section(節(jié))
    ### 兩個Section:__TEXT.__stubs、__TEXT.__stub_helper
  ## Load Command
    ### LC_CODE_SIGNATURE(數(shù)字簽名)
    ### LC_SEGMENT(進程虛擬內(nèi)存設(shè)置)
    ### LC_MAIN(設(shè)置主線程入口地址)
# 通用二進制格式(Universal Binary)
# 參考鏈接

上一篇說到源碼經(jīng)過預(yù)處理昭伸、編譯丐黄、匯編之后生成目標(biāo)文件,這一章介紹一下iOS硕舆、Mac OS中目標(biāo)文件的格式Mach-O的結(jié)構(gòu),方便了解之后的鏈接生成可執(zhí)行文件的過程骤公。

先附上相關(guān)源碼地址:與Mach-O 文件格式有關(guān)的結(jié)構(gòu)體定義都可以從 /usr/include/mach-o/loader.h 中找到(直接在xcode項目中找到loader.h抚官,然后Show In Finder即可)。

# 進程與二進制格式

進程在眾多操作系統(tǒng)中都有提及阶捆,它是作為一個正在執(zhí)行的程序的實例凌节,這是 UNIX 的一個基本概念钦听。而進程的出現(xiàn)是特殊文件在內(nèi)從中加載得到的結(jié)果,這種文件必須使用操作系統(tǒng)可以認知的格式倍奢,這樣才對該文件引入依賴庫朴上,初始化運行環(huán)境以及順利地執(zhí)行創(chuàng)造條件。

Mach-O(Mach Object File Format)是 macOS 上的可執(zhí)行文件格式卒煞,類似于 Linux 和大部分 UNIX 的原生格式 ELF(Extensible Firmware Interface)痪宰。macOS 支持三種可執(zhí)行格式:解釋器腳本格式、通用二進制格式和 Mach-O 格式(關(guān)于三者區(qū)別畔裕,在下面說到Mach-O Header的時候介紹)衣撬。

# 相關(guān)工具

命令行工具

  • file 命令,查看Mach-O文件的基本信息:file 文件路徑

  • otool 命令扮饶,查看Mach-O特定部分和段的內(nèi)容

#查看Mach-O文件的header信息
otool -h 文件路徑

#查看Mach-O文件的load commands信息
otool -l 文件路徑

# 更多使用方法具练,終端輸入otool -help查看
  • lipo 命令,來處理多架構(gòu)Mach-O文件甜无,常用命令如下
#查看架構(gòu)信息
lipo -info 文件路徑

#導(dǎo)出某種類型的架構(gòu)
lipo 文件路徑 -thin 架構(gòu)類型 -output 輸出文件路徑

#合并多種架構(gòu)類型
lipo 文件路徑1 文件路徑2 -output 輸出文件路徑

GUI工具

# Mach-O 文件格式

Mach-O 文件格式在官方文檔中有一個描述圖,很多教程中都引用到岂丘。官網(wǎng)文檔

可以看的出 Mach-O 主要由 3 部分組成占键,下面一一講述。Load Command的作用是指導(dǎo)內(nèi)核加載器元潘、動態(tài)鏈接器怎么將可執(zhí)行文件裝載到內(nèi)存進行執(zhí)行畔乙。所以Load Command放到最后一部分。

## 示例

用 helloworld 來做個試驗:

/// main.cpp
#import <stdio.h>

int main() {
    printf("hello");
    return 0;
}

使用 clang -g main.cpp -o main 生成執(zhí)行文件翩概。然后拖入到 MachOView 中來查看一下加載 Segment 的結(jié)構(gòu)(當(dāng)然使用 Synalyze It! 也能捕捉到這些信息的牲距,但是 MachOView 更對結(jié)構(gòu)的分層更加一目了然):

## Mach-O 頭

Mach-O 頭(Mach Header)描述了 Mach-O 的 CPU 架構(gòu)、大小端钥庇、文件類型以及加載命令等信息牍鞠。它的作用是讓內(nèi)核在讀取該文件創(chuàng)建虛擬進程空間的時候,檢查文件的合法性以及當(dāng)前硬件的特性是否能支持程序的運行评姨。

以下只給出 64 位定義的代碼难述,因為 32 位的區(qū)別是缺少了一個預(yù)留字段:

#define MH_MAGIC    0xfeedface    /* the mach magic number */
#define MH_CIGAM    0xcefaedfe    /* NXSwapInt(MH_MAGIC) */

struct mach_header_64 {
    uint32_t    magic;            / magic(魔數(shù)):用來確認文件的格式,操作系統(tǒng)在加載可執(zhí)行文件的時候會確認魔數(shù)是否正確吐句,如果不正確會拒絕加載胁后。 /
    cpu_type_t    cputype;        / CPU架構(gòu) /
    cpu_subtype_t    cpusubtype;  / CPU子版本 /
    uint32_t    filetype;         / 文件類型,常見的Mach-O文件有:MH_OBJECT(目標(biāo)文件)嗦枢、MH_EXECUTABLE(可執(zhí)行二進制文件)攀芯、MH_DYLIB(動態(tài)庫)等等。這些文件類型定義在 loader.h 文件中同樣可以找到 /
    uint32_t    ncmds;            / 加載器中加載命令的數(shù)量 /
    uint32_t    sizeofcmds;       / 加載器中所有加載命令的總大小 /
    uint32_t    flags;            / dyld 加載需要的一些標(biāo)志文虏,其中MH_PIE表示啟用地址空間布局隨機化(ASLR)侣诺。其他的值在loader.h文件中同樣可以找到 /
    uint32_t    reserved;         / 64位的保留字段 /
};

魔數(shù)會表明文件的格式殖演。filetype會表明具體是什么文件類型(都是貓,也分黑貓年鸳、白貓)趴久。

// magic:常見的魔數(shù)(Mac是小端模式)
Mach-O文件。用途:macOS 的原生二進制格式
  #define   MH_MAGIC    0xfeedface  / 32位設(shè)備上的魔數(shù)搔确,大端模式(符合人類閱讀習(xí)慣彼棍,高位數(shù)據(jù)在前) /
  #define   MH_CIGAM    0xcefaedfe  / 32位、小端(高位地址在后)妥箕,CIGAM就是MAGIC反過來寫,從命名上也可以看出端倪 /
  #define   MH_MAGIC_64 0xfeedfacf  / 64位更舞、大端 /
  #define   MH_CIGAM_64 0xcffaedfe  / 64位畦幢、小端 /

通用二進制格式FAT。用途:包含多種架構(gòu)支持的二進制格式缆蝉,只在 macOS 上支持宇葱。(在文章末尾簡單介紹一下,有興趣可以瞜一眼)
  #define FAT_MAGIC     0xcafebabe
  #define FAT_CIGAM     0xbebafeca  /* NXSwapLong(FAT_MAGIC) */
  #define FAT_MAGIC_64  0xcafebabf
  #define FAT_CIGAM_64  0xbfbafeca  /* NXSwapLong(FAT_MAGIC_64) */

腳本格式刊头。用途:主要用于 shell 腳本黍瞧,但是也常用語其他解釋器,如 Perl, AWK 等原杂。也就是我們常見的腳本文件中在 `#!` 標(biāo)記后的字符串印颤,即為執(zhí)行命令的指令方式,以文件的 stdin 來傳遞命令穿肄。
  魔數(shù)為 \x7FELF

// filetype:常見的Mach-O格式的文件類型
#define MH_OBJECT   0x1     / 可重定位的目標(biāo)文件 /
#define MH_EXECUTE  0x2     / 可執(zhí)行二進制文件 /
#define MH_DYLIB    0x6     / 動態(tài)綁定共享庫 /
#define MH_DYLINKER 0x7     / 動態(tài)鏈接編輯器年局,如dyld /
#define MH_BUNDLE   0x8     / 動態(tài)綁定bundle(包)文件 /
#define MH_DSYM     0xa     / 調(diào)試所用的符號文件 /

舉例:利用otool工具查看Mach-o文件的頭部

$ otool -hv bibi.decrypted 
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
   MH_MAGIC     ARM         V7  0x00     EXECUTE    59       6016   NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE

Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64   ARM64        ALL  0x00     EXECUTE    59       6744   NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE

## Data

數(shù)據(jù)區(qū)(Data):Data 中每一個段(Segment)的數(shù)據(jù)都保存在此,段的概念和 ELF 文件中段的概念類似咸产,都擁有一個或多個 Section 矢否,用來存放數(shù)據(jù)和代碼。

Raw segment data存放了所有的原始數(shù)據(jù)脑溢,而Load commands相當(dāng)于Raw segment data的索引目錄

### Segment(段)

其中僵朗,LC_SEGMENT_64定義了一個64位的段,當(dāng)文件加載后映射到地址空間(包括段里面節(jié)的定義)屑彻。64位段的定義如下:

struct segment_command_64 { /* for 64-bit architectures */
    uint32_t    cmd;          / Load Command類型验庙,這里L(fēng)C_SEGMENT_64代表將文件中64位的段映射到進程的地址空間。LC_SEGMENT_64和LC_SEGMENT的結(jié)構(gòu)差別不大 /
    uint32_t    cmdsize;      / 代表Load commands的大小 /
    char        segname[16];  / 16字節(jié)的段名稱 /
    uint64_t    vmaddr;       / 段映射到虛擬地址中的內(nèi)存起始地址 /
    uint64_t    vmsize;       / 段映射到虛擬地址中的內(nèi)存大小 /
    uint64_t    fileoff;      / 段在當(dāng)前架構(gòu)(MachO)文件中的偏移量社牲,如果是胖二進制文件壶谒,也指的是相對于當(dāng)前MachO文件的偏移 /
    uint64_t    filesize;     / 段在文件中的大小 /
    vm_prot_t   maxprot;     / 段頁面的最高內(nèi)存保護,用八進制表示(4=r(read)膳沽,2=w(write)汗菜,1=x(execute執(zhí)行權(quán)限)) /
    vm_prot_t   initprot;    / 段頁面最初始的內(nèi)存保護 /
    uint32_t    nsects;       / 段(segment)包含的區(qū)(section)的個數(shù)(如果存在的話) /
    uint32_t    flags;        / 段頁面標(biāo)志 /
};

系統(tǒng)將 fileoff 偏移處 filesize 大小的內(nèi)容加載到虛擬內(nèi)存的 vmaddr 處让禀,大小為vmsize,段頁面的權(quán)限由initprot進行初始化陨界。它的權(quán)限可以動態(tài)改變巡揍,但是不能超過maxprot的值,例如 _TEXT 初始化和最大權(quán)限都是可讀/可執(zhí)行/不可寫菌瘪。

常見的LC_SEGMENT Segment (cmd為LC_SEGMET)腮敌,其segname[16]有以下幾種值:

  • __PAGEZERO:空指針陷阱段,映射到虛擬內(nèi)存空間的第1頁俏扩,用于捕捉對 NULL 指針的引用糜工。
  • __TEXT:代碼段/只讀數(shù)據(jù)段。
  • __DATA:讀取和寫入數(shù)據(jù)的段录淡。
  • __LINKEDIT:動態(tài)鏈接器需要使用的信息捌木,包括符號表、重定位表嫉戚、綁定信息刨裆、懶加載信息等。
  • __OBJC:包含會被Objective Runtime使用到的一些數(shù)據(jù)彬檀。(從Macho文檔上看帆啃,他包含了一些編譯器私有的節(jié)。沒有任何公開的資料描述)

### Section(節(jié))

從示例圖中可以看到窍帝,部分的 Segment (__TEXT__DATA) 可以進一步分解為 Section努潘。

之所以按照 Segment(段) -> Section(節(jié)) 的結(jié)構(gòu)組織方式,是因為在同一個 Segment 下的 Section坤学,在內(nèi)存中的權(quán)限相同(編譯時慈俯,編譯器把相同權(quán)限的section放在一起,成為segment)拥峦,可以不完全按照 Page 的大小進行內(nèi)存對齊贴膘,節(jié)省內(nèi)存的空間。而 Segment 對外整體暴露略号,在裝載程序時刑峡,完整映射成一個vma(Virtual Memory Address),更好的做到內(nèi)存對齊玄柠,減少內(nèi)存碎片(可以參考《OS X & iOS Kernel Programming》第一章內(nèi)容)突梦。

Section 具體的數(shù)據(jù)結(jié)構(gòu)如下:

struct section_64 { 
    char        sectname[16];   / Section 的名字 /
    char        segname[16];    / Section 所在的 Segment 名稱 /
    uint64_t    addr;           / Section 映射到虛擬地址的偏移(所在的內(nèi)存地址) /
    uint64_t    size;           / Section 的大小 /
    uint32_t    offset;         / Section 在當(dāng)前架構(gòu)文件中的偏移 /
    uint32_t    align;          / Section 的內(nèi)存對齊邊界 (2 的次冪) /
    uint32_t    reloff;         / 重定位入口的文件偏移 /
    uint32_t    nreloc;         / 重定位入口的數(shù)目 /
    uint32_t    flags;          / Section標(biāo)志屬性 /
    uint32_t    reserved1;      / 保留字段1 (for offset or index) /
    uint32_t    reserved2;      / 保留字段2 (for count or sizeof) /
    uint32_t    reserved3;      / 保留字段3 /
};

結(jié)合示例圖,下面列舉一些常見(并非全部)的 Section:

__TEXT Segment(段)下面的節(jié):
  __text              程序可執(zhí)行的代碼區(qū)域
  __stubs             間接符號存根羽利。本質(zhì)上是一小段代碼宫患,跳轉(zhuǎn)到懶加載/延遲綁定(lazybinding)指針表(即__DATA.la_symbol_ptr)。找到對應(yīng)項指針指向的地址这弧。
  __sub_helper        輔助函數(shù)娃闲。幫助解決懶加載符號加載虚汛,上述提到的lazybinding的表(__DATA.la_symbol_ptr)中對應(yīng)項的指針在沒有找到真正的符號地址的時候,都指向這皇帮。
  __objc_methname     方法名
  __objc_classname    類名
  __objc_methtype     方法簽名
  __cstring           去重后的只讀的C風(fēng)格字符串卷哩,包含OC的部分字符串和屬性名
  __const             初始化過的常量
  __unwind_info       用戶存儲處理異常情況信息
  __eh_frame          調(diào)試輔助信息

__DATA Segment(段)下面的節(jié):
  __data              初始化過的可變的數(shù)據(jù)
  __const             沒有初始化過的常量
  __bss               沒有初始化的靜態(tài)變量
  __common            沒有初始化過的符號聲明
  __nl_symbol_ptr     非延遲導(dǎo)入/非懶加載(lazy-binding)符號指針表,每個表項中的指針都指向一個在dyld加載過程中属拾,搜索完成的符號将谊。即在dyld加載時會立即綁定值。
  __la_symbol_ptr     延遲導(dǎo)入/懶加載(lazy-binding)符號指針表渐白,每個表項中的指針一開始指向stub_helper尊浓。在第 1 次調(diào)用時才會綁定值。
  __got               非懶加載全局指針表
  __mod_init_func     初始化/constructor(構(gòu)造)函數(shù)
  __mod_term_func     destructor(析構(gòu))函數(shù)
  __cfstring          OC字符串
  __objc_classlist    程序中的類列表
  __objc_nlclslist    程序中自己實現(xiàn)了+load方法的類
  __objc_protolist    協(xié)議的列表
  __objc_classrefs    被引用的類列表
  __objc_ivar         成員變量

## 兩個section:__TEXT.__stubs纯衍、__TEXT.__stub_helper

在 wikipedia 有一個關(guān)于 Method stub 的詞條栋齿,大意就是:Stub 是指用來替換一部分功能的程序段。樁程序可以用來模擬已有程序的行為(比如一個遠端機器的過程)或是對將要開發(fā)的代碼的一種臨時替代托酸。

總結(jié)來說:

  • stub就是一段代碼褒颈,功能為:跳轉(zhuǎn)到 __DATA.__la_symbol_ptr( __DATA Segment 中的 __la_symbol_ptr Section) 對應(yīng)表項的數(shù)據(jù)柒巫,所指向的地址励堡。
  • __la_symbol_ptr 里面的所有表項的數(shù)據(jù)在初始時都會被 binding 成 __stub_helper
  • 當(dāng)懶加載符號第一次使用到的時候堡掏,按照上面的結(jié)構(gòu)应结,會跳轉(zhuǎn)到__stub_helper這個section的代碼,然后代碼中會調(diào)用dyld_stub_binder來執(zhí)行真正的bind泉唁。 bind結(jié)束后鹅龄,就將__la_symbol_ptr中該懶加載符號 原本對應(yīng)的指向__stub_helper的地址 修改為 符號的真實地址。
  • 之后的調(diào)用中亭畜,雖然依舊會跳到 __stub 區(qū)域扮休,但是 __la_symbol_ptr表由于在之前的調(diào)用中獲取到了符號的真實地址而已經(jīng)修正完成,所以無需在進入 dyld_stub_binder 階段拴鸵,可以直接使用符號玷坠。

這樣就完成了LazyBind的過程。Stub 機制 其實和 wikipedia 上的說法一致劲藐,設(shè)置一個樁函數(shù)(模擬八堡、占位函數(shù))并采用 lazy 思想做成延遲 binding 的流程。

在《深入解析 Mac OS X & iOS操作系統(tǒng)》中有詳細的驗證聘芜,也可以參考深入剖析Macho (1) 自己動手驗證一下兄渺。

## Load Command

Mach-O文件頭中包含了非常詳細的指令,這些指令在被調(diào)用時清晰地指導(dǎo)了如何設(shè)置并加載二進制數(shù)據(jù)汰现。這些指令挂谍,或稱為“加載命令”叔壤,緊跟在基本的mach_header之后。

每一條命令凳兵,在load.c文件中百新,都有對應(yīng)的結(jié)構(gòu)體,來記錄信息庐扫。共同點是都采用“類型-長度-值”的格式:

struct xxx_command {
  uint32_t  cmd;        / 32位的cmd值(表示類型) 饭望,下面列舉了部分 /
  uint32_t  cmdsize;    / 32位的cmdsize值(32位二進制為4的倍數(shù),64位二進制為8的倍數(shù)) /
  ...                   / 記錄命令本身的一些信息 /
}

//下面列舉一些load command的類型(對應(yīng)的cmd值)形庭,這里只列舉了部分铅辞,全面的可以看源碼,總共50多種load command萨醒。按照加載命令是由內(nèi)核加載器斟珊、動態(tài)鏈接器處理分開記錄。
內(nèi)核加載器處理的加載命令:
  #define   LC_SEGMENT                0x1    / 定義一個段(Segment)富纸,加載后被映射到內(nèi)存中囤踩,包括里面的節(jié)(Section) /
  #define   LC_LOAD_DYLINKER          0xe    / 默認的加載器路徑。通常路徑是“/usr/lib/dyld” /
  #define   LC_UUID                   0x1b   / 用于標(biāo)識Mach-0文件的ID晓褪,匹配二進制文件與符號表堵漱。在分析崩潰堆棧信息能用到,通過地址在符號表中找到符號 /
  #define LC_CODE_SIGNATURE           0x1d   / 代碼簽名信息 /
  #define   LC_ENCRYPTION_INFO_64     0x2C   / 文件是否加密的標(biāo)志涣仿,加密內(nèi)容的偏移和大小 /

動態(tài)鏈接器處理的加載命令:
  #define   LC_SYMTAB                 0x2    / 為文件定義符號表和字符串表勤庐,在鏈接文件時被鏈接器使用,同時也用于調(diào)試器映射符號到源文件好港。符號表定義的本地符號僅用于調(diào)試愉镰,而已定義和未定義的 external 符號被鏈接器使用 /
  #define   LC_DYSYMTAB               0xb    / 將符號表中給出符號的額外符號信息提供給動態(tài)鏈接器。 /
  #define   LC_ID_DYLIB               0xd    / 依賴的動態(tài)庫钧汹,包括動態(tài)庫名稱丈探、當(dāng)前版本號、兼容版本號拔莱⊥虢担可以使用“otool-L xxx”命令查看 /
  #define   LC_RPATH                 (0x1c | LC_REQ_DYLD)    / RunpathSearchPaths,@rpath搜索的路徑 /
  #define   LC_DYLD_INFO_ONLY        (0x22 | LC_REQ_DYLD)    / 記錄了有關(guān)鏈接的重要信息辨宠,包括在__LINKEDIT中動態(tài)鏈接相關(guān)信息的具體偏移和大小遗锣。ONLY表示這個加載指令是程序運行所必需的,如果舊的鏈接器無法識別它嗤形,程序就會出錯 /
  #define   LC_VERSION_MIN_IPHONEOS   0x25   / 系統(tǒng)要求的最低版本 /
  #define   LC_FUNCTION_STARTS        0x26   / 函數(shù)起始地址表精偿,使調(diào)試器和其他程序能很容易地看到一個地址是否在函數(shù)內(nèi) /
  #define   LC_MAIN                  (0x28 | LC_REQ_DYLD)    / 程序的入口。dyld獲取該地址,然后跳轉(zhuǎn)到該處執(zhí)行笔咽。replacement for LC_UNIXTHREAD /
  #define   LC_DATA_IN_CODE           0x29   / 定義在代碼段內(nèi)的非指令的表 /
  #define   LC_SOURCE_VERSION         0x2A   / 構(gòu)建二進制文件的源代碼版本號 /

有一些命令是由內(nèi)核加載器(定義在bsd/kern/mach_loader.c文件中) 直接使用的搔预, 其他命令是由動態(tài)鏈接器處理的。

在Mach-O文件加載解析時叶组,多個Load Command會告訴操作系統(tǒng)應(yīng)當(dāng)如何加載文件中每個Segment的數(shù)據(jù)拯田,對系統(tǒng)內(nèi)核加載器和動態(tài)鏈接器起引導(dǎo)作用。(不同的數(shù)據(jù)對應(yīng)不同的加載命令甩十,可以看到segment_command_64船庇、symtab_commanddylib_command等侣监,下面我們會講解Segment的加載命令鸭轮,下一節(jié)講靜態(tài)鏈接時,會涉及符號表symtab的加載命令)橄霉。

下面窃爷,以三個內(nèi)核加載器負責(zé)解析處理的load command,來簡單看下:

### LC_CODE_SIGNATURE(數(shù)字簽名)

Mach-O二進制文件有一個重要特性就是可以進行數(shù)字簽名姓蜂。盡管在 OS X 中仍然沒怎么使用數(shù)字簽名按厘,不過由于代碼簽名和新改進的沙盒機制綁定在一起,所以簽名的使用率也越來越高钱慢。在 iOS 中逮京,代碼簽名是強制要求的,這也是蘋果盡可能對系統(tǒng)封鎖的另一種嘗試:在 iOS 中只有蘋果自己的簽名才會被認可滩字。在 OS X 中造虏,code sign(1) 工具可以用于操縱和顯示代碼簽名御吞。man手冊頁麦箍,以及 Apple's code signing guide 和 Mac OS X Code Signing In Depth文檔都從系統(tǒng)管理員的角度詳細解釋了代碼簽名機制。

LC_CODE_SIGNATURE 包含了 Mach-O 二進制文件的代碼簽名陶珠,如果這個簽名和代碼本身不匹配(或者如果在iOS上這條命令不存在)挟裂,那么內(nèi)核會立即給進程發(fā)送一個SIGKILL信號將進程殺掉,沒有商量的余地揍诽,毫不留情诀蓉。

在iOS 4之前,還可以通過兩條sysctl(8)命令覆蓋負責(zé)強制執(zhí)行(利用內(nèi)核的MAC暑脆,即Mandatory AccessControl)的內(nèi)核變量渠啤,從而實現(xiàn)禁用代碼簽名檢查:

sysctl -w security.mac.proc_enforce = 0 //禁用進程的MAC
sysctl -w security.mac.vnode_enforce=0 //禁用VNode的MAC

而在之后版本的iOS中,蘋果意識到只要能夠獲得root權(quán)限添吗,越獄者就可以覆蓋內(nèi)核變量沥曹。因此這些變量變成了只讀變量。untethered越獄(即完美越獄)因為利用了一個內(nèi)核漏洞所以可以修改這些變量。由于這些變量的默認值都是啟用簽名檢查妓美,所以不完美越獄會導(dǎo)致非蘋果簽名的應(yīng)用程序崩潰——除非i設(shè)備以完美越獄的方式引導(dǎo)僵腺。

此外,通過 Saurik 的 ldid 這類工具可以在 Mach-O 中嵌入偽代碼簽名壶栋。這個工具可以替代OS X的code sign(1)辰如,允許生成自我簽署認證的偽簽名。這在iOS中尤為重要贵试,因為簽名和沙盒模型的應(yīng)用程序“entitlement”綁定在一起琉兜, 而后者在iOS中是強制要求的。entitlement 是聲明式的許可(以plist的形式保存)毙玻,必須內(nèi)嵌在Mach-O中并且通過簽名蓋章呕童,從而允許執(zhí)行安全敏感的操作時具有運行時權(quán)限。

OS X 和 iOS 都有一個特殊的系統(tǒng)調(diào)用csops(#169)用于代碼簽名的操作

### LC_SEGMENT(進程虛擬內(nèi)存設(shè)置)

LC_SEGMENT(或LC_SEGMENT_64) 命令是最主要的加載命令淆珊,這條命令指導(dǎo)內(nèi)核如何設(shè)置新運行的進程的內(nèi)存空間夺饲。這些“段”直接從Mach-O二進制文件加載到內(nèi)存中。

每一條LC_SEGMENT[64] 命令都提供了段布局的所有必要細節(jié)信息施符。見上文的數(shù)據(jù)結(jié)構(gòu)成員變量往声。

有了LC_SEGMENT命令,設(shè)置進程虛擬內(nèi)存的過程就變成遵循LC_SEGMENT命令的簡單操作戳吝。對于每一個段浩销,將文件中相應(yīng)的內(nèi)容加載到內(nèi)存中:從偏移量為 fileoff 處加載 filesize 字節(jié)到虛擬內(nèi)存地址 vmaddr 處的 vmsize 字節(jié)。每一個段的頁面都根據(jù) initprot 進行初始化听哭,initprot 指定了如何通過讀/寫/執(zhí)行位初始化頁面的保護級別慢洋。段的保護設(shè)置可以動態(tài)改變,但是不能超過 maxprot 中指定的值(在iOS中陆盘,+x和+w是互斥的)普筹。

### LC_MAIN(設(shè)置主線程入口地址)

從Mountain Lion開始,一條新的加載命令LC_MAIN替代了LC_UNIX_THREAD命令隘马。

  • 后者的作用是:開啟一個unix線程太防,初始化棧和寄存器,通常情況下酸员,除了指令指針(Intel的IP)或程序計數(shù)器(ARM的r15)之外蜒车,所有的寄存器值都為0。
  • 前者作用是設(shè)置程序主線程的入口點地址和棧大小幔嗦。

這條命令比LC_UNIXTHREAD命令更實用一些酿愧, 因為無論如何除了程序計數(shù)器之外所有的寄存器都設(shè)置為0了。由于沒有LC_UNIXTHREAD命令邀泉, 所以不可以在之前版本的 OS X 上運行使用了LC_MAIN的二進制文件(在加載時會導(dǎo)致dyld(1)崩潰)嬉挡。

LC_Main對應(yīng)的加載命令如下叛氨,記錄了可執(zhí)行文件的入口函數(shù)int main(int argc, char * argv[])的信息:

struct entry_point_command {
    uint32_t  cmd;        / LC_MAIN only used in MH_EXECUTE filetypes /
    uint32_t  cmdsize;    / 24 /
    uint64_t  entryoff;   / file (__TEXT) offset of main() /
    uint64_t  stacksize;  / if not zero, initial stack size /
};

從定義上可以看到入口函數(shù)的地址計算:Entry Point = vm_addr(__TEXT) + entryOff + Slide

dyld的源碼里能看到對Entry Point的獲取和調(diào)用:

dyld
  ▼ __dyld_start  // 源碼在dyldStartup.s這個文件,用匯編實現(xiàn)
    ▼ dyldbootstrap::start()   // dyldInitialization.cpp
      ▼ dyld::_main()
        ▼ //函數(shù)的最后棘伴,調(diào)用 getEntryFromLC_MAIN寞埠,從 Load Command 讀取LC_MAIN入口,如果沒有LC_MAIN入口焊夸,就讀取LC_UNIXTHREAD仁连,然后跳到主程序的入口處執(zhí)行

namespace dyldbootstrap {

uintptr_t start(const dyld3::MachOLoaded* appsMachHeader, int argc, const char* argv[],
                const dyld3::MachOLoaded* dyldsMachHeader, uintptr_t* startGlue) {
    //
    // Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
    // sets up some registers and call this function.
    //
    // Returns address of main() in target program which __dyld_start jumps to
    //
    uintptr_t
    _main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
            int argc, const char* argv[], const char* envp[], const char* apple[], 
            uintptr_t* startGlue) {
        // find entry point for main executable
        result = (uintptr_t)sMainExecutable->getEntryFromLC_MAIN();
        return result;
    }   
}

}

這里簡單看一下這幾種load command所表示的信息。關(guān)于進程地址空間分布阱穗、線程入口在第四節(jié) —— 裝載會從進程啟動到運行詳細梳理一下流程饭冬。

# 通用二進制格式(Universal Binary)

通常也被稱為胖二進制格式(Fat Binary),Apple 提出這個概念是為了解決一些歷史原因揪阶,macOS(更確切的應(yīng)該說是 OS X)最早是構(gòu)建于 PPC 架構(gòu)智商昌抠,后來才移植到 Intel 架構(gòu)(從 Mac OS X Tiger 10.4.7 開始),通用二進制格式的二進制文件可以在 PPC 和 x86 兩種處理器上執(zhí)行鲁僚。

說到底炊苫,通用二進制格式只不過是對多架構(gòu)的二進制文件的打包集合文件,而 macOS 中的多架構(gòu)二進制文件也就是適配不同架構(gòu)的 Mach-O 文件冰沙。即一個通用二進制格式包含了很多個 Mach-O 格式文件侨艾。它有以下特點:

  • 因為需要存儲多種架構(gòu)的代碼,所以通用二進制文件要比單架構(gòu)二進制文件要大
  • 因為兩種種架構(gòu)之間可以共用一些資源拓挥,所以兩種架構(gòu)的通用二進制文件大小不會達到單一架構(gòu)版本的兩倍唠梨。
  • 運行過程中只會調(diào)用其中的部分代碼,所以運行起來不會占用額外的內(nèi)存

Fat Header 的數(shù)據(jù)結(jié)構(gòu)在 <mach-o/fat.h> 頭文件中有定義侥啤,可以參看 /usr/include/mach-o/fat.h 找到定義頭:

#define FAT_MAGIC    0xcafebabe
#define FAT_CIGAM    0xbebafeca    /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
    uint32_t    magic;        /* FAT_MAGIC 或 FAT_MAGIC_64 */
    uint32_t    nfat_arch;    /* 結(jié)構(gòu)體實例的個數(shù) */
};

struct fat_arch {
    cpu_type_t    cputype;    /* cpu 說明符 (int) */
    cpu_subtype_t    cpusubtype;    /* 指定 cpu 確切型號的整數(shù) (int) */
    uint32_t    offset;        /* CPU 架構(gòu)數(shù)據(jù)相對于當(dāng)前文件開頭的偏移值 */
    uint32_t    size;        /* 數(shù)據(jù)大小 */
    uint32_t    align;        /* 數(shù)據(jù)內(nèi)潤對其邊界当叭,取值為 2 的冪 */
};

對于 cputypecpusubtype 兩個字段這里不講述,可以參看 /usr/include/mach/machine.h 頭中對其的定義盖灸,另外 Apple 官方文檔中也有簡單的描述蚁鳖。

fat_header 中,magic 也就是我們之前在表中羅列的 magic 標(biāo)識符糠雨,也可以類比成 UNIX 中 ELF 文件的 magic 標(biāo)識才睹。加載器會通過這個符號來判斷這是什么文件徘跪,通用二進制的 magic0xcafebabe甘邀。nfat_arch 字段指明當(dāng)前的通用二進制文件中包含了多少個不同架構(gòu)的 Mach-O 文件。fat_header 后會跟著多個 fat_arch垮庐,并與多個 Mach-O 文件及其描述信息(文件大小松邪、CPU 架構(gòu)、CPU 型號哨查、內(nèi)存對齊方式)相關(guān)聯(lián)逗抑。

這里可以通過 file 命令來查看簡要的架構(gòu)信息,這里以 iOS 平臺 WeChat 4.5.1 版本為例:

~ file Desktop/WeChat.app/WeChat
Desktop/WeChat.app/WeChat: Mach-O universal binary with 2 architectures: [arm_v7: Mach-O executable arm_v7] [arm64]
Desktop/WeChat.app/WeChat (for architecture armv7):    Mach-O executable arm_v7
Desktop/WeChat.app/WeChat (for architecture arm64):    Mach-O 64-bit executable arm64

進一步,也可以使用 otool 工具來打印其 fat_header 詳細信息:

~ otool -f -V Desktop/WeChat.app/WeChat
Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture armv7
    cputype CPU_TYPE_ARM
    cpusubtype CPU_SUBTYPE_ARM_V7
    capabilities 0x0
    offset 16384
    size 56450224
    align 2^14 (16384)
architecture arm64
    cputype CPU_TYPE_ARM64
    cpusubtype CPU_SUBTYPE_ARM64_ALL
    capabilities 0x0
    offset 56475648
    size 64571648
    align 2^14 (16384)

之后我們用 Synalyze It! 來查看 WeChat 的 Mach64 Header 的效果:

  • 從第一個段中得到 magic = 0xcafebabe 邮府,說明是 FAT_MAGIC荧关。
  • 第二段中所存儲的字段為 nfat_arch = 0x00000002,說明該 App 中包含了兩種 CPU 架構(gòu)褂傀。
  • 后續(xù)的則是 fat_arch 結(jié)構(gòu)體中的內(nèi)容忍啤,cputype(0x0000000c)cpusubtype(0x00000009)仙辟、offset(0x00004000)同波、size(0x03505C00) 等等。如果只含有一種 CPU 架構(gòu)叠国,是沒有 fat 頭定義的未檩,這部分則可跳過,從而直接過去 arch 數(shù)據(jù)粟焊。

# 參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冤狡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子项棠,更是在濱河造成了極大的恐慌筒溃,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沾乘,死亡現(xiàn)場離奇詭異怜奖,居然都是意外死亡,警方通過查閱死者的電腦和手機翅阵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門歪玲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人掷匠,你說我怎么就攤上這事滥崩。” “怎么了讹语?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵钙皮,是天一觀的道長。 經(jīng)常有香客問我顽决,道長短条,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任才菠,我火速辦了婚禮茸时,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘赋访。我一直安慰自己可都,他們只是感情好缓待,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渠牲,像睡著了一般旋炒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上签杈,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天国葬,我揣著相機與錄音,去河邊找鬼芹壕。 笑死汇四,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的踢涌。 我是一名探鬼主播通孽,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼睁壁!你這毒婦竟也來了背苦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤潘明,失蹤者是張志新(化名)和其女友劉穎行剂,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钳降,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡厚宰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了遂填。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片铲觉。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吓坚,靈堂內(nèi)的尸體忽然破棺而出撵幽,到底是詐尸還是另有隱情,我是刑警寧澤礁击,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布盐杂,位于F島的核電站,受9級特大地震影響哆窿,放射性物質(zhì)發(fā)生泄漏链烈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一更耻、第九天 我趴在偏房一處隱蔽的房頂上張望测垛。 院中可真熱鬧,春花似錦秧均、人聲如沸食侮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锯七。三九已至,卻和暖如春誉己,著一層夾襖步出監(jiān)牢的瞬間眉尸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工巨双, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留噪猾,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓筑累,卻偏偏與公主長得像袱蜡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子慢宗,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344