希望通過本文來記錄對于iOS開發對Mach-O需要有的基本了解。
蘋果推出Mach-O的背景:
- 過渡至基于 Mach 內核的操作系統:蘋果于 2001 年推出了 macOS(當時稱為 Mac OS X)操作系統,該操作系統采用了基于 Mach 內核的架構。為了適應新的操作系統架構,蘋果需要引入一種新的文件格式來支持該架構,用于可執行文件、靜態庫和動態庫的存儲和交互。
- 提高性能和可擴展性:Mach-O 文件格式相對于舊的目標文件格式(如 a.out 格式)具有更好的性能和可擴展性。它采用了更加緊湊和高效的數據結構,使得應用程序的加載、鏈接和執行更高效。這在日益復雜的應用程序和需求下變得尤為重要。
- 支持 Objective-C 和 Cocoa 框架:蘋果廣泛采用 Objective-C 編程語言和 Cocoa 框架來開發 macOS 和 iOS 上的應用程序。Mach-O 文件格式與 Objective-C 運行時和 Cocoa 框架的集成緊密相關,使得開發者可以更好地利用這些技術進行應用程序開發。
- 跨平臺支持和移植性:Mach-O 文件格式不僅用于 macOS 和 iOS,還可以支持其他基于 Darwin 內核的操作系統,如 tvOS 和 watchOS。這種一致的文件格式使得開發者可以更方便地在不同的蘋果平臺上共享和移植代碼,提高開發效率和代碼復用性。
- 操作系統集成:Mach-O 文件格式與蘋果操作系統的內核(XNU)緊密集成。蘋果控制了 Mach-O 格式的規范和解析器,從而使得操作系統和應用程序可以更緊密地進行交互和整合。
一、認識Mach-O
在Xcode
工程中,我們可以看到編譯設置里面有一個Mach-O type
, 可以看到主工程的格式是Executable
(可執行文件)。
而在組件化工程里,有一些本地或私有庫我們可能會在podspec
中聲明s.static_framework = true
,這樣就會是靜態庫;三方庫沒有這個聲明默認是動態庫。
靜態庫 | 動態庫 |
---|---|
假設本地庫Home模塊.png
|
AFNetworking.png
|
Mach-O
是 Mach Object
的縮寫,它是Mac/iOS 中用于存儲程序、庫的標準格式。作為 a.out
格式的替代,Mach-O
提供了更強的擴展性,并提升了符號表中信息的訪問速度。
類型 | 代表文件 |
---|---|
Executable(可執行文件) | xxx.app/xxx、推送擴展 |
Dynamic Library(動態庫文件) | .dylib(一般是系統動態庫)和 xxx.framework/xxx (三方動態庫) |
Bundle | 一種特定結構的文件夾,可以包含可執行文件、動態庫、靜態庫和各種資源文件,以及配置文件等,通常作為插件或擴展。需通過dlopen加載。 |
Static Library | 靜態庫文件(.a文件,是多個.o文件的集合),如pod庫聲明s.static_framework = true ,產物是靜態框架 |
Relocatable Object File | 目標文件(.o文件,編譯源代碼得到的中間文件) |
二、Mach-O的類型
1. 有哪些類型
我們可以在Xcode.app
查看到Mach-O
的類型定義如下,路徑為:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/mach-o/loader.h
#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 */
#define MH_FILESET 0xc /* a file composed of other Mach-Os to be run in the same userspace sharing a single linkedit. */
#define MH_GPU_EXECUTE 0xd /* gpu program */
#define MH_GPU_DYLIB 0xe /* gpu support functions */
2. 常見的Mach-O類型
MH_OBJECT
:目標文件即 .o 文件 以及靜態庫文件即 .a 文件(多個.o文件合并在一起);
MH_EXECUTE
:可執行文件,即App編譯運行后生成的可執行文件,在/Products路徑下;
MH_DYLIB
:動態庫文件,即.dylib文件 或者 .framework文件;
MH_DYLINKER
:/usr/lib/dyld路徑下的dyld文件;
MH_DSYM
:Xcode打包后生成的符號表文件,即.dSYM文件;
3. 如何查看mach-o文件類型
找到我們的APP后可通過命令行查看類型。
- 通過file命令
// 1.查看APP的可執行文件
file xxx.app/xxx
輸出: Mach-O 64-bit executable arm64
// 2.查看pod三方庫的machO
file xxx.app/Frameworks/AFNetworking.framework/AFNetworking
輸出:Mach-O 64-bit dynamically linked shared library arm64
三、Mach-O文件結構
項目 | 內容 |
---|---|
結構圖 | Mach-O文件結構.png
|
Header | 包含Mach-O文件的基本信息,例如文件類型,支持的CPU架構類型,加載指令的數量,所占內存大小等 |
Load Command | 不同數據段segment的加載命令,指導加載器加載數據 |
Data | 在Load Command中定義的Segment的原始數據。 |
四、查看Mach-O的方式
- 使用
MachOView
:https://github.com/gdbinit/MachOView - otool命令
otool -l <file>:顯示 Mach-O 文件的加載命令信息。
otool -t <file>:顯示 Mach-O 文件的文本節信息。
otool -L <file>:顯示 Mach-O 文件的依賴庫信息。
使用 man otool 命令查看 otool 的幫助文檔
- lipo命令
lipo -info 文件 // 查看架構信息
lipo <file> -thin 目標架構 -output 輸出文件 // 導出某種架構
lipo <file1> <file2> -output 輸出文件 // 合并多個架構
- objdump命令
objdump --macho --private-headers <file>
五、文件結構中各部分內容細節
源碼頭文件地址:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/mach-o/loader.h
1. Header
源碼中結構
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier 確定是64位還是32位 */
int32_t cputype; /* cpu specifier */
int32_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 */
};
// 通過otool命令查看一個Mach-O的頭部信息
otool -hv xxx
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 138 13384 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
2. Load Commands
這部分的作用是本質就是確定如何加載段segment數據,主結構是:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
可以通過MachOView來查看有哪些段命令.
項目 | 內容 |
---|---|
加載命令 | 加載命令段.png
|
LC_SEGMENT_64 | segment段加載指令 |
LC_DYLD_INF0_0NLY | 加載動態鏈接庫信息(重定向地址、弱引用綁定、懶加載綁定、開放函數等的偏移值等信息) |
... | ... |
2.1 其中LC_SEGMENT_64
命令的結構
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 段Segment的虛擬內存地址*/
uint32_t vmsize; /* memory size of this segment 段Segment的虛擬內存大小*/
uint32_t fileoff; /* file offset of this segment 段Segment的在文件中的偏移量*/
uint32_t filesize; /* amount to map from the file 段Segment在文件中所占的內存大小*/
vm_prot_t maxprot; /* maximum VM protection 表示頁面所需要的最高內存保護*/
vm_prot_t initprot; /* initial VM protection 表示頁面初始的內存保護*/
uint32_t nsects; /* number of sections in segment 段Segment包含節區sections的數量*/
uint32_t flags; /* flags 表示段的標志信息*/
}
- 結構體中
segname
是加載目標段Segment
的名稱,常見的段segment有四個(可以從上面圖中看到)
__PAGEZERO
: 在可執行文件有的,動態庫里沒有,這個段開始地址為0(NULL指針指向的位置),是一個不可讀、不可寫、不可執行的空間,能夠在空指針訪問時拋出異常。
__TEXT
:代碼段,里面主要是存放代碼的,該段是可讀可執行,但是不可寫;
__DATA
:數據段,里面主要是存放數據,該段是可讀可寫,但不可執行;
__LINKEDIT
:用于存放簽名信息,該段是只可讀,不可寫不可執行;
2.1 每個命令包含的內容
我們在MachOView展開LC_SEGMENT_64(__TEXT)
或LC_SEGMENT_64(__DATA)
,可以看到很多的section header
. 這個是數據段和代碼段的各個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
:當前section所隸屬的Segment,例如__TEXT(代碼段);
addr
: section在內存的起始位置;
size
: section所占內存大小;
offset
: section在文件中的偏移量;
align
:字節大小對齊,2的align次方;
reloff
:重定位入口的文件偏移;
nreloc
: 需要重定位的入口數量;
flags
:包含section的type和attributes;
3. Data部分
Data部分主要放的是__Text
段和__DATA
段的數據,根據不同功能分為不同的節(section)。
段數據的頭部信息是存放在Load Commands
中的.
代碼段和數據段的各個節分別代表什么可以通過這篇linkmap文章來了解
通過LinkMap來了解Mach-O
六、MachO里面各部分占用大小
我們還可以通過size命令行查看一個Mach-O大小信息:
// 假設app工程名xxx,
// -l 參數可以顯示目標文件的完整節(section)和段(segment)信息
// -m 參數用于指定目標文件的格式
size -l -m xxx.app/xxx
輸出文件信息包含四個部分:
Segment __PAGEZERO
Segment __TEXT
Segment __DATA
Segment __LINKEDIT
具體的信息可以打印你APP的對照看
接下來看看每個段的打印信息具體是什么意思。
Segment __PAGEZERO: 4294967296 (zero fill) (vmaddr 0x0 fileoff 0)
該段信息指的是一個名為 __PAGEZERO 的段(segment),其大小為 4GB,對應的是零填充的內存。__PAGEZERO 段的虛擬內存地址(vmaddr)為 0x0,文件偏移量(fileoff)為 0。Segment __TEXT: 43319296 (vmaddr 0x100000000 fileoff 0)
的意思?
該段信息指的是一個名為__TEXT
的段(segment),其大小為 約等于 41.31 MB。__TEXT
段的虛擬內存地址(vmaddr
)為 0x100000000,文件偏移量(fileoff
)為 0。虛擬內存地址指示了段在程序運行時被加載到內存的位置,而文件偏移量指示了段在目標文件中的位置。Segment __DATA: 5849088 (vmaddr 0x102950000 fileoff 43319296)
該段信息指的是一個名為 __DATA 的段(segment),其大小約等于 5.572 MB。__DATA 段的虛擬內存地址(vmaddr)為 0x102950000,文件偏移量(fileoff)為 43319296。Segment __LINKEDIT: 2736128 (vmaddr 0x102ee4000 fileoff 48840704)
該段信息指的是一個名為 __LINKEDIT 的段(segment),其大小約等于 2.61 MB。__LINKEDIT 段的虛擬內存地址(vmaddr)為 0x102ee4000,文件偏移量(fileoff)為 48840704。
1. __PAGEZERO
在 Mach-O 文件格式中,__PAGEZERO 段用于標識虛擬內存空間的起始位置,并指示該段之前的內存區域應該被清零填充。__PAGEZERO(又稱為 Page Zero)是一個特殊的節(Section)名稱,它在 Mach-O(Mach Object)文件中定義了虛擬地址空間的第一個頁。它沒有任何實際的可執行代碼或數據,僅作為一種占位符存在,用于確保虛擬內存空間的連續性和保護。
一些主要的特點和作用如下:
安全保護:__PAGEZERO 節的存在是為了提供一種安全機制,用于檢測和防止針對軟件漏洞的攻擊。通過將可執行文件或可加載文件的第一個頁設置為沒有權限的頁,可以防止非法訪問者利用指向第一個頁的指針進行漏洞利用。
空節:__PAGEZERO 節本身不包含實際的代碼或數據。它的大小通常為0字節,所以在執行時不會占用任何實際內存。
地址空間布局:__PAGEZERO 節通常位于可執行文件或可加載文件的開始位置,即位于虛擬地址空間的最低部分。它的存在確保了后續節的虛擬地址是從一個明確定義的位置開始的。
2.__TEXT
在 Mach-O 文件格式中,__TEXT
段包含了可執行程序的實際代碼和只讀數據。它是二進制文件中的一個重要段,存儲了程序的代碼段和只讀數據段。
3.__DATA
在 Mach-O 文件格式中,__DATA 段存儲了可執行程序的靜態變量和全局變量等數據。它包含了程序在運行時的可寫數據段,即存儲程序在運行過程中產生的數據的空間。
4. __LINKEDIT
在 Mach-O 文件格式中,__LINKEDIT 段存儲與鏈接器相關的信息,比如符號表、重定位信息等。鏈接器主要負責將不同的目標文件合并成可執行文件,__LINKEDIT 段存儲與該過程相關的一些信息,因此該段也被稱為鏈接器的信息段。
具體來說,__LINKEDIT 段包含以下內容:
- 符號表(Symbol Table):用于存儲程序中定義和引用的各種符號(變量、函數、類等)的信息,鏈接器通過符號表進行符號解析和重定位等操作。
- 字符串表(String Table):存儲符號表中的字符串,用于標識符號的名稱。
- 動態符號表(Dynamic Symbol Table):包含一些在運行時動態加載的符號信息。
- 重定位表(Relocation Table):存儲在鏈接過程中需要進行地址重定位的部分,包括指令中需要修改的地址和重定位類型等信息。
__LINKEDIT 段的存在使得鏈接器能夠在程序運行時解析和重定位符號,從而正確地連接和加載各種模塊。