Mach-O
什么Mach-O
Mach-O為Mach Object文件格式的縮寫怒见,它是一種用于可執(zhí)行文件,目標代碼禽车,動態(tài)庫寇漫,內核轉儲的文件格式刊殉。作為a.out格式的替代,Mach-O提供了更強的擴展性州胳,并提升了符號表中信息的訪問速度记焊。Mach-O是iOS、mac系統(tǒng)中的可執(zhí)行文件格式栓撞。
- MachO格式的常見文件
目標文件.o
庫文件
.a
.dylib
Framework
可執(zhí)行文件
dyld
.dsym
- 常用命令
使用lifo -info 可以查看MachO文件包含的架構
lipo -info MachO文件
使用lifo –thin 拆分某種架構
lipo MachO文件 –thin 架構 –output 輸出文件路徑
使用lipo -create 合并多種架構
lipo -create MachO1 MachO2 -output 輸出文件路徑
Mach-O文件結構
每個Mach-O文件包括一個Mach-O頭遍膜,然后是一系列的載入命令,再是一個或多個塊瓤湘,每個塊包括0到255個段瓢颅。Mach-O使用REL再定位格式控制對符號的引用。Mach-O在兩級命名空間中將每個符號編碼成“對象-符號名”對岭粤,在查找符號時則采用線性搜索法惜索。以下是蘋果官方關于Mach-O的結構圖:
通過MachOView可以查看更為詳細的Mach-O的結構特笋。如下圖所示:
- Mach64 Header: 包含該二進制文件的一般信息剃浇。包括字節(jié)順序、架構類型猎物、加載指令的數(shù)量等虎囚。使得可以快速確認一些信息,比如當前文件用于32位還是64位蔫磨,對應的處理器是什么淘讥、文件類型是什么。以下就是Header的數(shù)據結構:
struct mach_header_64 {
uint32_t magic; /* mach-o 格式的標識符 */
cpu_type_t cputype; /* cpu區(qū)分符 */
cpu_subtype_t cpusubtype; /* machine區(qū)分符 */
uint32_t filetype; /* 文件類型 */
uint32_t ncmds; /* 加載命令的個數(shù) */
uint32_t sizeofcmds; /* 加載命令的字節(jié)數(shù) */
uint32_t flags; /* 程序的標識位 */
uint32_t reserved; /* 保留字段 */
};
- Load Commands:是加載指令堤如,描述的是文件的加載信息蒲列,內容包括區(qū)域的位置、符號表搀罢、動態(tài)符號表等蝗岖。這個部分信息還是比較有用的,我們可以從這里獲取到符號表和字符串表的偏移量等榔至。通過MachOView可以查看Load Commands詳情:
字段解析
LC_SEGMENT_64 將文件中(32位或64位)的段映射到進程地址空間中
LC_DYLD_INFO_ONLY 動態(tài)鏈接相關信息
LC_SYMTAB 符號地址
LC_DYSYMTAB 動態(tài)符號表地址
LC_LOAD_DYLINKER 使用誰加載抵赢,我們使用dyld
LC_UUID 文件的UUID
LC_VERSION_MIN_MACOSX 支持最低的操作系統(tǒng)版本
LC_SOURCE_VERSION 源代碼版本
LC_MAIN 設置程序主線程的入口地址和棧大小
LC_LOAD_DYLIB 依賴庫的路徑,包含三方庫
LC_FUNCTION_STARTS 函數(shù)起始地址表
LC_CODE_SIGNATURE 代碼簽名
- 數(shù)據區(qū):除了Header和Load Commands外所有的原始數(shù)據唧取。數(shù)據區(qū)分為很多段(Section)铅鲤。
text段是代碼段。它用來放程序代碼(code)枫弟。它通常是只讀的邢享。
data段是數(shù)據段。它用來存放初始化了的(initailized)全局變量(global)和初始化了的靜態(tài)變量(static)淡诗。它是可讀可寫的骇塘。
bss段是全局變量數(shù)據段掸犬。它用來存放未初始化的(uninitailized)全局變量(global)和未初始化的靜態(tài)變量
接下來先介紹數(shù)據區(qū)幾個比較重要的模塊:
- (__TEXT,__text)
這里存放的是匯編后的代碼,當我們進行編譯時绪爸,每個.m文件會經過預編譯->編譯->匯編形成.o文件湾碎,稱之為目標文件。匯編后奠货,所有的代碼會形成匯編指令存儲在.o文件的(__TEXT,__text)區(qū)介褥。鏈接后,所有的.o文件會合并成一個文件递惋,所有.o文件的(__TEXT,__text)數(shù)據都會按鏈接順序存放到應用文件的(__TEXT,__text)中柔滔。
- (__DATA,__data)
存儲數(shù)據的section,static在進行非零賦值后會存儲在這里萍虽,如果static 變量沒有賦值或者賦值為0睛廊,那么它會存儲在(__DATA,__bss)中。
- Symbol Table
符號表杉编,這個是重點中的重點超全,符號表是將地址和符號聯(lián)系起來的橋梁。符號表并不能直接存儲符號邓馒,而是存儲符號位于字符串表的位置嘶朱。
- String Table
字符串表所有的變量名、函數(shù)名等光酣,都以字符串的形式存儲在字符串表中疏遏。
- Dynamic Symbol Table
動態(tài)符號表存儲的是動態(tài)庫函數(shù)位于符號表的偏移信息。(__DATA,__la_symbol_ptr) section 可以從動態(tài)符號表中獲取到該section位于符號表的索引數(shù)組救军。動態(tài)符號表并不存儲符號信息财异,而是存儲其位于符號表的偏移信息。
- Lazy Symbol Pointers
Lazy Symbol Pointers懶加載符號表唱遭。所謂懶加載是指在程序運行時需要訪問這些符號的時候再去綁定戳寸。這些符號一般來自程序依賴的動態(tài)庫。
-
Non Lazy Symbol Pointers
Non Lazy Symbol Pointers 非懶加載符號表胆萧。所謂非懶加載是指在程序一加載就綁定好的庆揩。這些符號一般來自程序依賴的動態(tài)庫。
-
Symbol Stubs
翻譯過來就是符號樁跌穗。它與Lazy Symbol Pointers是一一對應的订晌,每次訪問外部符號時都會先訪問Symbol Stubs,然后執(zhí)行樁代碼蚌吸,最后去Lazy Symbol Pointers找到相應的符號地址執(zhí)行下一步操作锈拨。
Assembly
這里面其實就是符號綁定執(zhí)行的匯編代碼。后面到符號綁定的時候再詳解羹唠。
符號重定向
對于iOS程序來說奕枢,由于ASLR安全機制的原因娄昆,每次啟動程序系統(tǒng)都會分配一個隨機偏移值,程序啟動時會根據偏移值進行符號地址修正缝彬。假設程序首地址的0x00000000, 隨機偏移值是0x00008000, 那程序的首地址就變成0x00000000 + 0x00008000, 程序內某個符號的偏移地址是0x00001000萌焰,那么它的內存地址是0x00001000 + 0x00008000 = 0x00009000。符號重定向實際上就是在程序運行時谷浅,把符號的偏移地址加上啟動時的隨機偏移值得到符號的內存地址的一個過程扒俯。符號重定向針對的是程序內的符號。接下來我們通過下面的demo來進行驗證一疯。首先我們新建一個類MyObject, 然后定義
一個方法doSomething撼玄,然后運行:
獲取編譯后的可執(zhí)行文件:
這個黑色icon的文件就是我們的可執(zhí)行文件,把這個文件拖入MachOView中:
接下來演示一下重定向的過程墩邀。首先在運行前- (void)doSomething入口打個斷點掌猛,然后在xcode->Debug-> Debug WorkFlow -> Always show disassembly 進入匯編模式,運行然后就進入如下畫面:
打開剛才的MachOView查看符號表眉睹,如下:
可以看到- (void)doSomething放的的符號-[MyObject doSomething]在文件中的偏移地址為5E28荔茬。接下來我們驗證一下重定向的過程:
由圖中可知方法doSomething的內存地址與程序的首地址的差值是不變的,而且是等于-[MyObject doSomething]在文件中的偏移值辣往。
符號綁定
相對于符號重定向針對的是程序內的符號兔院,符號綁定針對的是程序外的符號殖卑,比如所以依賴的動態(tài)庫等站削。由于編譯時并沒有把動態(tài)庫編譯到程序內,只是在連接階段生成動態(tài)符號表孵稽。但是生成這個動態(tài)符表的地址并不是符號的真是地址许起,只有在訪問這個符號時才會調用系統(tǒng)的符號綁定函數(shù)進行進行綁定,并獲取符號地址更新到符號表中菩鲜。這個過程就叫符號綁定园细。
綁定流程
當程序首次訪問外部函數(shù)的時候,它首先會訪問外部函數(shù)的樁Symbol Stubs接校,并執(zhí)行樁代碼(Symbol Stubs中的Data字段對應的值)猛频,而這個樁代碼執(zhí)行后,最后會跳轉到Lazy Symbol Pointers對應符號的地址蛛勉。首次訪問會根據這個地址在Assembly文件中找到相應的代碼執(zhí)行鹿寻,最后調用dyld_stub_binder函數(shù)進行符號綁定。綁定完成之后就會更新Lazy Symbol Pointers表中的值诽凌,將符號地址直接寫入到表中毡熏,再次訪問的時候就可以直接訪問這個地址而不需要在執(zhí)行Assembly中的代碼。 大致流程圖如下:
下面我們利用NSLog(NSLog來自系統(tǒng)動態(tài)庫Foundation)的例來對符號綁定整個過程進行演示侣诵。我們同時通過MachOView工具以及程序運行會的匯編調試來展示這個過程痢法。同樣的狱窘,以下面的demo為例:
- 進入匯編調試
首先,在NSLog入口處打個斷點财搁,進入匯編調試蘸炸,如下圖所示:
紅圈部分可以看到確實是訪問了Simbol Stubs。
- 執(zhí)行執(zhí)行Symbol Stubs中的代碼
通過匯編調試執(zhí)行bl指令進入如下頁面:
這一步可以看到實際上是執(zhí)行Symbol Stubs中的Data斷的代碼尖奔,這個段代碼最后會跳轉到0x000000010015651c幻馁,這個地址是通過Lazy Symbol Pointers獲取的。
- 執(zhí)行Assembly中的代碼
通過image list命令可以獲取程序的偏移地址(首地址)為0x0000000100150000越锈。偏移地址0x000000010015651c -0x0000000100150000 = 0x000000000000651c仗嗦。然后根據這個偏移值到Assembly執(zhí)行相應的代碼。執(zhí)行br指令甘凭,跳轉到x16中的地址(實際上就是剛才0x000000010015651c)稀拐,進入如下頁面:
- 調用符號綁定函數(shù)dyld_stub_binder
在這里執(zhí)行兩行代碼然后跳轉到6504這個這個地方。這個地方實際上就是調用符號綁定函數(shù)dyld_stub_binder的地方丹弱。以下通過匯編調試和Assembly中查看他們的匯編指令就可以看得出來德撬。
為了進一步驗證,執(zhí)行br指令跳轉到x16中的地址(實際上就是dyld_stub_binder函數(shù)地址)躲胳。進入如下頁面:
- Non-Lazy Symbol Pointers
這里有個疑問蜓洪,就是dyld_stub_binder本身也是外部符號。那它是怎么綁定的呢坯苹?又是什么時候綁定的呢隆檀。實際上dyld_stub_binder是非懶加載符號,是在程序一開始運行就綁定的粹湃,它存儲在Non-Lazy Symbol Pointers里面恐仑,所以不需要再走一遍符號綁定流程。
至此为鳄,NSlog函數(shù)第一次調用過程中的符號綁定流程就走完了裳仆。其他外部符號訪問流程都是一樣的。綁定成功之后會修改Lazy Symbol Pointers中的值為符號的內存地址孤钦,下次訪問時不需要再次綁定歧斟,可以直接在Lazy Symbol Pointers進行訪問。
第二次調用NSLog函數(shù)
查看Lazy Symbol Pointers中的編譯時偏移地址:
接下來我們進行第二次訪問的驗證偏形。首先在第二次調用處打個斷點静袖,進入匯編調試,進入如下頁面:
繼續(xù)執(zhí)行bl指令壳猜,跳轉一個地址勾徽,進入如下頁面:
最終看到第二次進入的時候Lazy Symbol Pointers中的值就已經是NSLog函數(shù)的地址了,程序就可以直接訪問了。
符號重綁定
有前面知道符號的綁定過程喘帚,我們知道符號綁定的本質就是將外部符號的地址跟新到Lazy Symbol Pointers表中畅姊。符號重綁定實際上也就是修改Lazy Symbol Pointers中的值。常用的第三方庫Fishhook之所以能夠hook系統(tǒng)代碼就是利用這個原理吹由,直接修改了Lazy Symbol Pointers對應符號的地址值實現(xiàn)若未。這里我們以Hook系統(tǒng)函數(shù)NSLog為例,演示Fishhook工作的大致流程如下:
- 通過字符換找到符號倾鲫。去Mach-O中尋找String Table(String Table中通過“.”字符將符號分割)粗合,得到偏移值(String Table Index)
- 通過String Table Index去找Symbols(符號表),得到符號表的偏移值(Symbol Index)
- 通過Symbol Index 去找indirect Symbols乌昔,得到符號的偏移值(Indirect Symbol Index)
- 最后去修改Lazy Symbol Pointers 里面的值(因為有前面的分析可知隙疚,外部符號調用都在找樁,樁去尋找Lazy Symbol Pointers 里面的地址)
接下來通過匯編調試驗證:
首先獲取NSLog符號在Lazy Symbol Pointers中的偏移值:
拿到符號偏移地址為0xC000,開始進入匯編調試:
hook之后符號地址就改成了我們自定義的函數(shù)myNSlog的地址磕道。