Mach-O文件
- Mach-O是Mach Object文件格式的縮寫,是mac以及iOS上可執(zhí)行文件的格式妖异,例如當(dāng)Xcode App工程編譯完成之后就會生成一個可執(zhí)行文件吵护,其格式就是Mach-O文件焦读;
Mach-O的相關(guān)名詞
- Executable 可執(zhí)行文件;
- Dylib 動態(tài)庫睁冬;
- Bundle 無法被連接的動態(tài)庫,只能通過dlopen()加載;
- Image 指的是Executable豆拨,Dylib或者Bundle的一種直奋,文中會多次使用Image這個名詞;
- Framework 動態(tài)庫(可以是靜態(tài)庫)和對應(yīng)的頭文件和資源文件的集合施禾;
Mach-O文件的常見類型
#define MH_OBJECT 0x1 /* relocatable object file */
#define MH_EXECUTE 0x2 /* demand paged executable file */
#define MH_FVMLIB 0x3 /* fixed VM shared library file */
#define MH_CORE 0x4 /* core file */
#define MH_PRELOAD 0x5 /* preloaded executable file */
#define MH_DYLIB 0x6 /* dynamically bound shared library */
#define MH_DYLINKER 0x7 /* dynamic link editor */
#define MH_BUNDLE 0x8 /* dynamically bound bundle file */
#define MH_DYLIB_STUB 0x9 /* shared library stub for static */
/* linking only, no section contents */
#define MH_DSYM 0xa /* companion file with only debug */
/* sections */
#define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */
-
MH_OBJECT
:目標(biāo)文件即 .o 文件 以及靜態(tài)庫文件即 .a 文件(多個.o文件合并在一起)脚线; -
MH_EXECUTE
:可執(zhí)行文件,即App編譯運行后生成的可執(zhí)行文件弥搞,在/Products路徑下邮绿; -
MH_DYLIB
:動態(tài)庫文件,即.dylib文件 或者 .framework文件攀例; -
MH_DYLINKER
:/usr/lib/dyld路徑下的dyld文件船逮; -
MH_DSYM
:Xcode打包后生成的符號表文件,即.dSYM文件粤铭;
查看文件的格式類型
使用命令行
file 文件名
-
查看自定義的目標(biāo).o文件
- 終端輸入:
file YYPerson.o
- 終端輸出:
YYPerson.o: Mach-O 64-bit object x86_64
- 終端輸入:
-
查看Xcode編譯運行后生成的可執(zhí)行文件
- 終端輸入:
file SuningWeiDian
- 終端輸出:
Mach-O 64-bit executable x86_64
- 終端輸入:
-
終端cd /usr/lib 然后 ls 列出所有l(wèi)ib文件挖胃;然后查看ACIPCBTLib.dylib文件
- 終端輸入:
file ACIPCBTLib.dylib
- 終端輸出:
ACIPCBTLib.dylib: Mach-O 64-bit dynamically linked shared library x86_64
- 終端輸入:
-
同上 查看 dyld文件
- 終端輸入:
file dyld
- 終端輸出:
dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
dyld (for architecture i386):Mach-O dynamic linker i386
- 可以看出 dyld 這個文件有點特殊;能同時支持x86_64與i386兩種架構(gòu)承耿;
- 終端輸入:
-
查看打包之后生成的dSYM文件
- 終端輸入:
file SuningWeiDian
- 終端輸出:
Mach-O universal binary with 2 architectures: [arm_v7:Mach-O dSYM companion file arm_v7] [arm64] SuningWeiDian (for architecture armv7): Mach-O dSYM companion file arm_v7 SuningWeiDian (for architecture arm64): Mach-O 64-bit dSYM companion file arm64
- 終端輸入:
通用二進制文件(Universal binary)
- 在iOS中不同手機對應(yīng)著可能不同的架構(gòu)冠骄,如arm64、armv7加袋、armv7s凛辣,為了兼容不同架構(gòu)的手機,蘋果推出了通用二進制文件职烧,其能同時支持多個不同架構(gòu)扁誓,因此通用二進制文件,比單一架構(gòu)二進制文件要大很多蚀之,因此也稱之為
胖二進制文件
蝗敢; - 當(dāng)一個文件同時支持多個架構(gòu)平臺,比如同時支持 ARMV7足删、ARM64寿谴,就相當(dāng)是兩個 Mach-O 文件,編譯器會編譯兩個Mach-O文件失受,然后合成一個Fat文件讶泰;
- 例如上面的dSYM文件,就是通用二進制文件拂到,支持兩種架構(gòu)痪署;
- 在Xcode工程中有配置支持不同架構(gòu)的選項,如下圖所示:
Mach-O文件的基本結(jié)構(gòu)
- 先上一個官方截圖兄旬,如下所示:
- 可以看出Mach-O文件主要包含三個部分:
-
Header
:包含Mach-O文件的基本信息狼犯,例如文件類型,支持的CPU架構(gòu)類型,加載指令的數(shù)量悯森,所占內(nèi)存大小等等宋舷; -
Load Commands
:不同數(shù)據(jù)段segment的加載命令,指導(dǎo)加載器加載數(shù)據(jù)瓢姻; -
Data
:指數(shù)據(jù)段Segment肥缔,其有不同的Section組成;
-
otool工具
-
otool是Mac系統(tǒng)自帶的
汹来,可以查看Mach-O文件特定部分和段的內(nèi)容的工具续膳; - 下面使用的資源是自己本地工程
Mach-O文件結(jié)構(gòu)
生成一個Mach-O文件結(jié)構(gòu).app文件,其包內(nèi)容中有一個Mach-O目標(biāo)文件:Mach-O文件結(jié)構(gòu)
收班,下面利用otool的常見命令行操作Mach-O文件結(jié)構(gòu)
: -
otool -h Mach-O文件結(jié)構(gòu)
:獲取Mach-O文件的Header頭信息
坟岔,輸出結(jié)果如下:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x00 2 75 8304 0x00218085
-
otool -L Mach-O文件結(jié)構(gòu)
:查看Mach-O文件所使用的動態(tài)庫
,會打印出App中所有的動態(tài)庫
如下所示:
-
objdump --macho --private-headers Mach-O文件結(jié)構(gòu)
摔桦,輸出結(jié)果如下:
MachOView圖形化界面工具
- otool是通過命令行來查看Mach-O文件的結(jié)構(gòu)社付,但是不夠直觀,而MachOView是一款圖形化的查看Mach-O文件結(jié)構(gòu)的工具軟件邻耕,更加直觀鸥咖;
- 點擊這里 進行下載兄世;
- 將
Mach-O文件結(jié)構(gòu)
這個Mach-O文件直接拖入MachOView中
啼辣,如下所示:
image.png
Mach-O文件三部分的詳細分析
- 源碼查看 在Xcode中按下
Command+Shift+O
然后輸入loader.h
可以定位到系統(tǒng)關(guān)于Mach-O文件的定義;
第一部分:Mach_Header
- 定義如下所示:
struct mach_header {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
};
-
magic
:提供給系統(tǒng)內(nèi)核御滩,用來判斷文件是否是Mach-O的文件格式鸥拧; -
cputype
:表示支持的CPU類型,一般有armv7, armv64, x86, x86_64 這幾種類型削解; -
filetype
:表示Mach-O的具體文件類型富弦,如果是可執(zhí)行文件就是 MH_EXECUTE,如果是動態(tài)庫就是 MH_DYLIB氛驮,詳情見文章頂部腕柜; -
ncmds
:表示Mach-O文件中所有Load Commands(加載命令)的總個數(shù); -
sizeofcmds
:表示Load Commands所有(加載命令)占用的字節(jié)總大薪梅稀盏缤; -
flags
:表示文件的標(biāo)志信息;
第二部分:Load Commands
-
Load Commands
緊跟在Mach_Header之后磷脯,這些加載指令告訴loader加載器如何加載二進制數(shù)據(jù)蛾找,本質(zhì)就是確定如何加載段segment數(shù)據(jù)
娩脾,其定義如下所示:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
-
屬性cmd
:表示Load Commands(加載命令)的類型赵誓; -
屬性cmdsize
:表示當(dāng)前的加載命令所占內(nèi)存大小; - 使用MachOView工具查看Mach-O文件的Load Commands部分可以看到:
- 常見的加載命令的簡介如下所示:
-
LC_SEGMENT_64
:將該段(64位)映射到進程地址空間中俩功; -
LC_DYLD_INF0_0NLY
:加載動態(tài)鏈接庫信息(重定向地址幻枉、弱引用綁定、懶加載綁定诡蜓、開放函數(shù)等的偏移值等信息)熬甫; -
LC_SYMTAB
:加載符號表; -
LC_DYSYMTAB
:加載動態(tài)符號表蔓罚; -
LC_LOAD_DYLINKER
:加載動態(tài)加載庫椿肩,可以看出示例使用的是/usr/lib/dyld; -
LC.UUID
: 確定文件的唯一標(biāo)識豺谈,crash解析中也會有這個郑象,去檢測dysm文件
和crash文件
是否匹配; -
LC_VERSION_MIN_IPHONEOS
:確定二進制文件要求的最低操作系統(tǒng)版本茬末; -
LC.SOURCE.VERSION
:構(gòu)建二進制文件的源代碼版本號厂榛; -
LC.MAIN
:主程序的入口,dyld獲取該地址丽惭,然后跳轉(zhuǎn)到該處執(zhí)行击奶; -
LC_ENCRYPTION_INFO_64
:加載加密信息; -
LC_LOADJDYLIB
:加載額外的動態(tài)庫责掏; -
LC_FUNCTION_STARTS
:定義一個函數(shù)起始地址表,使調(diào)試器和其他程序易于看到一個地址是否在函數(shù)內(nèi)柜砾; -
LC_DATA_IN_CODE
:定義在代碼段內(nèi)的非指令的表; -
LC_CODE_SIGNATURE
:獲取應(yīng)用簽名信息换衬;
-
- 下面以加載指令
LC_SEGMENT_64
為例局义,此加載指令的結(jié)構(gòu)如下所示:
-
LC_SEGMENT_64
此加載指令屬于segment段加載指令
,其結(jié)構(gòu)體源碼如下所示:
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
}
-
cmd
:加載命令的類型冗疮; -
cmdsize
:加載命令的所占內(nèi)存大刑汛健; -
segname
: 加載目標(biāo)段Segment的名稱术幔,常見的段segment有__PAGEZERO
另萤、__LINKEDIT
、__TEXT
诅挑、__DATA
四敞;-
__PAGEZERO
在可執(zhí)行文件有的,動態(tài)庫里沒有拔妥,這個段開始地址為0(NULL指針指向的位置)忿危,是一個不可讀、不可寫没龙、不可執(zhí)行的空間铺厨,能夠在空指針訪問時拋出異常缎玫; -
__TEXT
:代碼段
,里面主要是存放代碼的解滓,該段是可讀可執(zhí)行赃磨,但是不可寫; -
__DATA
:數(shù)據(jù)段
洼裤,里面主要是存放數(shù)據(jù)邻辉,該段是可讀可寫,但不可執(zhí)行腮鞍; -
__LINKEDIT
:用于存放簽名信息值骇,該段是只可讀,不可寫不可執(zhí)行移国;
-
- 段Segment類型的截圖如下:
-
vmaddr
:段Segment的虛擬內(nèi)存地址雷客; -
vmsize
:段Segment的虛擬內(nèi)存大小桥狡; -
fileoff
:段Segment的在文件中的偏移量搅裙; -
filesize
:段Segment在文件中所占的內(nèi)存大小裹芝; -
nsects
:段Segment包含節(jié)區(qū)sections的數(shù)量部逮; -
maxprot
:表示頁面所需要的最高內(nèi)存保護; -
initprot
:表示頁面初始的內(nèi)存保護嫂易; -
flags
:表示段的標(biāo)志信息兄朋;
第三部分:Data數(shù)據(jù)部分
- Data數(shù)據(jù)部分,就是指段Segment的數(shù)據(jù)怜械,而Segment段是由多個
Section
組成的颅和,所以其主體部分為Section
,而Section的頭部信息Section Header
是存放在段的加載命令中缕允,其結(jié)構(gòu)如下所示:
- 首先來介紹一下
Section Header
峡扩,當(dāng)一個段segmemt包含多個節(jié)區(qū)Section,節(jié)區(qū)頭Section Header
會以數(shù)組
的形式存儲在段加載命令
中障本,如上截圖所示教届,毋庸置疑其是描述Section
的結(jié)構(gòu)信息的; -
Section
的源碼如下所示:
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
-
sectname
:section的名稱驾霜,常見的section有_text案训、stubs等等; -
segname
:當(dāng)前section所隸屬的Segment粪糙,例如__TEXT(代碼段)强霎; -
addr
: section在內(nèi)存的起始位置; -
size
: section所占內(nèi)存大腥馗浴城舞; -
offset
: section在文件中的偏移量轩触; -
align
:字節(jié)大小對齊,2的align次方椿争; -
reloff
:重定位入口的文件偏移; -
nreloc
: 需要重定位的入口數(shù)量熟嫩; -
flags
:包含section的type和attributes秦踪;
__TEXT段中的Section組成如下所示:
-
__text
:代碼節(jié),存放機器編譯后的代碼掸茅; -
__stubs
:用于輔助做動態(tài)鏈接代碼(dyld)椅邓; -
__stub_helper
:用于輔助做動態(tài)鏈接(dyld); -
__objc_methname
:objc的方法名稱昧狮; -
__cstring
:代碼運行中包含的字符串常量景馁,比如代碼中定義#define kGeTuiPushAESKey "DWE2#@e2!"
,那DWE2#@e2!會存在這個區(qū)里逗鸣; -
__objc_classname
: objc類名合住; -
__objc_methtype
: objc方法類型; -
__ustring
: -
__gcc_except_tab
: -
__const
:存儲const修飾的常量撒璧; -
__dof_RACSignal
: -
__dof_RACCompou
: -
__unwind_info
:
__DATA段中的Section組成如下所示:
-
__got
:存儲引用符號的實際地址透葛,類似于動態(tài)符號表; -
__la_symbol_ptr
:lazy symbol pointers卿樱,懶加載的函數(shù)指針地址僚害,和__stubs和stub_helper配合使用,具體原理暫留繁调; -
__mod_init_func
:模塊初始化的方法萨蚕; -
__const
:存儲constant常量的數(shù)據(jù),比如使用extern導(dǎo)出的const修飾的常量蹄胰; -
__cfstring
:使用Core Foundation字符串岳遥; -
__objc_classlist
:objc類列表,保存類信息,映射了__objc_data的地址裕寨; -
__objc_nlclslist
:Objective-C 的 +load 函數(shù)列表寒随,比 __mod_init_func 更早執(zhí)行; -
__objc_catlist
:categories分類帮坚; -
__objc_nlcatlist
:Objective-C 的categories的 +load函數(shù)列表妻往; -
__objc_protolist
:objc協(xié)議列表; -
__objc_imageinfo
:objc鏡像信息试和; -
__objc_const
:objc常量讯泣,保存objc_classdata結(jié)構(gòu)體數(shù)據(jù),用于映射類相關(guān)數(shù)據(jù)的地址阅悍,比如類名好渠,方法名等昨稼; -
__objc_selrefs
:引用到的objc方法; -
__objc_protorefs
:引用到的objc協(xié)議拳锚; -
__objc_classrefs
:引用到的objc類假栓; -
__objc_superrefs
:objc超類引用; -
__objc_ivar
:objc ivar指針霍掺,存儲屬性匾荆; -
__objc_data
:objc的數(shù)據(jù),用于保存類需要的數(shù)據(jù)杆烁,最主要的內(nèi)容是映射牙丽,__objc_const地址,用于找到類的相關(guān)數(shù)據(jù)兔魂; -
__data
:暫時沒理解烤芦,從日志看存放了協(xié)議和一些固定了地址已經(jīng)初始化的靜態(tài)量; -
__bss
:存儲未初始化的靜態(tài)量析校,比如:static NSThread *_networkRequestThread = nil构罗,
其中這里面的size表示應(yīng)用運行占用的內(nèi)存,不是實際的占用空間智玻,所以計算大小的時候應(yīng)該去掉這部分數(shù)據(jù)绰播; -
__common
:存儲導(dǎo)出的全局的數(shù)據(jù),類似于static尚困,但是沒有用static修飾蠢箩,比如KSCrash里面NSDictionary* g_registerOrders
,g_registerOrders就存儲在__common里面事甜;
Mach-O文件的結(jié)構(gòu)分析
- 首先創(chuàng)建兩個.c文件分別為
a.c
與b.c
谬泌,代碼如下:
//a.c文件
#include <stdio.h>
//顯式的說明了a的存儲空間是在程序的其他地方分配的,在文件中其他位置或者其他文件中尋找a這個變量
extern int global_var;
void func(int a);
int main(int argc, const char * argv[]) {
int a = 100;
func(a + global_var);
return 0;
}
//b.c文件
#include <stdio.h>
int global_var = 1;
void func(int a) {
global_var = a;
}
在進行代碼分析之前逻谦,首先介紹兩個概念
模塊
和符號
掌实;模塊
:我們可以理解一個源代碼文件為一個模塊。比如上面a模塊和b模塊邦马。我們現(xiàn)在寫一個程序贱鼻,不可能所有代碼都在一個源代碼文件上,都是分模塊的滋将,一般一個類在一個源文件上邻悬,就成為一個模塊,模塊化好處就是復(fù)用随闽、維護父丰,還有編譯時候,未改動的模塊掘宪,不用重新編譯蛾扇,直接用之前編譯好的緩存攘烛;符號
:簡單理解就是函數(shù)名和變量名,比如上面總共有三個符號:global_var
镀首、main
坟漱、func
;將a.c與b.c文件分別編譯生成目標(biāo)文件
a.o
與b.o
文件更哄,可通過終端命令來實現(xiàn)芋齿,輸入:xcrun -sdk iphoneos clang -c a.c b.c -target arm64-apple-ios12.2
-
將生成的a.o與b.o目標(biāo)文件,進行靜態(tài)鏈接竖瘾,生成一個最終的目標(biāo)文件沟突,命名為
ab
花颗,可通過終端命令來實現(xiàn)捕传,輸入:xcrun -sdk iphoneos clang a.o b.o -o ab -target arm64-apple-ios12.2
,最終的所有文件如下所示:
image.png -
可使用
file 文件名
扩劝,查看文件類型庸论,如下所示:
image.png a.o
,b.o
和ab
均屬于Mach-O文件棒呛,可使用MachOView打開進行查看聂示;-
使用MachOView打開
a.o
文件,內(nèi)容如下所示:
Snip20220104_121.png adrp x10 #0
:其中#0
是全局變量global_var
的臨時內(nèi)存地址簇秒,是編譯器暫時用#0
代替的鱼喉;ldr w11 [x10]
:將x10寄存器中的內(nèi)存地址中的數(shù)值,也就是全局變量global_var
趋观,寫入w11寄存器中扛禽;add w0 w9 w11
:將w9與w11中的數(shù)值相加,即100+1計算結(jié)果賦值給w0皱坛,w0寄存器中存儲著func函數(shù)的參數(shù)编曼;bl #0x3c
:其中#0x3c
是func函數(shù)的臨時地址,是編譯器暫時用#0x3c
代替的剩辟;-
使用MachOView打開
ab
文件掐场,內(nèi)容如下所示:
Snip20220104_118.png a.o
與b.o
在經(jīng)過鏈接器
進行靜態(tài)鏈接之后,生成ab文件贩猎,在ab
文件中的全局變量global_var
與函數(shù)func
的內(nèi)存地址是真正的內(nèi)存地址熊户,那么鏈接器是怎么進行調(diào)整的;全局變量
global_var
與函數(shù)func
的內(nèi)存地址從a.o
到ab
經(jīng)過了鏈接器
的靜態(tài)鏈接吭服,這兩個符號的內(nèi)存地址在前后發(fā)生了變化敏弃,現(xiàn)在我們來探索其中的工作原理;首先在
a.o
文件中包含了一個重定位表
噪馏,其專門保存了所有需要進行重定位的符號麦到,根據(jù)符號信息可以在當(dāng)前文件的符號表
中查看符號的詳細信息绿饵;-
在進行
a.o
與b.o
文件鏈接時,會將a.o里面有這兩符號的引用瓶颠,然后b.o里面有這兩符號的定義拟赊,一起合并到全局符號表里,最后在全局符號表中粹淋,對符號進行重定位吸祟,修正符號的正確地址;
Snip20220104_130.png