一、什麼是Mach-O檔案?
Mach-O
是Mach Object
檔案格式的縮寫,是mac
以及iOS
上可執行檔案的格式。Mach-O
檔案對應有多種格式:
- 目標檔案
.o
- 庫檔案:
.a
靜態庫檔案.dylib
動態庫檔案.framework
系統級為動態庫檔案,自己建立的為靜態庫檔案- 可執行檔案及
MDW.app
內部的MDW
檔案(通用二進位制檔案)dyld
動態連結器將依賴的動態庫載入到記憶體.dsym
符號表
在Xcode
中我們可以直接建立.c
檔案,通過終端clang
命令來對.c
檔案進行編譯或生成可執行檔案,下面看一下clang
怎樣使用的。
1、建立一個main.c檔案如下:
#include <stdio.h>
int main(){
printf("列印:yahibo\n");
return 0;
}
複製程式碼
2、編譯檔案
clang -c main.c
複製程式碼
會生成main.o
檔案,該檔案即為mach-o
檔案,通過命令file main.o
檢視檔案資訊如下:
main.o: Mach-O 64-bit object x86_64
複製程式碼
是一個object
型別的檔案稱為目標檔案,並不是可執行檔案
3、生成可執行檔案
- 命令
clang main.o
會生成a.out
檔案,即可執行檔案,通過ls
檢視 - 命令
clang -o main main.o
也會生成可執行檔案main
- 命令
clang -o main main.c
直接根據原始檔生成可執行檔案main
- 命令
size -x -l -m a.out
檢視檔案資訊,如下:
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
Section __text: 0x2a (addr 0x100000f50 offset 3920)
Section __stubs: 0x6 (addr 0x100000f7a offset 3962)
Section __stub_helper: 0x1a (addr 0x100000f80 offset 3968)
Section __cstring: 0x11 (addr 0x100000f9a offset 3994)
Section __unwind_info: 0x48 (addr 0x100000fac offset 4012)
total 0xa3
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000
複製程式碼
4、執行檔案
./a.out 或 ./main
複製程式碼
輸出:
列印:yahibo
複製程式碼
以上步驟可以用來寫c
並編譯執行。
多個檔案是如何編譯的呢?
開發中根據不同功能模組我們會分很多檔案來實現,在clang
中是可以對多個檔案進行一次性打包,生成一個可執行檔案。如下:
1、新建一個功能檔案
people.c
#include <stdio.h>
void sleep(){
printf("正在睡覺\n");
}
複製程式碼
2、在main.c
中宣告sleep
方法並呼叫
void sleep();//宣告方法
int main(){
printf("列印:yahibo\n");
sleep();//呼叫方法
return 0;
}
複製程式碼
3、編譯為可執行檔案
clang -o main main.c people.c
複製程式碼
4、執行可執行檔案
./main
複製程式碼
執行如下:
列印:yahibo
正在睡覺
複製程式碼
二、通用二進位制檔案(Universal binary)
在iOS
中不同手機對應著可能不同的架構,如arm64、armv7、armv7s
。為了相容不同架構的手機,蘋果推出了通用二進位制檔案,包含了應用程式常用的這些架構,因此通用二進位制檔案,比單一架構二進位制檔案要大很多。
架構選擇
- 注意以上標記的兩處取交集,來確認最終架構
1
處預設架構為arm64、armv7
- 如果需要新增
armv7s
還需要在1
處新增armv7s
字元
通過以上配置真實編譯出來的是包含arm64、armv7
架構,因為工程中使用了第三方靜態庫不包含armv7s
因此這裡配置為標準架構模式。
通用二進位制檔案在哪呢?
在xxx.app
中的xxx黑色檔案
即是通用二進位制檔案,右鍵xxx.app
顯示包內容即可獲得。
lipo命令
通過lipo
命令可以檢視、拆分及合併以上提出的架構,在做靜態庫時也會使用,來合併真機下和模擬器下的靜態庫,以適應不同的除錯環境。
- 從
MDW.app
中我獲取可執行檔案MDW
1、檢視架構資訊
lipo -info MDW
複製程式碼
列印如下:
Architectures in the fat file: MDW are: armv7 arm64
複製程式碼
2、拆分armv7、arm64
架構
lipo MDW -thin armv7 -output MDW_armv7
lipo MDW -thin arm64 -output MDW_arm64
複製程式碼
檢視armv7資訊:
lipo -info MDW_armv7
複製程式碼
列印如下:
Non-fat file: MDW_armv7 is architecture: armv7
複製程式碼
檢視arm64資訊:
lipo -info MDW_arm64
複製程式碼
列印如下:
Non-fat file: MDW_arm64 is architecture: arm64
複製程式碼
3、合併架構
lipo -create MDW_armv7 MDW_arm64 -output MDW_ALL
複製程式碼
檢視合併後的資訊
lipo -info MDW_ALL
複製程式碼
列印如下:
Architectures in the fat file: MDW_ALL are: armv7 arm64
複製程式碼
產生的可執行檔案如圖:
三、Mach-O檔案結構
官方圖解:
檔案分為三個部分:
Header:
包含Mach-O
檔案的基本資訊,位元組順序、架構型別、載入指令的數量等Load commands:
包含區域位置、符號表、動態符號表,載入Mach-O
檔案時使用這裡的資料確定記憶體分佈Data:
資料段segement
,包含具體程式碼、常量、類、方法等,有多個segment
,每個segment
有0到多個section
,每個段有一個虛擬地址對映到程式的地址空間
直接使用MachOView開啟MDW可執行檔案,如下:
- 胖二進位制檔案中包含了
armv7、arm64
架構 - 通過MachOView即可檢視可執行檔案的所有資訊
1、Header
除了以上直接檢視header
,還可以通過otool
命令檢視header
資訊:
otool -f MDW
複製程式碼
列印如下:
Fat headers
fat_magic 0xcafebabe
nfat_arch 2
architecture 0
cputype 12
cpusubtype 9
capabilities 0x0
offset 16384
size 7587424
align 2^14 (16384)
architecture 1
cputype 16777228
cpusubtype 0
capabilities 0x0
offset 7618560
size 8748384
align 2^14 (16384)
複製程式碼
或
otool -h MDW
複製程式碼
列印:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedface 12 9 0x00 2 48 5080 0x00210085
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777228 0 0x00 2 48 5752 0x00210085
複製程式碼
- 以上列印的兩段分別是
armv7、arm64
架構下的header
資訊 在objc4
原始碼loader.h檔案中有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 */
};
struct mach_header_64 {
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 */
uint32_t reserved; /* reserved */
};
複製程式碼
magic:
魔數,確定是64位還是32位cputype:cpu
型別cpusubtype:cpu
子型別filetype:Mach-O
支援多種檔案型別,使用filetype
來標註具體檔案型別ncmds:
載入命令的數量sizeofcmds:
命令區域(load commands
)總的位元組大小flags:
標識二進位制檔案所支援的功能,主要與系統的載入、連結有關
2、Load commands
Header
之後是load commands
段為載入命令段,在header
結構體中有對載入命令段相關資訊的描述,用於解析載入命令。在objc4
原始碼loader.h中,有對loadcommand
的定義:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
複製程式碼
cmd:
命令型別,針對不同架構有不同的結構(32位、64位)cmdsize:
命令所佔位元組大小(32位size必須為4位元組的倍數,64位size必須為8位元組的倍數) 在檔案中有兩個結構體segment_command
和segment_command_64
針對不同架構的結構體,內部設定欄位相同。以segment_command_64
為例:
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_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:
載入命令型別LC_SEGMENT:
表示這好似一個段載入命令,需要將它載入到對應的程式空間上LC_LOAD_DYLIB:
這是一個需要動態載入的連結庫,它使用dylib_command
結構體表示LC_MAIN:
記錄了可執行檔案的主函式main()
的位置,它使用entry_point_command
結構體表示LC_CODE_SIGNATURE:
程式碼簽名的載入命令,描述了Mach-O
的程式碼簽名資訊,它屬於連結資訊,使用linkedit_data_command
結構體表示cmdsize:
載入命令所佔記憶體大小segname:
存放16位元組大小的段名字,當前是__PAGEZERO
。vmaddr:
段的虛擬記憶體起始地址vmsize:
段的虛擬記憶體大小fileoff:
段在檔案中偏移量filesize:
段在檔案大小maxprot:
段頁面所需要的最高記憶體保護(4=r,2=w,1=x)initprot:
段頁面初始的記憶體保護nsects:
段中包含section的數量flags:
其他雜項標誌位 在看MachOView
中的loadcommands
欄位:
以上為是應用程式所有載入命令,通過上面流程能夠看到對系統庫的載入順序。對比專案中引入的庫檔案,順序是一致的,如下圖:
以上載入命令含義如下:
LC_SEGMENT_64:
將檔案中的段對映到程式地址空間中LC_DYLD_INFO_ONLY:
動態連結相關資訊LC_SYMTAB:
符號表資訊,位置、偏移、資料個數,供dyld使用LC_DYSYMTAB:
動態符號表資訊,供dyld使用LC_LOAD_DYLINKER:
連結器資訊,記錄使用那些連結器完成核心後序的載入工作LC_UUID:Mach-O
檔案的唯一標識LC_VERSION_MIN_MACOSX:
支援最低作業系統版本LC_SOURCE_VERSION:
原始碼的版本號LC_MAIN:
設定主執行緒的入口即棧大小LC_LOAD_DYLIB:
依賴庫資訊,dyld
通過該命令去載入依賴庫LC_FUNCTION_STARTS:
函式的起始地址表LC_CODE_SIGNATURE:
程式碼簽名
3、Data
Data
區域由Segment
段和Section
節組成:
segment
主要有__TEXT
和__DATA
組成__text:
是主程式程式碼__stubs、__stub_helper:
是動態連結的樁__cstring:
程式中c語言字串__const:
常量
Section含義:
Section64(__TEXT,__objc_methname):
OC類名Section64(__DATA,__objc_classlist):
OC類列表Section64(__DATA,__objc_protollist):
OC原型列表Section64(__DATA,__objc_imageinfo):
OC映象資訊Section64(__DATA,__objc_selfrefs):
OC類自引用Section64(__DATA,__objc_superrefs):
OC類超類的引用Section64(__DATA,__ivar):
OC類成員變數
等等,都是通過section
來對OC
中的具體類別做載入的。segment
段分32位和64位,欄位相同,以64為例如下:
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:
是__text
,就是主程式程式碼- segname:該
section
所屬的segment
名,第一個是__TEXT
addr:
當前section
在記憶體中的起始位置size:
當前section
所佔記憶體大小offset:
當前section
的檔案偏移align:
位元組大小對齊reloff:
重定位入口的檔案偏移,0nreloc:
需要重定位的入口數量,0flags:
包含section
的type
和attributes
reserved1、reserved2
預留欄位