# 進程與二進制格式
# 相關(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工具
- MachOView:文件瀏覽扛点。MachOView官網(wǎng)
- hopper:反匯編工具
# 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_command
、dylib_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 的冪 */
};
對于 cputype
和 cpusubtype
兩個字段這里不講述,可以參看 /usr/include/mach/machine.h
頭中對其的定義盖灸,另外 Apple 官方文檔中也有簡單的描述蚁鳖。
在 fat_header
中,magic
也就是我們之前在表中羅列的 magic 標(biāo)識符糠雨,也可以類比成 UNIX 中 ELF 文件的 magic 標(biāo)識才睹。加載器會通過這個符號來判斷這是什么文件徘跪,通用二進制的 magic 為 0xcafebabe
甘邀。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ù)粟焊。